diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 7b85bd3323..aba2c0d52f 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -17132,7 +17132,8 @@ const docTemplate = `{ "paused", "completed", "error", - "requires_action" + "requires_action", + "interrupting" ], "x-enum-varnames": [ "ChatStatusWaiting", @@ -17141,7 +17142,8 @@ const docTemplate = `{ "ChatStatusPaused", "ChatStatusCompleted", "ChatStatusError", - "ChatStatusRequiresAction" + "ChatStatusRequiresAction", + "ChatStatusInterrupting" ] }, "codersdk.ChatStreamActionRequired": { diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 0bff17f6dc..0a6290d7fe 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -15457,7 +15457,8 @@ "paused", "completed", "error", - "requires_action" + "requires_action", + "interrupting" ], "x-enum-varnames": [ "ChatStatusWaiting", @@ -15466,7 +15467,8 @@ "ChatStatusPaused", "ChatStatusCompleted", "ChatStatusError", - "ChatStatusRequiresAction" + "ChatStatusRequiresAction", + "ChatStatusInterrupting" ] }, "codersdk.ChatStreamActionRequired": { diff --git a/coderd/database/dbauthz/dbauthz.go b/coderd/database/dbauthz/dbauthz.go index 086a35d229..6542ec0be2 100644 --- a/coderd/database/dbauthz/dbauthz.go +++ b/coderd/database/dbauthz/dbauthz.go @@ -1813,6 +1813,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) @@ -1903,6 +1907,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 { @@ -1914,6 +1922,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 @@ -1960,6 +1972,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 @@ -1992,6 +2008,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 @@ -2265,6 +2285,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 @@ -2750,6 +2774,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") +} + // TODO (PR #24810): Replace rbac.ResourceAuditLog with dedicated boundary_log resource type. func (q *querier) GetBoundaryLogByID(ctx context.Context, id uuid.UUID) (database.BoundaryLog, error) { if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceAuditLog); err != nil { @@ -2974,6 +3002,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 { @@ -3040,6 +3072,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 @@ -3148,6 +3184,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 { @@ -3156,6 +3200,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. @@ -3237,6 +3285,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 @@ -3259,6 +3311,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) } @@ -3329,6 +3385,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. @@ -5365,6 +5425,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) } @@ -5513,6 +5577,14 @@ 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) InsertChatWithSnapshot(ctx context.Context, arg database.InsertChatWithSnapshotParams) (database.Chat, 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 @@ -6071,6 +6143,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 { @@ -6257,6 +6333,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()) @@ -6363,6 +6443,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 @@ -6605,6 +6689,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 @@ -8066,6 +8154,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 fd4537ccec..ecbe1bd363 100644 --- a/coderd/database/dbmetrics/querymetrics.go +++ b/coderd/database/dbmetrics/querymetrics.go @@ -177,14 +177,6 @@ func (m queryMetricsStore) ArchiveUnusedTemplateVersions(ctx context.Context, ar return r0, r1 } -func (m queryMetricsStore) AutoArchiveInactiveChats(ctx context.Context, arg database.AutoArchiveInactiveChatsParams) ([]database.AutoArchiveInactiveChatsRow, error) { - start := time.Now() - r0, r1 := m.s.AutoArchiveInactiveChats(ctx, arg) - m.queryLatencies.WithLabelValues("AutoArchiveInactiveChats").Observe(time.Since(start).Seconds()) - m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "AutoArchiveInactiveChats").Inc() - return r0, r1 -} - func (m queryMetricsStore) BackoffChatDiffStatus(ctx context.Context, arg database.BackoffChatDiffStatusParams) error { start := time.Now() r0 := m.s.BackoffChatDiffStatus(ctx, arg) @@ -321,6 +313,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) @@ -409,6 +409,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) @@ -417,6 +425,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) @@ -457,6 +473,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) @@ -489,6 +513,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) @@ -769,6 +801,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) @@ -1257,6 +1297,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) @@ -1305,6 +1353,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) @@ -1433,6 +1489,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) @@ -1465,6 +1529,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) @@ -1561,6 +1633,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) @@ -1569,6 +1657,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) @@ -1641,6 +1737,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) @@ -1665,6 +1769,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) @@ -1737,6 +1849,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) @@ -3665,6 +3785,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) @@ -3817,6 +3945,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) @@ -4313,6 +4449,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) @@ -4497,6 +4641,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) @@ -4585,6 +4737,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) @@ -4777,6 +4937,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) @@ -4865,6 +5033,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) @@ -5809,6 +5985,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 36f8429e8f..2af6fe1098 100644 --- a/coderd/database/dbmock/dbmock.go +++ b/coderd/database/dbmock/dbmock.go @@ -178,21 +178,6 @@ func (mr *MockStoreMockRecorder) ArchiveUnusedTemplateVersions(ctx, arg any) *go return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ArchiveUnusedTemplateVersions", reflect.TypeOf((*MockStore)(nil).ArchiveUnusedTemplateVersions), ctx, arg) } -// AutoArchiveInactiveChats mocks base method. -func (m *MockStore) AutoArchiveInactiveChats(ctx context.Context, arg database.AutoArchiveInactiveChatsParams) ([]database.AutoArchiveInactiveChatsRow, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "AutoArchiveInactiveChats", ctx, arg) - ret0, _ := ret[0].([]database.AutoArchiveInactiveChatsRow) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// AutoArchiveInactiveChats indicates an expected call of AutoArchiveInactiveChats. -func (mr *MockStoreMockRecorder) AutoArchiveInactiveChats(ctx, arg any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AutoArchiveInactiveChats", reflect.TypeOf((*MockStore)(nil).AutoArchiveInactiveChats), ctx, arg) -} - // BackoffChatDiffStatus mocks base method. func (m *MockStore) BackoffChatDiffStatus(ctx context.Context, arg database.BackoffChatDiffStatusParams) error { m.ctrl.T.Helper() @@ -498,6 +483,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() @@ -659,6 +659,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() @@ -673,6 +687,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() @@ -746,6 +775,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() @@ -802,6 +846,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() @@ -1304,6 +1363,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() @@ -2325,6 +2399,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() @@ -2415,6 +2504,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() @@ -2655,6 +2759,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() @@ -2715,6 +2834,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() @@ -2895,6 +3029,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() @@ -2910,6 +3074,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() @@ -3045,6 +3224,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() @@ -3090,6 +3284,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() @@ -3225,6 +3434,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() @@ -6884,6 +7108,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() @@ -7169,6 +7408,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() @@ -8084,6 +8338,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() @@ -8504,6 +8773,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() @@ -8696,6 +8980,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() @@ -9042,6 +9341,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() @@ -9205,6 +9519,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() @@ -10914,6 +11243,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 7fe8b7b80f..c8211cc869 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -333,7 +333,8 @@ CREATE TYPE chat_status AS ENUM ( 'paused', 'completed', 'error', - 'requires_action' + 'requires_action', + 'interrupting' ); CREATE TYPE connection_status AS ENUM ( @@ -706,6 +707,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 $$ @@ -1175,6 +1199,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_model_prices ( provider text NOT NULL, model text NOT NULL, @@ -1533,6 +1654,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, @@ -1554,7 +1681,8 @@ CREATE TABLE chat_messages ( total_cost_micros bigint, runtime_ms bigint, deleted boolean DEFAULT false NOT NULL, - provider_response_id text + provider_response_id text, + revision bigint NOT NULL ); CREATE SEQUENCE chat_messages_id_seq @@ -1588,12 +1716,21 @@ 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 + model_config_id uuid, + "position" bigint DEFAULT nextval('chat_queued_messages_position_seq'::regclass) NOT NULL, + created_by uuid NOT NULL ); CREATE SEQUENCE chat_queued_messages_id_seq @@ -1658,6 +1795,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))), @@ -1745,6 +1890,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, @@ -3689,6 +3842,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); @@ -4022,6 +4178,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); @@ -4134,6 +4292,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)); @@ -4378,6 +4538,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(); @@ -4388,6 +4554,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(); @@ -4453,6 +4629,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_chat_id_fkey FOREIGN KEY (chat_id) REFERENCES chats(id) ON DELETE CASCADE; diff --git a/coderd/database/foreign_key_constraint.go b/coderd/database/foreign_key_constraint.go index 47dba3d673..1467f12302 100644 --- a/coderd/database/foreign_key_constraint.go +++ b/coderd/database/foreign_key_constraint.go @@ -21,6 +21,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; 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); ForeignKeyChatModelConfigsAiProviderID ForeignKeyConstraint = "chat_model_configs_ai_provider_id_fkey" // ALTER TABLE ONLY chat_model_configs ADD CONSTRAINT chat_model_configs_ai_provider_id_fkey FOREIGN KEY (ai_provider_id) REFERENCES ai_providers(id); diff --git a/coderd/database/modelqueries.go b/coderd/database/modelqueries.go index 74aa53859c..2beb0ca403 100644 --- a/coderd/database/modelqueries.go +++ b/coderd/database/modelqueries.go @@ -824,6 +824,8 @@ func (q *sqlQuerier) GetAuthorizedChats(ctx context.Context, arg GetChatsParams, &i.Chat.HistoryVersion, &i.Chat.QueueVersion, &i.Chat.GenerationAttempt, + &i.Chat.RetryState, + &i.Chat.RetryStateVersion, &i.Chat.RunnerID, &i.Chat.RequiresActionDeadlineAt, &i.Chat.UserACL, @@ -897,6 +899,8 @@ func (q *sqlQuerier) GetAuthorizedChatsByChatFileID(ctx context.Context, fileID &i.HistoryVersion, &i.QueueVersion, &i.GenerationAttempt, + &i.RetryState, + &i.RetryStateVersion, &i.RunnerID, &i.RequiresActionDeadlineAt, &i.UserACL, diff --git a/coderd/database/models.go b/coderd/database/models.go index 593d89e4d1..49d3aeb5e0 100644 --- a/coderd/database/models.go +++ b/coderd/database/models.go @@ -1543,6 +1543,7 @@ const ( ChatStatusCompleted ChatStatus = "completed" ChatStatusError ChatStatus = "error" ChatStatusRequiresAction ChatStatus = "requires_action" + ChatStatusInterrupting ChatStatus = "interrupting" ) func (e *ChatStatus) Scan(src interface{}) error { @@ -1588,7 +1589,8 @@ func (e ChatStatus) Valid() bool { ChatStatusPaused, ChatStatusCompleted, ChatStatusError, - ChatStatusRequiresAction: + ChatStatusRequiresAction, + ChatStatusInterrupting: return true } return false @@ -1603,6 +1605,7 @@ func AllChatStatusValues() []ChatStatus { ChatStatusCompleted, ChatStatusError, ChatStatusRequiresAction, + ChatStatusInterrupting, } } @@ -4564,38 +4567,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 { @@ -4677,6 +4688,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"` @@ -4699,6 +4716,7 @@ type ChatMessage struct { RuntimeMs sql.NullInt64 `db:"runtime_ms" json:"runtime_ms"` Deleted bool `db:"deleted" json:"deleted"` ProviderResponseID sql.NullString `db:"provider_response_id" json:"provider_response_id"` + Revision int64 `db:"revision" json:"revision"` } type ChatModelConfig struct { @@ -4726,39 +4744,49 @@ type ChatQueuedMessage struct { Content json.RawMessage `db:"content" json:"content"` CreatedAt time.Time `db:"created_at" json:"created_at"` ModelConfigID uuid.NullUUID `db:"model_config_id" json:"model_config_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 6b16e0771a..1e130a1b4c 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -62,14 +62,6 @@ type sqlcQuerier interface { // Only unused template versions will be archived, which are any versions not // referenced by the latest build of a workspace. ArchiveUnusedTemplateVersions(ctx context.Context, arg ArchiveUnusedTemplateVersionsParams) ([]uuid.UUID, error) - // Archives inactive root chats (pinned and already-archived chats skipped), - // cascading to children via root_chat_id. Limits apply to roots, not total - // rows. The Go caller passes @archive_cutoff as UTC midnight so that all - // chats sharing the same last-activity date are archived together. - // Used by dbpurge. - // created_at ASC flows through to dbpurge's digest truncation; see - // buildDigestData in dbpurge.go for the tradeoff rationale. - AutoArchiveInactiveChats(ctx context.Context, arg AutoArchiveInactiveChatsParams) ([]AutoArchiveInactiveChatsRow, error) BackoffChatDiffStatus(ctx context.Context, arg BackoffChatDiffStatusParams) error BatchUpdateWorkspaceAgentMetadata(ctx context.Context, arg BatchUpdateWorkspaceAgentMetadataParams) error BatchUpdateWorkspaceLastUsedAt(ctx context.Context, arg BatchUpdateWorkspaceLastUsedAtParams) error @@ -89,6 +81,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. @@ -105,7 +100,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 @@ -123,10 +122,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) @@ -195,6 +200,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) @@ -316,6 +322,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) @@ -327,6 +337,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. @@ -364,6 +375,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 @@ -371,6 +386,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; @@ -393,7 +409,12 @@ 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. @@ -423,11 +444,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 @@ -444,6 +472,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) @@ -907,6 +939,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) @@ -936,7 +970,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 @@ -1016,6 +1057,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 @@ -1066,6 +1112,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. @@ -1090,6 +1149,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) @@ -1191,6 +1253,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 @@ -1209,11 +1276,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) @@ -1221,6 +1286,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) @@ -1367,6 +1435,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 270f017e57..448f6648f8 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -5752,7 +5752,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 @@ -5784,6 +5784,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, @@ -5793,7 +5801,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 ` @@ -5843,6 +5851,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, @@ -5985,7 +6001,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 @@ -6017,6 +6033,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, @@ -6026,7 +6050,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 ` @@ -6069,6 +6093,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, @@ -6087,158 +6119,6 @@ func (q *sqlQuerier) ArchiveChatByID(ctx context.Context, id uuid.UUID) ([]Chat, return items, nil } -const autoArchiveInactiveChats = `-- name: AutoArchiveInactiveChats :many -WITH to_archive AS ( - SELECT - c.id, - -- Activity = MAX(cm.created_at) across the family, or c.created_at - -- when the family has no non-deleted messages. - COALESCE(activity.last_activity_at, c.created_at) AS last_activity_at - FROM chats c - LEFT JOIN LATERAL ( - SELECT MAX(cm.created_at) AS last_activity_at - FROM chat_messages cm - JOIN chats fc ON fc.id = cm.chat_id - WHERE (fc.id = c.id OR fc.root_chat_id = c.id) - AND cm.deleted = false - ) activity ON TRUE - WHERE c.archived = false - AND c.pin_order = 0 - AND c.parent_chat_id IS NULL -- roots only - -- Redundant filter helps the planner use the partial index on created_at. - AND c.created_at < $1::timestamptz - -- New active statuses must be added here to prevent archiving. - AND c.status NOT IN ('running', 'pending', 'paused', 'requires_action') - AND COALESCE(activity.last_activity_at, c.created_at) < $1::timestamptz - -- Sorting by created_at lets Postgres drive the scan from the - -- partial index instead of evaluating every LATERAL subquery - -- before sorting. All candidates are past the cutoff, so the - -- archive order is immaterial once the backlog drains. - ORDER BY c.created_at ASC - LIMIT $2 -), -archived AS ( - UPDATE chats c - SET archived = true, pin_order = 0, updated_at = NOW() - 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 -) -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, - -- Children inherit their root's activity so last_activity_at is never null. - COALESCE( - t.last_activity_at, - (SELECT tr.last_activity_at FROM to_archive tr WHERE tr.id = a.root_chat_id), - a.created_at - )::timestamptz AS last_activity_at -FROM archived a -LEFT JOIN to_archive t ON t.id = a.id -ORDER BY (a.root_chat_id IS NULL) DESC, a.owner_id ASC, a.created_at ASC, a.id ASC -` - -type AutoArchiveInactiveChatsParams struct { - ArchiveCutoff time.Time `db:"archive_cutoff" json:"archive_cutoff"` - LimitCount int32 `db:"limit_count" json:"limit_count"` -} - -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"` -} - -// Archives inactive root chats (pinned and already-archived chats skipped), -// cascading to children via root_chat_id. Limits apply to roots, not total -// rows. The Go caller passes @archive_cutoff as UTC midnight so that all -// chats sharing the same last-activity date are archived together. -// Used by dbpurge. -// created_at ASC flows through to dbpurge's digest truncation; see -// buildDigestData in dbpurge.go for the tradeoff rationale. -func (q *sqlQuerier) AutoArchiveInactiveChats(ctx context.Context, arg AutoArchiveInactiveChatsParams) ([]AutoArchiveInactiveChatsRow, error) { - rows, err := q.db.QueryContext(ctx, autoArchiveInactiveChats, arg.ArchiveCutoff, arg.LimitCount) - if err != nil { - return nil, err - } - defer rows.Close() - var items []AutoArchiveInactiveChatsRow - for rows.Next() { - var i AutoArchiveInactiveChatsRow - 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.UserACL, - &i.GroupACL, - &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 backoffChatDiffStatus = `-- name: BackoffChatDiffStatus :exec UPDATE chat_diff_statuses @@ -6275,6 +6155,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 @@ -6299,6 +6194,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 ` @@ -6308,6 +6214,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 ` @@ -6322,6 +6263,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 ` @@ -6373,8 +6335,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 @@ -6423,6 +6398,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, @@ -6441,6 +6424,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, @@ -6464,7 +6593,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 ` @@ -6501,6 +6630,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, @@ -6511,7 +6754,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 @@ -6546,6 +6789,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, @@ -6555,7 +6806,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 ` @@ -6591,6 +6842,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, @@ -7127,9 +7386,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 + 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, revision FROM chat_messages WHERE @@ -7162,6 +7471,7 @@ func (q *sqlQuerier) GetChatMessageByID(ctx context.Context, id int64) (ChatMess &i.RuntimeMs, &i.Deleted, &i.ProviderResponseID, + &i.Revision, ) return i, err } @@ -7251,7 +7561,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 + 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, revision FROM chat_messages WHERE @@ -7299,6 +7609,7 @@ func (q *sqlQuerier) GetChatMessagesByChatID(ctx context.Context, arg GetChatMes &i.RuntimeMs, &i.Deleted, &i.ProviderResponseID, + &i.Revision, ); err != nil { return nil, err } @@ -7315,7 +7626,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 + 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, revision FROM chat_messages WHERE @@ -7366,6 +7677,7 @@ func (q *sqlQuerier) GetChatMessagesByChatIDAscPaginated(ctx context.Context, ar &i.RuntimeMs, &i.Deleted, &i.ProviderResponseID, + &i.Revision, ); err != nil { return nil, err } @@ -7382,7 +7694,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 + 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, revision FROM chat_messages WHERE @@ -7446,6 +7758,7 @@ func (q *sqlQuerier) GetChatMessagesByChatIDDescPaginated(ctx context.Context, a &i.RuntimeMs, &i.Deleted, &i.ProviderResponseID, + &i.Revision, ); err != nil { return nil, err } @@ -7478,7 +7791,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 + 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, revision FROM chat_messages WHERE @@ -7550,6 +7863,7 @@ func (q *sqlQuerier) GetChatMessagesForPromptByChatID(ctx context.Context, chatI &i.RuntimeMs, &i.Deleted, &i.ProviderResponseID, + &i.Revision, ); err != nil { return nil, err } @@ -7610,8 +7924,56 @@ 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, 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.Position, + &i.CreatedBy, + ) + return i, err +} + +const getChatQueuedMessageHead = `-- name: GetChatQueuedMessageHead :one +SELECT id, chat_id, content, created_at, model_config_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.Position, + &i.CreatedBy, + ) + return i, err +} + const getChatQueuedMessages = `-- name: GetChatQueuedMessages :many -SELECT id, chat_id, content, created_at, model_config_id FROM chat_queued_messages +SELECT id, chat_id, content, created_at, model_config_id, position, created_by FROM chat_queued_messages WHERE chat_id = $1 ORDER BY created_at ASC, id ASC ` @@ -7631,6 +7993,46 @@ func (q *sqlQuerier) GetChatQueuedMessages(ctx context.Context, chatID uuid.UUID &i.Content, &i.CreatedAt, &i.ModelConfigID, + &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, 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.Position, + &i.CreatedBy, ); err != nil { return nil, err } @@ -7766,6 +8168,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 @@ -7776,7 +8332,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 @@ -8008,6 +8564,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, @@ -8029,7 +8593,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 @@ -8080,6 +8644,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, @@ -8099,7 +8742,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[]) @@ -8144,6 +8787,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, @@ -8232,7 +8883,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 @@ -8305,6 +8956,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, @@ -8324,9 +8983,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 + 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, revision FROM chat_messages WHERE @@ -8369,13 +9042,14 @@ func (q *sqlQuerier) GetLastChatMessageByRole(ctx context.Context, arg GetLastCh &i.RuntimeMs, &i.Deleted, &i.ProviderResponseID, + &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 @@ -8436,6 +9110,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, @@ -8516,6 +9198,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 ( @@ -8553,7 +9250,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 @@ -8585,6 +9282,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, @@ -8594,7 +9299,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 ` @@ -8666,6 +9371,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, @@ -8743,7 +9456,7 @@ SELECT NULLIF(UNNEST($17::bigint[]), 0), NULLIF(UNNEST($18::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 + 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, revision ` type InsertChatMessagesParams struct { @@ -8817,6 +9530,7 @@ func (q *sqlQuerier) InsertChatMessages(ctx context.Context, arg InsertChatMessa &i.RuntimeMs, &i.Deleted, &i.ProviderResponseID, + &i.Revision, ); err != nil { return nil, err } @@ -8832,13 +9546,15 @@ 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) -VALUES ( - $1, - $2, - $3::uuid -) -RETURNING id, chat_id, content, created_at, model_config_id +INSERT INTO chat_queued_messages (chat_id, content, model_config_id, created_by) +SELECT + $1::uuid, + $2::jsonb, + $3::uuid, + chats.owner_id +FROM chats +WHERE chats.id = $1::uuid +RETURNING id, chat_id, content, created_at, model_config_id, position, created_by ` type InsertChatQueuedMessageParams struct { @@ -8847,6 +9563,9 @@ type InsertChatQueuedMessageParams struct { ModelConfigID uuid.NullUUID `db:"model_config_id" json:"model_config_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, arg.Content, arg.ModelConfigID) var i ChatQueuedMessage @@ -8856,10 +9575,79 @@ func (q *sqlQuerier) InsertChatQueuedMessage(ctx context.Context, arg InsertChat &i.Content, &i.CreatedAt, &i.ModelConfigID, + &i.Position, + &i.CreatedBy, ) return i, err } +const insertChatQueuedMessageWithCreator = `-- name: InsertChatQueuedMessageWithCreator :one +INSERT INTO chat_queued_messages (chat_id, content, model_config_id, created_by) +VALUES ( + $1::uuid, + $2::jsonb, + $3::uuid, + $4::uuid +) +RETURNING id, chat_id, content, created_at, model_config_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"` + 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.CreatedBy, + ) + var i ChatQueuedMessage + err := row.Scan( + &i.ID, + &i.ChatID, + &i.Content, + &i.CreatedAt, + &i.ModelConfigID, + &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 @@ -9011,6 +9799,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 @@ -9080,7 +9990,7 @@ WHERE id = ( ORDER BY cqm.created_at ASC, cqm.id ASC LIMIT 1 ) -RETURNING id, chat_id, content, created_at, model_config_id +RETURNING id, chat_id, content, created_at, model_config_id, position, created_by ` func (q *sqlQuerier) PopNextQueuedMessage(ctx context.Context, chatID uuid.UUID) (ChatQueuedMessage, error) { @@ -9092,6 +10002,8 @@ func (q *sqlQuerier) PopNextQueuedMessage(ctx context.Context, chatID uuid.UUID) &i.Content, &i.CreatedAt, &i.ModelConfigID, + &i.Position, + &i.CreatedBy, ) return i, err } @@ -9121,6 +10033,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 @@ -9230,7 +10171,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 @@ -9262,6 +10203,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, @@ -9271,7 +10220,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 ` @@ -9318,6 +10267,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, @@ -9424,7 +10381,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 @@ -9456,6 +10413,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, @@ -9465,7 +10430,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 ` @@ -9507,6 +10472,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, @@ -9524,7 +10497,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 @@ -9556,6 +10529,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, @@ -9565,7 +10546,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 ` @@ -9606,6 +10587,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, @@ -9668,7 +10792,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 @@ -9700,6 +10824,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, @@ -9709,7 +10841,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 ` @@ -9750,6 +10882,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, @@ -9764,7 +10904,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 @@ -9796,6 +10936,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, @@ -9805,7 +10953,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 ` @@ -9850,6 +10998,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, @@ -9867,7 +11023,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 @@ -9899,6 +11055,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, @@ -9908,7 +11072,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 ` @@ -9949,6 +11113,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, @@ -9983,26 +11155,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 } @@ -10018,7 +11188,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 @@ -10050,6 +11220,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, @@ -10059,7 +11237,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 ` @@ -10100,6 +11278,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, @@ -10117,7 +11303,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 + 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, revision ` type UpdateChatMessageByIDParams struct { @@ -10151,6 +11337,7 @@ func (q *sqlQuerier) UpdateChatMessageByID(ctx context.Context, arg UpdateChatMe &i.RuntimeMs, &i.Deleted, &i.ProviderResponseID, + &i.Revision, ) return i, err } @@ -10235,7 +11422,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 @@ -10267,6 +11454,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, @@ -10276,7 +11471,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 ` @@ -10317,6 +11512,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, @@ -10325,20 +11528,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 @@ -10370,6 +11567,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, @@ -10379,7 +11704,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 ` @@ -10431,6 +11756,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, @@ -10452,7 +11785,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 @@ -10484,6 +11817,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, @@ -10493,7 +11834,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 ` @@ -10547,6 +11888,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, @@ -10566,7 +11915,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 @@ -10598,6 +11947,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, @@ -10607,7 +11964,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 ` @@ -10648,6 +12005,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, @@ -10664,7 +12029,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 @@ -10696,6 +12061,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, @@ -10705,7 +12078,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 ` @@ -10753,6 +12126,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, @@ -10980,6 +12361,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 8ef517a9cb..df2a136598 100644 --- a/coderd/database/unique_constraint.go +++ b/coderd/database/unique_constraint.go @@ -25,6 +25,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/docs/admin/security/audit-logs.md b/docs/admin/security/audit-logs.md index 712724e064..d8c0655709 100644 --- a/docs/admin/security/audit-logs.md +++ b/docs/admin/security/audit-logs.md @@ -23,7 +23,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
public_keytrue
updated_atfalse
user_idtrue
| | GroupSyncSettings
| |
FieldTracked
auto_create_missing_groupstrue
fieldtrue
legacy_group_name_mappingfalse
mappingtrue
regex_filtertrue
| diff --git a/docs/reference/api/chats.md b/docs/reference/api/chats.md index 758f6641f5..909cf2b24f 100644 --- a/docs/reference/api/chats.md +++ b/docs/reference/api/chats.md @@ -298,7 +298,7 @@ Status Code **200** | `kind` | `auth`, `config`, `generic`, `overloaded`, `rate_limit`, `startup_timeout`, `timeout`, `usage_limit` | | `type` | `context-file`, `file`, `file-reference`, `reasoning`, `skill`, `source`, `text`, `tool-call`, `tool-result` | | `plan_mode` | `plan` | -| `status` | `completed`, `error`, `paused`, `pending`, `requires_action`, `running`, `waiting` | +| `status` | `completed`, `error`, `interrupting`, `paused`, `pending`, `requires_action`, `running`, `waiting` | To perform this operation, you must be authenticated. [Learn more](authentication.md). diff --git a/docs/reference/api/schemas.md b/docs/reference/api/schemas.md index c2cffd13d4..70cf8c4fbe 100644 --- a/docs/reference/api/schemas.md +++ b/docs/reference/api/schemas.md @@ -3554,9 +3554,9 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in #### Enumerated Values -| Value(s) | -|------------------------------------------------------------------------------------| -| `completed`, `error`, `paused`, `pending`, `requires_action`, `running`, `waiting` | +| Value(s) | +|----------------------------------------------------------------------------------------------------| +| `completed`, `error`, `interrupting`, `paused`, `pending`, `requires_action`, `running`, `waiting` | ## codersdk.ChatStreamActionRequired diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index af46758f9f..41255aa499 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -2714,6 +2714,7 @@ export interface ChatSourcePart { export type ChatStatus = | "completed" | "error" + | "interrupting" | "paused" | "pending" | "requires_action" @@ -2723,6 +2724,7 @@ export type ChatStatus = export const ChatStatuses: ChatStatus[] = [ "completed", "error", + "interrupting", "paused", "pending", "requires_action",