feat: add organization scoping to chats (#23827)

Fixes https://github.com/coder/internal/issues/1436

* Adds organization_id to chats with backfill (workspace org → user org membership → default org)
* No support yet for ACLs (follow-up issue)
- Cross-org workspace binding rejected (both in `CreateChatRequest` and in `create_workspace` tool
- Adds `OrganizationAutocomplete` to `AgentCreateForm`
- Docs updated with `organization_id` in chats-api.md

> 🤖 Written by a Coder Agent. Reviewed by many humans and many agents.

---------

Co-authored-by: Mathias Fredriksson <mafredri@gmail.com>
This commit is contained in:
Cian Johnston
2026-04-13 12:31:25 +01:00
committed by GitHub
co-authored by Mathias Fredriksson
parent cb0b84a2d3
commit 22062ec52e
58 changed files with 1533 additions and 581 deletions
+1
View File
@@ -1550,6 +1550,7 @@ func Chat(c database.Chat, diffStatus *database.ChatDiffStatus, files []database
}
chat := codersdk.Chat{
ID: c.ID,
OrganizationID: c.OrganizationID,
OwnerID: c.OwnerID,
LastModelConfigID: c.LastModelConfigID,
Title: c.Title,
+1
View File
@@ -530,6 +530,7 @@ func TestChat_AllFieldsPopulated(t *testing.T) {
input := database.Chat{
ID: uuid.New(),
OwnerID: uuid.New(),
OrganizationID: uuid.New(),
WorkspaceID: uuid.NullUUID{UUID: uuid.New(), Valid: true},
BuildID: uuid.NullUUID{UUID: uuid.New(), Valid: true},
AgentID: uuid.NullUUID{UUID: uuid.New(), Valid: true},
+1 -1
View File
@@ -4846,7 +4846,7 @@ func (q *querier) InsertAuditLog(ctx context.Context, arg database.InsertAuditLo
}
func (q *querier) InsertChat(ctx context.Context, arg database.InsertChatParams) (database.Chat, error) {
return insert(q.log, q.auth, rbac.ResourceChat.WithOwner(arg.OwnerID.String()), q.db.InsertChat)(ctx, arg)
return insert(q.log, q.auth, rbac.ResourceChat.WithOwner(arg.OwnerID.String()).InOrg(arg.OrganizationID), q.db.InsertChat)(ctx, arg)
}
func (q *querier) InsertChatFile(ctx context.Context, arg database.InsertChatFileParams) (database.InsertChatFileRow, error) {
+1 -1
View File
@@ -783,7 +783,7 @@ func (s *MethodTestSuite) TestChats() {
})
chat := testutil.Fake(s.T(), faker, database.Chat{OwnerID: arg.OwnerID})
dbm.EXPECT().InsertChat(gomock.Any(), arg).Return(chat, nil).AnyTimes()
check.Args(arg).Asserts(rbac.ResourceChat.WithOwner(arg.OwnerID.String()), policy.ActionCreate).Returns(chat)
check.Args(arg).Asserts(rbac.ResourceChat.WithOwner(arg.OwnerID.String()).InOrg(arg.OrganizationID), policy.ActionCreate).Returns(chat)
}))
s.Run("InsertChatFile", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
arg := testutil.Fake(s.T(), faker, database.InsertChatFileParams{})
+16 -13
View File
@@ -1670,9 +1670,10 @@ func TestDeleteOldChatFiles(t *testing.T) {
// createChat inserts a chat and optionally archives it, then
// backdates updated_at to control the "archived since" window.
createChat := func(ctx context.Context, t *testing.T, db database.Store, rawDB *sql.DB, ownerID, modelConfigID uuid.UUID, archived bool, updatedAt time.Time) database.Chat {
createChat := func(ctx context.Context, t *testing.T, db database.Store, rawDB *sql.DB, ownerID, orgID, modelConfigID uuid.UUID, archived bool, updatedAt time.Time) database.Chat {
t.Helper()
chat, err := db.InsertChat(ctx, database.InsertChatParams{
OrganizationID: orgID,
OwnerID: ownerID,
LastModelConfigID: modelConfigID,
Title: "test-chat",
@@ -1736,7 +1737,7 @@ func TestDeleteOldChatFiles(t *testing.T) {
require.NoError(t, err)
// Create an old archived chat and an orphaned old file.
oldChat := createChat(ctx, t, db, rawDB, deps.user.ID, deps.modelConfig.ID, true, now.Add(-31*24*time.Hour))
oldChat := createChat(ctx, t, db, rawDB, deps.user.ID, deps.org.ID, deps.modelConfig.ID, true, now.Add(-31*24*time.Hour))
oldFileID := createChatFile(ctx, t, db, rawDB, deps.user.ID, deps.org.ID, now.Add(-31*24*time.Hour))
done := awaitDoTick(ctx, t, clk)
@@ -1766,7 +1767,7 @@ func TestDeleteOldChatFiles(t *testing.T) {
require.NoError(t, err)
// Old archived chat (31 days) — should be deleted.
oldChat := createChat(ctx, t, db, rawDB, deps.user.ID, deps.modelConfig.ID, true, now.Add(-31*24*time.Hour))
oldChat := createChat(ctx, t, db, rawDB, deps.user.ID, deps.org.ID, deps.modelConfig.ID, true, now.Add(-31*24*time.Hour))
// Insert a message so we can verify CASCADE.
_, err = db.InsertChatMessages(ctx, database.InsertChatMessagesParams{
ChatID: oldChat.ID,
@@ -1791,10 +1792,10 @@ func TestDeleteOldChatFiles(t *testing.T) {
require.NoError(t, err)
// Recently archived chat (10 days) — should be retained.
recentChat := createChat(ctx, t, db, rawDB, deps.user.ID, deps.modelConfig.ID, true, now.Add(-10*24*time.Hour))
recentChat := createChat(ctx, t, db, rawDB, deps.user.ID, deps.org.ID, deps.modelConfig.ID, true, now.Add(-10*24*time.Hour))
// Active chat — should be retained.
activeChat := createChat(ctx, t, db, rawDB, deps.user.ID, deps.modelConfig.ID, false, now)
activeChat := createChat(ctx, t, db, rawDB, deps.user.ID, deps.org.ID, deps.modelConfig.ID, false, now)
done := awaitDoTick(ctx, t, clk)
closer := dbpurge.New(ctx, logger, db, &codersdk.DeploymentValues{}, clk, prometheus.NewRegistry())
@@ -1839,7 +1840,7 @@ func TestDeleteOldChatFiles(t *testing.T) {
// File B: 31 days old, in an active chat -> should be retained.
fileB := createChatFile(ctx, t, db, rawDB, deps.user.ID, deps.org.ID, now.Add(-31*24*time.Hour))
activeChat := createChat(ctx, t, db, rawDB, deps.user.ID, deps.modelConfig.ID, false, now)
activeChat := createChat(ctx, t, db, rawDB, deps.user.ID, deps.org.ID, deps.modelConfig.ID, false, now)
_, err = db.LinkChatFiles(ctx, database.LinkChatFilesParams{
ChatID: activeChat.ID,
MaxFileLinks: 100,
@@ -1887,7 +1888,7 @@ func TestDeleteOldChatFiles(t *testing.T) {
// File D: 31 days old, in a chat archived 31 days ago -> should be deleted.
fileD := createChatFile(ctx, t, db, rawDB, deps.user.ID, deps.org.ID, now.Add(-31*24*time.Hour))
oldArchivedChat := createChat(ctx, t, db, rawDB, deps.user.ID, deps.modelConfig.ID, true, now.Add(-31*24*time.Hour))
oldArchivedChat := createChat(ctx, t, db, rawDB, deps.user.ID, deps.org.ID, deps.modelConfig.ID, true, now.Add(-31*24*time.Hour))
_, err = db.LinkChatFiles(ctx, database.LinkChatFilesParams{
ChatID: oldArchivedChat.ID,
MaxFileLinks: 100,
@@ -1901,7 +1902,7 @@ func TestDeleteOldChatFiles(t *testing.T) {
// File E: 31 days old, in a chat archived 10 days ago -> should be retained.
fileE := createChatFile(ctx, t, db, rawDB, deps.user.ID, deps.org.ID, now.Add(-31*24*time.Hour))
recentArchivedChat := createChat(ctx, t, db, rawDB, deps.user.ID, deps.modelConfig.ID, true, now.Add(-10*24*time.Hour))
recentArchivedChat := createChat(ctx, t, db, rawDB, deps.user.ID, deps.org.ID, deps.modelConfig.ID, true, now.Add(-10*24*time.Hour))
_, err = db.LinkChatFiles(ctx, database.LinkChatFilesParams{
ChatID: recentArchivedChat.ID,
MaxFileLinks: 100,
@@ -1914,7 +1915,7 @@ func TestDeleteOldChatFiles(t *testing.T) {
// File F: 31 days old, in BOTH an active chat AND an old archived chat -> should be retained.
fileF := createChatFile(ctx, t, db, rawDB, deps.user.ID, deps.org.ID, now.Add(-31*24*time.Hour))
anotherOldArchivedChat := createChat(ctx, t, db, rawDB, deps.user.ID, deps.modelConfig.ID, true, now.Add(-31*24*time.Hour))
anotherOldArchivedChat := createChat(ctx, t, db, rawDB, deps.user.ID, deps.org.ID, deps.modelConfig.ID, true, now.Add(-31*24*time.Hour))
_, err = db.LinkChatFiles(ctx, database.LinkChatFilesParams{
ChatID: anotherOldArchivedChat.ID,
MaxFileLinks: 100,
@@ -1925,7 +1926,7 @@ func TestDeleteOldChatFiles(t *testing.T) {
now.Add(-31*24*time.Hour), anotherOldArchivedChat.ID)
require.NoError(t, err)
activeChatForF := createChat(ctx, t, db, rawDB, deps.user.ID, deps.modelConfig.ID, false, now)
activeChatForF := createChat(ctx, t, db, rawDB, deps.user.ID, deps.org.ID, deps.modelConfig.ID, false, now)
_, err = db.LinkChatFiles(ctx, database.LinkChatFilesParams{
ChatID: activeChatForF.ID,
MaxFileLinks: 100,
@@ -1964,7 +1965,7 @@ func TestDeleteOldChatFiles(t *testing.T) {
fileB := createChatFile(ctx, t, db, rawDB, deps.user.ID, deps.org.ID, now)
fileC := createChatFile(ctx, t, db, rawDB, deps.user.ID, deps.org.ID, now)
chat := createChat(ctx, t, db, rawDB, deps.user.ID, deps.modelConfig.ID, false, now)
chat := createChat(ctx, t, db, rawDB, deps.user.ID, deps.org.ID, deps.modelConfig.ID, false, now)
_, err := db.LinkChatFiles(ctx, database.LinkChatFilesParams{
ChatID: chat.ID,
MaxFileLinks: 100,
@@ -2009,14 +2010,16 @@ func TestDeleteOldChatFiles(t *testing.T) {
// Test parent+child cascade: deleting files should
// clean up links for both parent and child chats
// independently via FK cascade.
parentChat := createChat(ctx, t, db, rawDB, deps.user.ID, deps.modelConfig.ID, false, now)
parentChat := createChat(ctx, t, db, rawDB, deps.user.ID, deps.org.ID, deps.modelConfig.ID, false, now)
childChat, err := db.InsertChat(ctx, database.InsertChatParams{
OrganizationID: deps.org.ID,
OwnerID: deps.user.ID,
LastModelConfigID: deps.modelConfig.ID,
Title: "child-chat",
Status: database.ChatStatusWaiting,
})
require.NoError(t, err)
// Set root_chat_id to link child to parent.
_, err = rawDB.ExecContext(ctx, "UPDATE chats SET root_chat_id = $1 WHERE id = $2", parentChat.ID, childChat.ID)
require.NoError(t, err)
@@ -2104,7 +2107,7 @@ func TestDeleteOldChatFiles(t *testing.T) {
// Create 3 deletable old archived chats.
for range 3 {
createChat(ctx, t, db, rawDB, deps.user.ID, deps.modelConfig.ID, true, now.Add(-31*24*time.Hour))
createChat(ctx, t, db, rawDB, deps.user.ID, deps.org.ID, deps.modelConfig.ID, true, now.Add(-31*24*time.Hour))
}
// Delete with limit 2 — should delete 2, leave 1.
+7 -1
View File
@@ -1431,7 +1431,8 @@ CREATE TABLE chats (
pin_order integer DEFAULT 0 NOT NULL,
last_read_message_id bigint,
last_injected_context jsonb,
dynamic_tools jsonb
dynamic_tools jsonb,
organization_id uuid NOT NULL
);
CREATE TABLE connection_logs (
@@ -3789,6 +3790,8 @@ CREATE INDEX idx_chats_labels ON chats USING gin (labels);
CREATE INDEX idx_chats_last_model_config_id ON chats USING btree (last_model_config_id);
CREATE INDEX idx_chats_organization_id ON chats USING btree (organization_id);
CREATE INDEX idx_chats_owner ON chats USING btree (owner_id);
CREATE INDEX idx_chats_parent_chat_id ON chats USING btree (parent_chat_id);
@@ -4104,6 +4107,9 @@ ALTER TABLE ONLY chats
ALTER TABLE ONLY chats
ADD CONSTRAINT chats_last_model_config_id_fkey FOREIGN KEY (last_model_config_id) REFERENCES chat_model_configs(id);
ALTER TABLE ONLY chats
ADD CONSTRAINT chats_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE;
ALTER TABLE ONLY chats
ADD CONSTRAINT chats_owner_id_fkey FOREIGN KEY (owner_id) REFERENCES users(id) ON DELETE CASCADE;
@@ -25,6 +25,7 @@ const (
ForeignKeyChatsAgentID ForeignKeyConstraint = "chats_agent_id_fkey" // ALTER TABLE ONLY chats ADD CONSTRAINT chats_agent_id_fkey FOREIGN KEY (agent_id) REFERENCES workspace_agents(id) ON DELETE SET NULL;
ForeignKeyChatsBuildID ForeignKeyConstraint = "chats_build_id_fkey" // ALTER TABLE ONLY chats ADD CONSTRAINT chats_build_id_fkey FOREIGN KEY (build_id) REFERENCES workspace_builds(id) ON DELETE SET NULL;
ForeignKeyChatsLastModelConfigID ForeignKeyConstraint = "chats_last_model_config_id_fkey" // ALTER TABLE ONLY chats ADD CONSTRAINT chats_last_model_config_id_fkey FOREIGN KEY (last_model_config_id) REFERENCES chat_model_configs(id);
ForeignKeyChatsOrganizationID ForeignKeyConstraint = "chats_organization_id_fkey" // ALTER TABLE ONLY chats ADD CONSTRAINT chats_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE;
ForeignKeyChatsOwnerID ForeignKeyConstraint = "chats_owner_id_fkey" // ALTER TABLE ONLY chats ADD CONSTRAINT chats_owner_id_fkey FOREIGN KEY (owner_id) REFERENCES users(id) ON DELETE CASCADE;
ForeignKeyChatsParentChatID ForeignKeyConstraint = "chats_parent_chat_id_fkey" // ALTER TABLE ONLY chats ADD CONSTRAINT chats_parent_chat_id_fkey FOREIGN KEY (parent_chat_id) REFERENCES chats(id) ON DELETE SET NULL;
ForeignKeyChatsRootChatID ForeignKeyConstraint = "chats_root_chat_id_fkey" // ALTER TABLE ONLY chats ADD CONSTRAINT chats_root_chat_id_fkey FOREIGN KEY (root_chat_id) REFERENCES chats(id) ON DELETE SET NULL;
@@ -0,0 +1 @@
ALTER TABLE chats DROP COLUMN organization_id;
@@ -0,0 +1,20 @@
-- Step 1: Add nullable column with FK.
ALTER TABLE chats
ADD COLUMN organization_id UUID REFERENCES organizations(id) ON DELETE CASCADE;
-- Step 2: Backfill from workspace org (primary path). Fall back to
-- user's oldest org membership, then default org for rows where
-- workspace_id was NULLed out by ON DELETE SET NULL or never set.
UPDATE chats c
SET organization_id = COALESCE(
(SELECT w.organization_id FROM workspaces w WHERE w.id = c.workspace_id),
(SELECT om.organization_id FROM organization_members om
WHERE om.user_id = c.owner_id ORDER BY om.created_at ASC LIMIT 1),
(SELECT id FROM organizations WHERE is_default = true LIMIT 1)
);
-- Step 3: Enforce NOT NULL going forward.
ALTER TABLE chats ALTER COLUMN organization_id SET NOT NULL;
-- Step 4: Index for efficient lookups by organization.
CREATE INDEX idx_chats_organization_id ON chats (organization_id);
+1 -1
View File
@@ -176,7 +176,7 @@ func (t Task) RBACObject() rbac.Object {
}
func (c Chat) RBACObject() rbac.Object {
return rbac.ResourceChat.WithID(c.ID).WithOwner(c.OwnerID.String())
return rbac.ResourceChat.WithID(c.ID).WithOwner(c.OwnerID.String()).InOrg(c.OrganizationID)
}
func (r GetChatsRow) RBACObject() rbac.Object {
+1
View File
@@ -799,6 +799,7 @@ func (q *sqlQuerier) GetAuthorizedChats(ctx context.Context, arg GetChatsParams,
&i.Chat.LastReadMessageID,
&i.Chat.LastInjectedContext,
&i.Chat.DynamicTools,
&i.Chat.OrganizationID,
&i.HasUnread); err != nil {
return nil, err
}
+1
View File
@@ -4246,6 +4246,7 @@ type Chat struct {
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"`
}
type ChatDiffStatus struct {
+109 -66
View File
@@ -1258,6 +1258,11 @@ func TestGetAuthorizedChats(t *testing.T) {
RBACRoles: pq.StringArray{rbac.RoleAgentsAccess().String()},
})
org := dbgen.Organization(t, db, database.Organization{})
dbgen.OrganizationMember(t, db, database.OrganizationMember{UserID: owner.ID, OrganizationID: org.ID})
dbgen.OrganizationMember(t, db, database.OrganizationMember{UserID: member.ID, OrganizationID: org.ID})
dbgen.OrganizationMember(t, db, database.OrganizationMember{UserID: secondMember.ID, OrganizationID: org.ID})
// Create FK dependencies: a chat provider and model config.
ctx := testutil.Context(t, testutil.WaitMedium)
_, err = db.InsertChatProvider(ctx, database.InsertChatProviderParams{
@@ -1286,6 +1291,7 @@ func TestGetAuthorizedChats(t *testing.T) {
// Create 3 chats owned by owner.
for i := range 3 {
_, err := db.InsertChat(ctx, database.InsertChatParams{
OrganizationID: org.ID,
Status: database.ChatStatusWaiting,
OwnerID: owner.ID,
LastModelConfigID: modelCfg.ID,
@@ -1297,6 +1303,7 @@ func TestGetAuthorizedChats(t *testing.T) {
// Create 2 chats owned by member.
for i := range 2 {
_, err := db.InsertChat(ctx, database.InsertChatParams{
OrganizationID: org.ID,
Status: database.ChatStatusWaiting,
OwnerID: member.ID,
LastModelConfigID: modelCfg.ID,
@@ -1340,8 +1347,8 @@ func TestGetAuthorizedChats(t *testing.T) {
require.NoError(t, err)
require.Len(t, secondRows, 0)
// Org admin should NOT see other users' chats — chats are
// not org-scoped resources.
// Org admin should NOT see other users' chats when they are
// in a different org than the chat owner.
orgs, err := db.GetOrganizations(ctx, database.GetOrganizationsParams{})
require.NoError(t, err)
require.NotEmpty(t, orgs)
@@ -1359,6 +1366,21 @@ func TestGetAuthorizedChats(t *testing.T) {
require.NoError(t, err)
require.Len(t, orgAdminRows, 0, "org admin with no chats should see 0 chats")
// Org admin in SAME org should see all chats in that org.
sameOrgAdmin := dbgen.User(t, db, database.User{})
dbgen.OrganizationMember(t, db, database.OrganizationMember{
UserID: sameOrgAdmin.ID,
OrganizationID: org.ID,
Roles: []string{rbac.RoleOrgAdmin()},
})
sameOrgAdminSubject, _, err := httpmw.UserRBACSubject(ctx, db, sameOrgAdmin.ID, rbac.ExpandableScope(rbac.ScopeAll))
require.NoError(t, err)
preparedSameOrgAdmin, err := authorizer.Prepare(ctx, sameOrgAdminSubject, policy.ActionRead, rbac.ResourceChat.Type)
require.NoError(t, err)
sameOrgAdminRows, err := db.GetAuthorizedChats(ctx, database.GetChatsParams{}, preparedSameOrgAdmin)
require.NoError(t, err)
require.GreaterOrEqual(t, len(sameOrgAdminRows), 5, "same-org admin should see all chats in their org")
// OwnerID filter: member queries their own chats.
memberFilterSelf, err := db.GetAuthorizedChats(ctx, database.GetChatsParams{
OwnerID: member.ID,
@@ -1417,8 +1439,10 @@ func TestGetAuthorizedChats(t *testing.T) {
paginationUser := dbgen.User(t, db, database.User{
RBACRoles: pq.StringArray{rbac.RoleAgentsAccess().String()},
})
dbgen.OrganizationMember(t, db, database.OrganizationMember{UserID: paginationUser.ID, OrganizationID: org.ID})
for i := range 7 {
_, err := db.InsertChat(ctx, database.InsertChatParams{
OrganizationID: org.ID,
Status: database.ChatStatusWaiting,
OwnerID: paginationUser.ID,
LastModelConfigID: modelCfg.ID,
@@ -9753,8 +9777,9 @@ func TestInsertChatMessages(t *testing.T) {
store, _ := dbtestutil.NewDB(t)
ctx := context.Background()
dbgen.Organization(t, store, database.Organization{})
org := dbgen.Organization(t, store, database.Organization{})
user := dbgen.User(t, store, database.User{})
dbgen.OrganizationMember(t, store, database.OrganizationMember{UserID: user.ID, OrganizationID: org.ID})
provider := "openai"
_, err := store.InsertChatProvider(ctx, database.InsertChatProviderParams{
@@ -9778,6 +9803,7 @@ func TestInsertChatMessages(t *testing.T) {
)
chat, err := store.InsertChat(ctx, database.InsertChatParams{
OrganizationID: org.ID,
Status: database.ChatStatusWaiting,
OwnerID: user.ID,
LastModelConfigID: modelConfigA.ID,
@@ -9921,6 +9947,8 @@ func TestGetChatMessagesForPromptByChatID(t *testing.T) {
// Helper: create a chat model config (required FK for chats).
user := dbgen.User(t, db, database.User{})
org := dbgen.Organization(t, db, database.Organization{})
dbgen.OrganizationMember(t, db, database.OrganizationMember{UserID: user.ID, OrganizationID: org.ID})
// A chat_providers row is required as a FK for model configs.
_, err := db.InsertChatProvider(ctx, database.InsertChatProviderParams{
@@ -9949,6 +9977,7 @@ func TestGetChatMessagesForPromptByChatID(t *testing.T) {
newChat := func(t *testing.T) database.Chat {
t.Helper()
chat, err := db.InsertChat(ctx, database.InsertChatParams{
OrganizationID: org.ID,
Status: database.ChatStatusWaiting,
OwnerID: user.ID,
LastModelConfigID: modelCfg.ID,
@@ -10288,12 +10317,13 @@ func TestGetPRInsights(t *testing.T) {
// setupChatInfra creates a fresh database with a user, chat provider,
// and model config. Returns the store, user ID, and model config ID.
setupChatInfra := func(t *testing.T) (database.Store, uuid.UUID, uuid.UUID) {
setupChatInfra := func(t *testing.T) (database.Store, uuid.UUID, uuid.UUID, uuid.UUID) {
t.Helper()
store, _ := dbtestutil.NewDB(t)
ctx := context.Background()
dbgen.Organization(t, store, database.Organization{})
org := dbgen.Organization(t, store, database.Organization{})
user := dbgen.User(t, store, database.User{})
dbgen.OrganizationMember(t, store, database.OrganizationMember{UserID: user.ID, OrganizationID: org.ID})
_, err := store.InsertChatProvider(ctx, database.InsertChatProviderParams{
Provider: "anthropic",
@@ -10318,12 +10348,13 @@ func TestGetPRInsights(t *testing.T) {
})
require.NoError(t, err)
return store, user.ID, mc.ID
return store, user.ID, mc.ID, org.ID
}
createChat := func(t *testing.T, store database.Store, userID, mcID uuid.UUID, title string) database.Chat {
createChat := func(t *testing.T, store database.Store, userID, mcID, orgID uuid.UUID, title string) database.Chat {
t.Helper()
chat, err := store.InsertChat(context.Background(), database.InsertChatParams{
OrganizationID: orgID,
Status: database.ChatStatusWaiting,
OwnerID: userID,
LastModelConfigID: mcID,
@@ -10384,12 +10415,12 @@ func TestGetPRInsights(t *testing.T) {
t.Run("MultipleChatsSamePR_CostSummed", func(t *testing.T) {
t.Parallel()
store, userID, mcID := setupChatInfra(t)
store, userID, mcID, orgID := setupChatInfra(t)
chatA := createChat(t, store, userID, mcID, "chat-A")
chatA := createChat(t, store, userID, mcID, orgID, "chat-A")
insertCostMessage(t, store, chatA.ID, userID, mcID, 5_000_000) // $5
chatB := createChat(t, store, userID, mcID, "chat-B")
chatB := createChat(t, store, userID, mcID, orgID, "chat-B")
insertCostMessage(t, store, chatB.ID, userID, mcID, 3_000_000) // $3
prURL := "https://github.com/org/repo/pull/123"
@@ -10420,13 +10451,13 @@ func TestGetPRInsights(t *testing.T) {
t.Run("DifferentPRs_NoDuplication", func(t *testing.T) {
t.Parallel()
store, userID, mcID := setupChatInfra(t)
store, userID, mcID, orgID := setupChatInfra(t)
chatA := createChat(t, store, userID, mcID, "chat-A")
chatA := createChat(t, store, userID, mcID, orgID, "chat-A")
insertCostMessage(t, store, chatA.ID, userID, mcID, 5_000_000)
linkPR(t, store, chatA.ID, "https://github.com/org/repo/pull/1", "merged", "feat: A", 50, 10, 2)
chatB := createChat(t, store, userID, mcID, "chat-B")
chatB := createChat(t, store, userID, mcID, orgID, "chat-B")
insertCostMessage(t, store, chatB.ID, userID, mcID, 3_000_000)
linkPR(t, store, chatB.ID, "https://github.com/org/repo/pull/2", "open", "feat: B", 80, 30, 4)
@@ -10455,9 +10486,10 @@ func TestGetPRInsights(t *testing.T) {
// createChildChat creates a chat with ParentChatID and RootChatID
// set, simulating a subagent/child chat in a tree.
createChildChat := func(t *testing.T, store database.Store, userID, mcID, parentID, rootID uuid.UUID, title string) database.Chat {
createChildChat := func(t *testing.T, store database.Store, userID, mcID, orgID, parentID, rootID uuid.UUID, title string) database.Chat {
t.Helper()
chat, err := store.InsertChat(context.Background(), database.InsertChatParams{
OrganizationID: orgID,
Status: database.ChatStatusWaiting,
OwnerID: userID,
LastModelConfigID: mcID,
@@ -10471,11 +10503,11 @@ func TestGetPRInsights(t *testing.T) {
t.Run("DuplicatePRUrl_CountedOnce", func(t *testing.T) {
t.Parallel()
store, userID, mcID := setupChatInfra(t)
store, userID, mcID, orgID := setupChatInfra(t)
prURL := "https://github.com/org/repo/pull/99"
for i := 0; i < 3; i++ {
chat := createChat(t, store, userID, mcID, fmt.Sprintf("chat-%d", i))
chat := createChat(t, store, userID, mcID, orgID, fmt.Sprintf("chat-%d", i))
insertCostMessage(t, store, chat.ID, userID, mcID, 1_000_000)
linkPR(t, store, chat.ID, prURL, "merged", "fix: same PR", 40, 10, 3)
}
@@ -10500,19 +10532,19 @@ func TestGetPRInsights(t *testing.T) {
t.Run("ChildChatCostsIncluded", func(t *testing.T) {
t.Parallel()
store, userID, mcID := setupChatInfra(t)
store, userID, mcID, orgID := setupChatInfra(t)
// Parent chat with a $5 cost.
parent := createChat(t, store, userID, mcID, "parent-chat")
parent := createChat(t, store, userID, mcID, orgID, "parent-chat")
insertCostMessage(t, store, parent.ID, userID, mcID, 5_000_000)
// Two child chats (subagents) with $2 each. Only the parent
// has a chat_diff_statuses entry, but the children's costs
// should be included via the tree join.
child1 := createChildChat(t, store, userID, mcID, parent.ID, parent.ID, "child-1")
child1 := createChildChat(t, store, userID, mcID, orgID, parent.ID, parent.ID, "child-1")
insertCostMessage(t, store, child1.ID, userID, mcID, 2_000_000)
child2 := createChildChat(t, store, userID, mcID, parent.ID, parent.ID, "child-2")
child2 := createChildChat(t, store, userID, mcID, orgID, parent.ID, parent.ID, "child-2")
insertCostMessage(t, store, child2.ID, userID, mcID, 2_000_000)
prURL := "https://github.com/org/repo/pull/42"
@@ -10542,19 +10574,19 @@ func TestGetPRInsights(t *testing.T) {
t.Run("SiblingPRs_NoCrossContamination", func(t *testing.T) {
t.Parallel()
store, userID, mcID := setupChatInfra(t)
store, userID, mcID, orgID := setupChatInfra(t)
// Parent chat with $10 orchestration cost.
parent := createChat(t, store, userID, mcID, "parent")
parent := createChat(t, store, userID, mcID, orgID, "parent")
insertCostMessage(t, store, parent.ID, userID, mcID, 10_000_000)
// Child C1 ($5) creates PR1.
c1 := createChildChat(t, store, userID, mcID, parent.ID, parent.ID, "child-1")
c1 := createChildChat(t, store, userID, mcID, orgID, parent.ID, parent.ID, "child-1")
insertCostMessage(t, store, c1.ID, userID, mcID, 5_000_000)
linkPR(t, store, c1.ID, "https://github.com/org/repo/pull/10", "merged", "feat: PR1", 50, 10, 2)
// Child C2 ($3) creates PR2.
c2 := createChildChat(t, store, userID, mcID, parent.ID, parent.ID, "child-2")
c2 := createChildChat(t, store, userID, mcID, orgID, parent.ID, parent.ID, "child-2")
insertCostMessage(t, store, c2.ID, userID, mcID, 3_000_000)
linkPR(t, store, c2.ID, "https://github.com/org/repo/pull/11", "open", "feat: PR2", 30, 5, 1)
@@ -10585,23 +10617,23 @@ func TestGetPRInsights(t *testing.T) {
t.Run("ParentAndChildDifferentPRs_NoCrossContamination", func(t *testing.T) {
t.Parallel()
store, userID, mcID := setupChatInfra(t)
store, userID, mcID, orgID := setupChatInfra(t)
// Parent P ($10) creates PR1.
parent := createChat(t, store, userID, mcID, "parent")
parent := createChat(t, store, userID, mcID, orgID, "parent")
insertCostMessage(t, store, parent.ID, userID, mcID, 10_000_000)
linkPR(t, store, parent.ID, "https://github.com/org/repo/pull/20", "merged", "feat: parent PR", 80, 20, 4)
// Child C1 ($5) has its own PR2. Because C1 has its own
// chat_diff_statuses entry, its cost should NOT be included
// under PR1 — it belongs to PR2 only.
c1 := createChildChat(t, store, userID, mcID, parent.ID, parent.ID, "child-1")
c1 := createChildChat(t, store, userID, mcID, orgID, parent.ID, parent.ID, "child-1")
insertCostMessage(t, store, c1.ID, userID, mcID, 5_000_000)
linkPR(t, store, c1.ID, "https://github.com/org/repo/pull/21", "open", "feat: child PR", 30, 5, 1)
// Child C2 ($2) has NO cds entry — pure subagent.
// Its cost should be included under PR1 (the parent's PR).
c2 := createChildChat(t, store, userID, mcID, parent.ID, parent.ID, "child-2")
c2 := createChildChat(t, store, userID, mcID, orgID, parent.ID, parent.ID, "child-2")
insertCostMessage(t, store, c2.ID, userID, mcID, 2_000_000)
// PR1 cost = parent ($10) + C2 ($2) = $12 (C1 excluded)
@@ -10630,16 +10662,16 @@ func TestGetPRInsights(t *testing.T) {
t.Run("EmptyURLNotCollapsed", func(t *testing.T) {
t.Parallel()
store, userID, mcID := setupChatInfra(t)
store, userID, mcID, orgID := setupChatInfra(t)
// Two chats with empty-string URLs should be treated as
// separate PRs (NULLIF converts '' to NULL, falling back
// to c.id::text).
chatX := createChat(t, store, userID, mcID, "chat-X")
chatX := createChat(t, store, userID, mcID, orgID, "chat-X")
insertCostMessage(t, store, chatX.ID, userID, mcID, 4_000_000)
linkPR(t, store, chatX.ID, "", "open", "draft: X", 10, 2, 1)
chatY := createChat(t, store, userID, mcID, "chat-Y")
chatY := createChat(t, store, userID, mcID, orgID, "chat-Y")
insertCostMessage(t, store, chatY.ID, userID, mcID, 6_000_000)
linkPR(t, store, chatY.ID, "", "merged", "draft: Y", 20, 5, 2)
@@ -10663,14 +10695,14 @@ func TestGetPRInsights(t *testing.T) {
t.Run("ParentAndChildSameURL_DedupedWithCombinedCost", func(t *testing.T) {
t.Parallel()
store, userID, mcID := setupChatInfra(t)
store, userID, mcID, orgID := setupChatInfra(t)
// Parent P ($10) links to a PR.
parent := createChat(t, store, userID, mcID, "parent")
parent := createChat(t, store, userID, mcID, orgID, "parent")
insertCostMessage(t, store, parent.ID, userID, mcID, 10_000_000)
// Child C ($5) also links to the same PR URL.
child := createChildChat(t, store, userID, mcID, parent.ID, parent.ID, "child")
child := createChildChat(t, store, userID, mcID, orgID, parent.ID, parent.ID, "child")
insertCostMessage(t, store, child.ID, userID, mcID, 5_000_000)
prURL := "https://github.com/org/repo/pull/50"
@@ -10700,11 +10732,11 @@ func TestGetPRInsights(t *testing.T) {
t.Run("ZeroCostChat_StillCounted", func(t *testing.T) {
t.Parallel()
store, userID, mcID := setupChatInfra(t)
store, userID, mcID, orgID := setupChatInfra(t)
// A chat linked to a PR but with NO chat_messages at all.
// The PR should still appear with zero cost.
chat := createChat(t, store, userID, mcID, "zero-cost-chat")
chat := createChat(t, store, userID, mcID, orgID, "zero-cost-chat")
linkPR(t, store, chat.ID, "https://github.com/org/repo/pull/60", "open", "feat: no messages", 25, 5, 2)
summary, err := store.GetPRInsightsSummary(context.Background(), database.GetPRInsightsSummaryParams{
@@ -10728,7 +10760,7 @@ func TestGetPRInsights(t *testing.T) {
t.Run("BlankDisplayNameFallsBackToModel", func(t *testing.T) {
t.Parallel()
store, userID, _ := setupChatInfra(t)
store, userID, _, orgID := setupChatInfra(t)
const modelName = "claude-4.1"
emptyDisplayModel, err := store.InsertChatModelConfig(context.Background(), database.InsertChatModelConfigParams{
@@ -10745,7 +10777,7 @@ func TestGetPRInsights(t *testing.T) {
})
require.NoError(t, err)
chat := createChat(t, store, userID, emptyDisplayModel.ID, "chat-empty-display-name")
chat := createChat(t, store, userID, emptyDisplayModel.ID, orgID, "chat-empty-display-name")
insertCostMessage(t, store, chat.ID, userID, emptyDisplayModel.ID, 1_000_000)
linkPR(t, store, chat.ID, "https://github.com/org/repo/pull/72", "merged", "fix: blank display name", 10, 2, 1)
@@ -10770,15 +10802,15 @@ func TestGetPRInsights(t *testing.T) {
t.Run("MergedCostMicros_OnlyCountsMerged", func(t *testing.T) {
t.Parallel()
store, userID, mcID := setupChatInfra(t)
store, userID, mcID, orgID := setupChatInfra(t)
// Merged PR with $5 cost.
chatMerged := createChat(t, store, userID, mcID, "chat-merged")
chatMerged := createChat(t, store, userID, mcID, orgID, "chat-merged")
insertCostMessage(t, store, chatMerged.ID, userID, mcID, 5_000_000)
linkPR(t, store, chatMerged.ID, "https://github.com/org/repo/pull/70", "merged", "fix: merged", 40, 10, 2)
// Open PR with $3 cost.
chatOpen := createChat(t, store, userID, mcID, "chat-open")
chatOpen := createChat(t, store, userID, mcID, orgID, "chat-open")
insertCostMessage(t, store, chatOpen.ID, userID, mcID, 3_000_000)
linkPR(t, store, chatOpen.ID, "https://github.com/org/repo/pull/71", "open", "feat: open", 20, 5, 1)
@@ -10796,13 +10828,13 @@ func TestGetPRInsights(t *testing.T) {
t.Run("AllPRsReturnedWithSafetyCap", func(t *testing.T) {
t.Parallel()
store, userID, mcID := setupChatInfra(t)
store, userID, mcID, orgID := setupChatInfra(t)
// Create 25 distinct PRs — more than the old LIMIT 20 — and
// verify all are returned.
const prCount = 25
for i := range prCount {
chat := createChat(t, store, userID, mcID, fmt.Sprintf("chat-%d", i))
chat := createChat(t, store, userID, mcID, orgID, fmt.Sprintf("chat-%d", i))
insertCostMessage(t, store, chat.ID, userID, mcID, 1_000_000)
linkPR(t, store, chat.ID,
fmt.Sprintf("https://github.com/org/repo/pull/%d", 100+i),
@@ -10825,11 +10857,13 @@ func TestChatPinOrderQueries(t *testing.T) {
t.SkipNow()
}
setup := func(t *testing.T) (context.Context, database.Store, uuid.UUID, uuid.UUID) {
setup := func(t *testing.T) (context.Context, database.Store, uuid.UUID, uuid.UUID, uuid.UUID) {
t.Helper()
db, _ := dbtestutil.NewDB(t)
org := dbgen.Organization(t, db, database.Organization{})
owner := dbgen.User(t, db, database.User{})
dbgen.OrganizationMember(t, db, database.OrganizationMember{UserID: owner.ID, OrganizationID: org.ID})
// Use background context for fixture setup so the
// timed test context doesn't tick during DB init.
@@ -10858,13 +10892,14 @@ func TestChatPinOrderQueries(t *testing.T) {
require.NoError(t, err)
ctx := testutil.Context(t, testutil.WaitMedium)
return ctx, db, owner.ID, modelCfg.ID
return ctx, db, owner.ID, modelCfg.ID, org.ID
}
createChat := func(t *testing.T, ctx context.Context, db database.Store, ownerID, modelCfgID uuid.UUID, title string) database.Chat {
createChat := func(t *testing.T, ctx context.Context, db database.Store, ownerID, modelCfgID, orgID uuid.UUID, title string) database.Chat {
t.Helper()
chat, err := db.InsertChat(ctx, database.InsertChatParams{
OrganizationID: orgID,
Status: database.ChatStatusWaiting,
OwnerID: ownerID,
LastModelConfigID: modelCfgID,
@@ -10887,13 +10922,13 @@ func TestChatPinOrderQueries(t *testing.T) {
t.Run("PinChatByIDAppendsWithinOwner", func(t *testing.T) {
t.Parallel()
ctx, db, ownerID, modelCfgID := setup(t)
first := createChat(t, ctx, db, ownerID, modelCfgID, "first")
second := createChat(t, ctx, db, ownerID, modelCfgID, "second")
third := createChat(t, ctx, db, ownerID, modelCfgID, "third")
ctx, db, ownerID, modelCfgID, orgID := setup(t)
first := createChat(t, ctx, db, ownerID, modelCfgID, orgID, "first")
second := createChat(t, ctx, db, ownerID, modelCfgID, orgID, "second")
third := createChat(t, ctx, db, ownerID, modelCfgID, orgID, "third")
otherOwner := dbgen.User(t, db, database.User{})
other := createChat(t, ctx, db, otherOwner.ID, modelCfgID, "other-owner")
other := createChat(t, ctx, db, otherOwner.ID, modelCfgID, orgID, "other-owner")
require.NoError(t, db.PinChatByID(ctx, other.ID))
require.NoError(t, db.PinChatByID(ctx, first.ID))
@@ -10911,10 +10946,10 @@ func TestChatPinOrderQueries(t *testing.T) {
t.Run("UpdateChatPinOrderShiftsNeighborsAndClamps", func(t *testing.T) {
t.Parallel()
ctx, db, ownerID, modelCfgID := setup(t)
first := createChat(t, ctx, db, ownerID, modelCfgID, "first")
second := createChat(t, ctx, db, ownerID, modelCfgID, "second")
third := createChat(t, ctx, db, ownerID, modelCfgID, "third")
ctx, db, ownerID, modelCfgID, orgID := setup(t)
first := createChat(t, ctx, db, ownerID, modelCfgID, orgID, "first")
second := createChat(t, ctx, db, ownerID, modelCfgID, orgID, "second")
third := createChat(t, ctx, db, ownerID, modelCfgID, orgID, "third")
for _, chat := range []database.Chat{first, second, third} {
require.NoError(t, db.PinChatByID(ctx, chat.ID))
@@ -10944,10 +10979,10 @@ func TestChatPinOrderQueries(t *testing.T) {
t.Run("UnpinChatByIDCompactsPinnedChats", func(t *testing.T) {
t.Parallel()
ctx, db, ownerID, modelCfgID := setup(t)
first := createChat(t, ctx, db, ownerID, modelCfgID, "first")
second := createChat(t, ctx, db, ownerID, modelCfgID, "second")
third := createChat(t, ctx, db, ownerID, modelCfgID, "third")
ctx, db, ownerID, modelCfgID, orgID := setup(t)
first := createChat(t, ctx, db, ownerID, modelCfgID, orgID, "first")
second := createChat(t, ctx, db, ownerID, modelCfgID, orgID, "second")
third := createChat(t, ctx, db, ownerID, modelCfgID, orgID, "third")
for _, chat := range []database.Chat{first, second, third} {
require.NoError(t, db.PinChatByID(ctx, chat.ID))
@@ -10964,10 +10999,10 @@ func TestChatPinOrderQueries(t *testing.T) {
t.Run("ArchiveClearsPinAndExcludesFromRanking", func(t *testing.T) {
t.Parallel()
ctx, db, ownerID, modelCfgID := setup(t)
first := createChat(t, ctx, db, ownerID, modelCfgID, "first")
second := createChat(t, ctx, db, ownerID, modelCfgID, "second")
third := createChat(t, ctx, db, ownerID, modelCfgID, "third")
ctx, db, ownerID, modelCfgID, orgID := setup(t)
first := createChat(t, ctx, db, ownerID, modelCfgID, orgID, "first")
second := createChat(t, ctx, db, ownerID, modelCfgID, orgID, "second")
third := createChat(t, ctx, db, ownerID, modelCfgID, orgID, "third")
for _, chat := range []database.Chat{first, second, third} {
require.NoError(t, db.PinChatByID(ctx, chat.ID))
@@ -11014,6 +11049,8 @@ func TestChatLabels(t *testing.T) {
ctx := testutil.Context(t, testutil.WaitMedium)
owner := dbgen.User(t, db, database.User{})
org := dbgen.Organization(t, db, database.Organization{})
dbgen.OrganizationMember(t, db, database.OrganizationMember{UserID: owner.ID, OrganizationID: org.ID})
_, err = db.InsertChatProvider(ctx, database.InsertChatProviderParams{
Provider: "openai",
@@ -11047,6 +11084,7 @@ func TestChatLabels(t *testing.T) {
require.NoError(t, err)
chat, err := db.InsertChat(ctx, database.InsertChatParams{
OrganizationID: org.ID,
Status: database.ChatStatusWaiting,
OwnerID: owner.ID,
LastModelConfigID: modelCfg.ID,
@@ -11070,6 +11108,7 @@ func TestChatLabels(t *testing.T) {
ctx := testutil.Context(t, testutil.WaitMedium)
chat, err := db.InsertChat(ctx, database.InsertChatParams{
OrganizationID: org.ID,
Status: database.ChatStatusWaiting,
OwnerID: owner.ID,
LastModelConfigID: modelCfg.ID,
@@ -11086,6 +11125,7 @@ func TestChatLabels(t *testing.T) {
ctx := testutil.Context(t, testutil.WaitMedium)
chat, err := db.InsertChat(ctx, database.InsertChatParams{
OrganizationID: org.ID,
Status: database.ChatStatusWaiting,
OwnerID: owner.ID,
LastModelConfigID: modelCfg.ID,
@@ -11127,6 +11167,7 @@ func TestChatLabels(t *testing.T) {
require.NoError(t, err)
chat, err := db.InsertChat(ctx, database.InsertChatParams{
OrganizationID: org.ID,
Status: database.ChatStatusWaiting,
OwnerID: owner.ID,
LastModelConfigID: modelCfg.ID,
@@ -11164,10 +11205,10 @@ func TestChatLabels(t *testing.T) {
labelsJSON, err := json.Marshal(tc.labels)
require.NoError(t, err)
_, err = db.InsertChat(ctx, database.InsertChatParams{
OrganizationID: org.ID,
Status: database.ChatStatusWaiting,
OwnerID: owner.ID,
LastModelConfigID: modelCfg.ID,
Title: tc.title,
LastModelConfigID: modelCfg.ID, Title: tc.title,
Labels: pqtype.NullRawMessage{
RawMessage: labelsJSON,
Valid: true,
@@ -11224,8 +11265,9 @@ func TestChatHasUnread(t *testing.T) {
store, _ := dbtestutil.NewDB(t)
ctx := context.Background()
dbgen.Organization(t, store, database.Organization{})
org := dbgen.Organization(t, store, database.Organization{})
user := dbgen.User(t, store, database.User{})
dbgen.OrganizationMember(t, store, database.OrganizationMember{UserID: user.ID, OrganizationID: org.ID})
_, err := store.InsertChatProvider(ctx, database.InsertChatProviderParams{
Provider: "openai",
@@ -11251,6 +11293,7 @@ func TestChatHasUnread(t *testing.T) {
require.NoError(t, err)
chat, err := store.InsertChat(ctx, database.InsertChatParams{
OrganizationID: org.ID,
Status: database.ChatStatusWaiting,
OwnerID: user.ID,
LastModelConfigID: modelCfg.ID,
+50 -27
View File
@@ -4239,7 +4239,7 @@ WHERE
$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
id, owner_id, 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
`
type AcquireChatsParams struct {
@@ -4284,6 +4284,7 @@ func (q *sqlQuerier) AcquireChats(ctx context.Context, arg AcquireChatsParams) (
&i.LastReadMessageID,
&i.LastInjectedContext,
&i.DynamicTools,
&i.OrganizationID,
); err != nil {
return nil, err
}
@@ -4422,9 +4423,9 @@ WITH 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
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
)
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
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
FROM chats
ORDER BY (id = $1::uuid) DESC, created_at ASC, id ASC
`
@@ -4463,6 +4464,7 @@ func (q *sqlQuerier) ArchiveChatByID(ctx context.Context, id uuid.UUID) ([]Chat,
&i.LastReadMessageID,
&i.LastInjectedContext,
&i.DynamicTools,
&i.OrganizationID,
); err != nil {
return nil, err
}
@@ -4612,7 +4614,7 @@ func (q *sqlQuerier) DeleteOldChats(ctx context.Context, arg DeleteOldChatsParam
}
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
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
FROM chats
WHERE agent_id = $1::uuid
AND archived = false
@@ -4657,6 +4659,7 @@ func (q *sqlQuerier) GetActiveChatsByAgentID(ctx context.Context, agentID uuid.U
&i.LastReadMessageID,
&i.LastInjectedContext,
&i.DynamicTools,
&i.OrganizationID,
); err != nil {
return nil, err
}
@@ -4673,7 +4676,7 @@ func (q *sqlQuerier) GetActiveChatsByAgentID(ctx context.Context, agentID uuid.U
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
id, owner_id, 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
FROM
chats
WHERE
@@ -4708,12 +4711,13 @@ func (q *sqlQuerier) GetChatByID(ctx context.Context, id uuid.UUID) (Chat, error
&i.LastReadMessageID,
&i.LastInjectedContext,
&i.DynamicTools,
&i.OrganizationID,
)
return i, err
}
const getChatByIDForUpdate = `-- name: GetChatByIDForUpdate :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 FROM chats WHERE id = $1::uuid FOR UPDATE
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 FROM chats WHERE id = $1::uuid FOR UPDATE
`
func (q *sqlQuerier) GetChatByIDForUpdate(ctx context.Context, id uuid.UUID) (Chat, error) {
@@ -4744,6 +4748,7 @@ func (q *sqlQuerier) GetChatByIDForUpdate(ctx context.Context, id uuid.UUID) (Ch
&i.LastReadMessageID,
&i.LastInjectedContext,
&i.DynamicTools,
&i.OrganizationID,
)
return i, err
}
@@ -5798,7 +5803,7 @@ func (q *sqlQuerier) GetChatUsageLimitUserOverride(ctx context.Context, userID u
const getChats = `-- name: GetChats :many
SELECT
chats.id, chats.owner_id, chats.workspace_id, chats.title, chats.status, chats.worker_id, chats.started_at, chats.heartbeat_at, chats.created_at, chats.updated_at, chats.parent_chat_id, chats.root_chat_id, chats.last_model_config_id, chats.archived, chats.last_error, chats.mode, chats.mcp_server_ids, chats.labels, chats.build_id, chats.agent_id, chats.pin_order, chats.last_read_message_id, chats.last_injected_context, chats.dynamic_tools,
chats.id, chats.owner_id, chats.workspace_id, chats.title, chats.status, chats.worker_id, chats.started_at, chats.heartbeat_at, chats.created_at, chats.updated_at, chats.parent_chat_id, chats.root_chat_id, chats.last_model_config_id, chats.archived, chats.last_error, chats.mode, chats.mcp_server_ids, chats.labels, chats.build_id, chats.agent_id, chats.pin_order, chats.last_read_message_id, chats.last_injected_context, chats.dynamic_tools, chats.organization_id,
EXISTS (
SELECT 1 FROM chat_messages cm
WHERE cm.chat_id = chats.id
@@ -5911,6 +5916,7 @@ func (q *sqlQuerier) GetChats(ctx context.Context, arg GetChatsParams) ([]GetCha
&i.Chat.LastReadMessageID,
&i.Chat.LastInjectedContext,
&i.Chat.DynamicTools,
&i.Chat.OrganizationID,
&i.HasUnread,
); err != nil {
return nil, err
@@ -5927,7 +5933,7 @@ func (q *sqlQuerier) GetChats(ctx context.Context, arg GetChatsParams) ([]GetCha
}
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
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
FROM chats
WHERE archived = false
AND workspace_id = ANY($1::uuid[])
@@ -5968,6 +5974,7 @@ func (q *sqlQuerier) GetChatsByWorkspaceIDs(ctx context.Context, ids []uuid.UUID
&i.LastReadMessageID,
&i.LastInjectedContext,
&i.DynamicTools,
&i.OrganizationID,
); err != nil {
return nil, err
}
@@ -6095,7 +6102,7 @@ func (q *sqlQuerier) GetLastChatMessageByRole(ctx context.Context, arg GetLastCh
const getStaleChats = `-- name: GetStaleChats :many
SELECT
id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools
id, owner_id, 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
FROM
chats
WHERE
@@ -6143,6 +6150,7 @@ func (q *sqlQuerier) GetStaleChats(ctx context.Context, staleThreshold time.Time
&i.LastReadMessageID,
&i.LastInjectedContext,
&i.DynamicTools,
&i.OrganizationID,
); err != nil {
return nil, err
}
@@ -6199,6 +6207,7 @@ func (q *sqlQuerier) GetUserGroupSpendLimit(ctx context.Context, userID uuid.UUI
const insertChat = `-- name: InsertChat :one
INSERT INTO chats (
organization_id,
owner_id,
workspace_id,
build_id,
@@ -6220,18 +6229,20 @@ INSERT INTO chats (
$5::uuid,
$6::uuid,
$7::uuid,
$8::text,
$9::chat_mode,
$10::chat_status,
COALESCE($11::uuid[], '{}'::uuid[]),
COALESCE($12::jsonb, '{}'::jsonb),
$13::jsonb
$8::uuid,
$9::text,
$10::chat_mode,
$11::chat_status,
COALESCE($12::uuid[], '{}'::uuid[]),
COALESCE($13::jsonb, '{}'::jsonb),
$14::jsonb
)
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
id, owner_id, 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
`
type InsertChatParams struct {
OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"`
OwnerID uuid.UUID `db:"owner_id" json:"owner_id"`
WorkspaceID uuid.NullUUID `db:"workspace_id" json:"workspace_id"`
BuildID uuid.NullUUID `db:"build_id" json:"build_id"`
@@ -6249,6 +6260,7 @@ type InsertChatParams struct {
func (q *sqlQuerier) InsertChat(ctx context.Context, arg InsertChatParams) (Chat, error) {
row := q.db.QueryRowContext(ctx, insertChat,
arg.OrganizationID,
arg.OwnerID,
arg.WorkspaceID,
arg.BuildID,
@@ -6289,6 +6301,7 @@ func (q *sqlQuerier) InsertChat(ctx context.Context, arg InsertChatParams) (Chat
&i.LastReadMessageID,
&i.LastInjectedContext,
&i.DynamicTools,
&i.OrganizationID,
)
return i, err
}
@@ -6796,9 +6809,9 @@ WITH 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
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
)
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
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
FROM chats
ORDER BY (id = $1::uuid) DESC, created_at ASC, id ASC
`
@@ -6841,6 +6854,7 @@ func (q *sqlQuerier) UnarchiveChatByID(ctx context.Context, id uuid.UUID) ([]Cha
&i.LastReadMessageID,
&i.LastInjectedContext,
&i.DynamicTools,
&i.OrganizationID,
); err != nil {
return nil, err
}
@@ -6921,7 +6935,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
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
`
type UpdateChatBuildAgentBindingParams struct {
@@ -6958,6 +6972,7 @@ func (q *sqlQuerier) UpdateChatBuildAgentBinding(ctx context.Context, arg Update
&i.LastReadMessageID,
&i.LastInjectedContext,
&i.DynamicTools,
&i.OrganizationID,
)
return i, err
}
@@ -6971,7 +6986,7 @@ SET
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
id, owner_id, 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
`
type UpdateChatByIDParams struct {
@@ -7007,6 +7022,7 @@ func (q *sqlQuerier) UpdateChatByID(ctx context.Context, arg UpdateChatByIDParam
&i.LastReadMessageID,
&i.LastInjectedContext,
&i.DynamicTools,
&i.OrganizationID,
)
return i, err
}
@@ -7065,7 +7081,7 @@ SET
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
id, owner_id, 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
`
type UpdateChatLabelsByIDParams struct {
@@ -7101,6 +7117,7 @@ func (q *sqlQuerier) UpdateChatLabelsByID(ctx context.Context, arg UpdateChatLab
&i.LastReadMessageID,
&i.LastInjectedContext,
&i.DynamicTools,
&i.OrganizationID,
)
return i, err
}
@@ -7110,7 +7127,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
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
`
type UpdateChatLastInjectedContextParams struct {
@@ -7150,6 +7167,7 @@ func (q *sqlQuerier) UpdateChatLastInjectedContext(ctx context.Context, arg Upda
&i.LastReadMessageID,
&i.LastInjectedContext,
&i.DynamicTools,
&i.OrganizationID,
)
return i, err
}
@@ -7163,7 +7181,7 @@ SET
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
id, owner_id, 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
`
type UpdateChatLastModelConfigByIDParams struct {
@@ -7199,6 +7217,7 @@ func (q *sqlQuerier) UpdateChatLastModelConfigByID(ctx context.Context, arg Upda
&i.LastReadMessageID,
&i.LastInjectedContext,
&i.DynamicTools,
&i.OrganizationID,
)
return i, err
}
@@ -7230,7 +7249,7 @@ SET
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
id, owner_id, 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
`
type UpdateChatMCPServerIDsParams struct {
@@ -7266,6 +7285,7 @@ func (q *sqlQuerier) UpdateChatMCPServerIDs(ctx context.Context, arg UpdateChatM
&i.LastReadMessageID,
&i.LastInjectedContext,
&i.DynamicTools,
&i.OrganizationID,
)
return i, err
}
@@ -7401,7 +7421,7 @@ SET
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
id, owner_id, 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
`
type UpdateChatStatusParams struct {
@@ -7448,6 +7468,7 @@ func (q *sqlQuerier) UpdateChatStatus(ctx context.Context, arg UpdateChatStatusP
&i.LastReadMessageID,
&i.LastInjectedContext,
&i.DynamicTools,
&i.OrganizationID,
)
return i, err
}
@@ -7465,7 +7486,7 @@ SET
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
id, owner_id, 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
`
type UpdateChatStatusPreserveUpdatedAtParams struct {
@@ -7514,6 +7535,7 @@ func (q *sqlQuerier) UpdateChatStatusPreserveUpdatedAt(ctx context.Context, arg
&i.LastReadMessageID,
&i.LastInjectedContext,
&i.DynamicTools,
&i.OrganizationID,
)
return i, err
}
@@ -7525,7 +7547,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
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
`
type UpdateChatWorkspaceBindingParams struct {
@@ -7568,6 +7590,7 @@ func (q *sqlQuerier) UpdateChatWorkspaceBinding(ctx context.Context, arg UpdateC
&i.LastReadMessageID,
&i.LastInjectedContext,
&i.DynamicTools,
&i.OrganizationID,
)
return i, err
}
+2
View File
@@ -392,6 +392,7 @@ LIMIT
-- name: InsertChat :one
INSERT INTO chats (
organization_id,
owner_id,
workspace_id,
build_id,
@@ -406,6 +407,7 @@ INSERT INTO chats (
labels,
dynamic_tools
) VALUES (
@organization_id::uuid,
@owner_id::uuid,
sqlc.narg('workspace_id')::uuid,
sqlc.narg('build_id')::uuid,