chore(db): remove workspace_agent_id from chats table (#22442)

## Summary

Remove the `workspace_agent_id` column from the `chats` table and
dynamically look up the first workspace agent instead.

## Problem

When a workspace is stopped and restarted, the workspace agent gets a
new ID. The `workspace_agent_id` stored on the chat at creation time
becomes stale, making the agent unreachable. This caused chats to break
after workspace restarts.

## Solution

Instead of persisting the agent ID, dynamically look up the first agent
from the workspace's latest build via
`GetWorkspaceAgentsInLatestBuildByWorkspaceID` whenever an agent
connection is needed. The `workspace_id` on the chat remains stable
across restarts.

This behavior may be refined later (e.g., agent selection heuristics),
but picking the first agent resolves the immediate breakage.

## Changes

- **Migration 000425**: Drop `workspace_agent_id` column from `chats`
- **SQL queries**: Remove `workspace_agent_id` from `InsertChat` and
`UpdateChatWorkspace`
- **chatd.go**: `getWorkspaceConn` and `resolveInstructions` now look up
agents dynamically from workspace ID
- **chatd.go**: Remove `refreshChatWorkspaceSnapshot` (no longer needed)
- **createworkspace.go**: Stop persisting agent ID when associating
workspace with chat
- **subagent.go**: Stop passing agent ID to child chats
- **SDK/frontend**: Remove `WorkspaceAgentID` / `workspace_agent_id`
from Chat type

---------

Co-authored-by: Kyle Carberry <kylecarbs@gmail.com>
This commit is contained in:
Kyle Carberry
2026-02-28 16:46:51 -05:00
committed by GitHub
co-authored by Kyle Carberry
parent c316d0a3e7
commit 34d9392e37
18 changed files with 47 additions and 136 deletions
+2 -3
View File
@@ -694,9 +694,8 @@ func (s *MethodTestSuite) TestChats() {
s.Run("UpdateChatWorkspace", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
chat := testutil.Fake(s.T(), faker, database.Chat{})
arg := database.UpdateChatWorkspaceParams{
ID: chat.ID,
WorkspaceID: uuid.NullUUID{UUID: uuid.New(), Valid: true},
WorkspaceAgentID: uuid.NullUUID{UUID: uuid.New(), Valid: true},
ID: chat.ID,
WorkspaceID: uuid.NullUUID{UUID: uuid.New(), Valid: true},
}
updatedChat := testutil.Fake(s.T(), faker, database.Chat{ID: chat.ID})
dbm.EXPECT().GetChatByID(gomock.Any(), chat.ID).Return(chat, nil).AnyTimes()
-4
View File
@@ -1263,7 +1263,6 @@ CREATE TABLE chats (
id uuid DEFAULT gen_random_uuid() NOT NULL,
owner_id uuid NOT NULL,
workspace_id uuid,
workspace_agent_id uuid,
title text DEFAULT 'New Chat'::text NOT NULL,
status chat_status DEFAULT 'waiting'::chat_status NOT NULL,
worker_id uuid,
@@ -3790,9 +3789,6 @@ ALTER TABLE ONLY chats
ALTER TABLE ONLY chats
ADD CONSTRAINT chats_root_chat_id_fkey FOREIGN KEY (root_chat_id) REFERENCES chats(id) ON DELETE SET NULL;
ALTER TABLE ONLY chats
ADD CONSTRAINT chats_workspace_agent_id_fkey FOREIGN KEY (workspace_agent_id) REFERENCES workspace_agents(id) ON DELETE SET NULL;
ALTER TABLE ONLY chats
ADD CONSTRAINT chats_workspace_id_fkey FOREIGN KEY (workspace_id) REFERENCES workspaces(id) ON DELETE SET NULL;
@@ -21,7 +21,6 @@ const (
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;
ForeignKeyChatsWorkspaceAgentID ForeignKeyConstraint = "chats_workspace_agent_id_fkey" // ALTER TABLE ONLY chats ADD CONSTRAINT chats_workspace_agent_id_fkey FOREIGN KEY (workspace_agent_id) REFERENCES workspace_agents(id) ON DELETE SET NULL;
ForeignKeyChatsWorkspaceID ForeignKeyConstraint = "chats_workspace_id_fkey" // ALTER TABLE ONLY chats ADD CONSTRAINT chats_workspace_id_fkey FOREIGN KEY (workspace_id) REFERENCES workspaces(id) ON DELETE SET NULL;
ForeignKeyConnectionLogsOrganizationID ForeignKeyConstraint = "connection_logs_organization_id_fkey" // ALTER TABLE ONLY connection_logs ADD CONSTRAINT connection_logs_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE;
ForeignKeyConnectionLogsWorkspaceID ForeignKeyConstraint = "connection_logs_workspace_id_fkey" // ALTER TABLE ONLY connection_logs ADD CONSTRAINT connection_logs_workspace_id_fkey FOREIGN KEY (workspace_id) REFERENCES workspaces(id) ON DELETE CASCADE;
@@ -0,0 +1 @@
ALTER TABLE chats ADD COLUMN workspace_agent_id UUID REFERENCES workspace_agents(id) ON DELETE SET NULL;
@@ -0,0 +1 @@
ALTER TABLE chats DROP COLUMN workspace_agent_id;
-1
View File
@@ -3889,7 +3889,6 @@ 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"`
WorkspaceAgentID uuid.NullUUID `db:"workspace_agent_id" json:"workspace_agent_id"`
Title string `db:"title" json:"title"`
Status ChatStatus `db:"status" json:"status"`
WorkerID uuid.NullUUID `db:"worker_id" json:"worker_id"`
+16 -33
View File
@@ -2842,7 +2842,7 @@ WHERE
1
)
RETURNING
id, owner_id, workspace_id, workspace_agent_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
id, owner_id, 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
`
type AcquireChatParams struct {
@@ -2859,7 +2859,6 @@ func (q *sqlQuerier) AcquireChat(ctx context.Context, arg AcquireChatParams) (Ch
&i.ID,
&i.OwnerID,
&i.WorkspaceID,
&i.WorkspaceAgentID,
&i.Title,
&i.Status,
&i.WorkerID,
@@ -2940,7 +2939,7 @@ func (q *sqlQuerier) DeleteChatQueuedMessage(ctx context.Context, arg DeleteChat
const getChatByID = `-- name: GetChatByID :one
SELECT
id, owner_id, workspace_id, workspace_agent_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
id, owner_id, 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
FROM
chats
WHERE
@@ -2954,7 +2953,6 @@ func (q *sqlQuerier) GetChatByID(ctx context.Context, id uuid.UUID) (Chat, error
&i.ID,
&i.OwnerID,
&i.WorkspaceID,
&i.WorkspaceAgentID,
&i.Title,
&i.Status,
&i.WorkerID,
@@ -2972,7 +2970,7 @@ func (q *sqlQuerier) GetChatByID(ctx context.Context, id uuid.UUID) (Chat, error
}
const getChatByIDForUpdate = `-- name: GetChatByIDForUpdate :one
SELECT id, owner_id, workspace_id, workspace_agent_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 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 FROM chats WHERE id = $1::uuid FOR UPDATE
`
func (q *sqlQuerier) GetChatByIDForUpdate(ctx context.Context, id uuid.UUID) (Chat, error) {
@@ -2982,7 +2980,6 @@ func (q *sqlQuerier) GetChatByIDForUpdate(ctx context.Context, id uuid.UUID) (Ch
&i.ID,
&i.OwnerID,
&i.WorkspaceID,
&i.WorkspaceAgentID,
&i.Title,
&i.Status,
&i.WorkerID,
@@ -3291,7 +3288,7 @@ func (q *sqlQuerier) GetChatQueuedMessages(ctx context.Context, chatID uuid.UUID
const getChatsByOwnerID = `-- name: GetChatsByOwnerID :many
SELECT
id, owner_id, workspace_id, workspace_agent_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
id, owner_id, 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
FROM
chats
WHERE
@@ -3314,7 +3311,6 @@ func (q *sqlQuerier) GetChatsByOwnerID(ctx context.Context, ownerID uuid.UUID) (
&i.ID,
&i.OwnerID,
&i.WorkspaceID,
&i.WorkspaceAgentID,
&i.Title,
&i.Status,
&i.WorkerID,
@@ -3343,7 +3339,7 @@ func (q *sqlQuerier) GetChatsByOwnerID(ctx context.Context, ownerID uuid.UUID) (
const getStaleChats = `-- name: GetStaleChats :many
SELECT
id, owner_id, workspace_id, workspace_agent_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
id, owner_id, 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
FROM
chats
WHERE
@@ -3366,7 +3362,6 @@ func (q *sqlQuerier) GetStaleChats(ctx context.Context, staleThreshold time.Time
&i.ID,
&i.OwnerID,
&i.WorkspaceID,
&i.WorkspaceAgentID,
&i.Title,
&i.Status,
&i.WorkerID,
@@ -3397,7 +3392,6 @@ const insertChat = `-- name: InsertChat :one
INSERT INTO chats (
owner_id,
workspace_id,
workspace_agent_id,
parent_chat_id,
root_chat_id,
last_model_config_id,
@@ -3408,17 +3402,15 @@ INSERT INTO chats (
$3::uuid,
$4::uuid,
$5::uuid,
$6::uuid,
$7::text
$6::text
)
RETURNING
id, owner_id, workspace_id, workspace_agent_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
id, owner_id, 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
`
type InsertChatParams struct {
OwnerID uuid.UUID `db:"owner_id" json:"owner_id"`
WorkspaceID uuid.NullUUID `db:"workspace_id" json:"workspace_id"`
WorkspaceAgentID uuid.NullUUID `db:"workspace_agent_id" json:"workspace_agent_id"`
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"`
@@ -3429,7 +3421,6 @@ func (q *sqlQuerier) InsertChat(ctx context.Context, arg InsertChatParams) (Chat
row := q.db.QueryRowContext(ctx, insertChat,
arg.OwnerID,
arg.WorkspaceID,
arg.WorkspaceAgentID,
arg.ParentChatID,
arg.RootChatID,
arg.LastModelConfigID,
@@ -3440,7 +3431,6 @@ func (q *sqlQuerier) InsertChat(ctx context.Context, arg InsertChatParams) (Chat
&i.ID,
&i.OwnerID,
&i.WorkspaceID,
&i.WorkspaceAgentID,
&i.Title,
&i.Status,
&i.WorkerID,
@@ -3578,7 +3568,7 @@ func (q *sqlQuerier) InsertChatQueuedMessage(ctx context.Context, arg InsertChat
const listChatsByRootID = `-- name: ListChatsByRootID :many
SELECT
id, owner_id, workspace_id, workspace_agent_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
id, owner_id, 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
FROM
chats
WHERE
@@ -3600,7 +3590,6 @@ func (q *sqlQuerier) ListChatsByRootID(ctx context.Context, rootChatID uuid.UUID
&i.ID,
&i.OwnerID,
&i.WorkspaceID,
&i.WorkspaceAgentID,
&i.Title,
&i.Status,
&i.WorkerID,
@@ -3629,7 +3618,7 @@ func (q *sqlQuerier) ListChatsByRootID(ctx context.Context, rootChatID uuid.UUID
const listChildChatsByParentID = `-- name: ListChildChatsByParentID :many
SELECT
id, owner_id, workspace_id, workspace_agent_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
id, owner_id, 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
FROM
chats
WHERE
@@ -3651,7 +3640,6 @@ func (q *sqlQuerier) ListChildChatsByParentID(ctx context.Context, parentChatID
&i.ID,
&i.OwnerID,
&i.WorkspaceID,
&i.WorkspaceAgentID,
&i.Title,
&i.Status,
&i.WorkerID,
@@ -3719,7 +3707,7 @@ SET
WHERE
id = $2::uuid
RETURNING
id, owner_id, workspace_id, workspace_agent_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
id, owner_id, 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
`
type UpdateChatByIDParams struct {
@@ -3734,7 +3722,6 @@ func (q *sqlQuerier) UpdateChatByID(ctx context.Context, arg UpdateChatByIDParam
&i.ID,
&i.OwnerID,
&i.WorkspaceID,
&i.WorkspaceAgentID,
&i.Title,
&i.Status,
&i.WorkerID,
@@ -3831,7 +3818,7 @@ SET
WHERE
id = $6::uuid
RETURNING
id, owner_id, workspace_id, workspace_agent_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
id, owner_id, 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
`
type UpdateChatStatusParams struct {
@@ -3857,7 +3844,6 @@ func (q *sqlQuerier) UpdateChatStatus(ctx context.Context, arg UpdateChatStatusP
&i.ID,
&i.OwnerID,
&i.WorkspaceID,
&i.WorkspaceAgentID,
&i.Title,
&i.Status,
&i.WorkerID,
@@ -3879,28 +3865,25 @@ UPDATE
chats
SET
workspace_id = $1::uuid,
workspace_agent_id = $2::uuid,
updated_at = NOW()
WHERE
id = $3::uuid
id = $2::uuid
RETURNING
id, owner_id, workspace_id, workspace_agent_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
id, owner_id, 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
`
type UpdateChatWorkspaceParams struct {
WorkspaceID uuid.NullUUID `db:"workspace_id" json:"workspace_id"`
WorkspaceAgentID uuid.NullUUID `db:"workspace_agent_id" json:"workspace_agent_id"`
ID uuid.UUID `db:"id" json:"id"`
WorkspaceID uuid.NullUUID `db:"workspace_id" json:"workspace_id"`
ID uuid.UUID `db:"id" json:"id"`
}
func (q *sqlQuerier) UpdateChatWorkspace(ctx context.Context, arg UpdateChatWorkspaceParams) (Chat, error) {
row := q.db.QueryRowContext(ctx, updateChatWorkspace, arg.WorkspaceID, arg.WorkspaceAgentID, arg.ID)
row := q.db.QueryRowContext(ctx, updateChatWorkspace, arg.WorkspaceID, arg.ID)
var i Chat
err := row.Scan(
&i.ID,
&i.OwnerID,
&i.WorkspaceID,
&i.WorkspaceAgentID,
&i.Title,
&i.Status,
&i.WorkerID,
-3
View File
@@ -136,7 +136,6 @@ ORDER BY
INSERT INTO chats (
owner_id,
workspace_id,
workspace_agent_id,
parent_chat_id,
root_chat_id,
last_model_config_id,
@@ -144,7 +143,6 @@ INSERT INTO chats (
) VALUES (
@owner_id::uuid,
sqlc.narg('workspace_id')::uuid,
sqlc.narg('workspace_agent_id')::uuid,
sqlc.narg('parent_chat_id')::uuid,
sqlc.narg('root_chat_id')::uuid,
@last_model_config_id::uuid,
@@ -222,7 +220,6 @@ UPDATE
chats
SET
workspace_id = sqlc.narg('workspace_id')::uuid,
workspace_agent_id = sqlc.narg('workspace_agent_id')::uuid,
updated_at = NOW()
WHERE
id = @id::uuid