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
+20 -49
View File
@@ -133,7 +133,6 @@ var (
type CreateOptions struct {
OwnerID uuid.UUID
WorkspaceID uuid.NullUUID
WorkspaceAgentID uuid.NullUUID
ParentChatID uuid.NullUUID
RootChatID uuid.NullUUID
Title string
@@ -212,7 +211,6 @@ func (p *Server) CreateChat(ctx context.Context, opts CreateOptions) (database.C
insertedChat, err := tx.InsertChat(ctx, database.InsertChatParams{
OwnerID: opts.OwnerID,
WorkspaceID: opts.WorkspaceID,
WorkspaceAgentID: opts.WorkspaceAgentID,
ParentChatID: opts.ParentChatID,
RootChatID: opts.RootChatID,
LastModelConfigID: opts.ModelConfigID,
@@ -1421,10 +1419,6 @@ func (p *Server) publishChatPubsubEvent(chat database.Chat, kind coderdpubsub.Ch
if chat.WorkspaceID.Valid {
sdkChat.WorkspaceID = &chat.WorkspaceID.UUID
}
if chat.WorkspaceAgentID.Valid {
sdkChat.WorkspaceAgentID = &chat.WorkspaceAgentID.UUID
}
event := coderdpubsub.ChatEvent{
Kind: kind,
Chat: sdkChat,
@@ -1825,13 +1819,6 @@ func (p *Server) runChat(
}()
currentChat := chat
loadChatSnapshot := func(
loadCtx context.Context,
chatID uuid.UUID,
) (database.Chat, error) {
//nolint:gocritic // System context required to load chat snapshots for the stream.
return p.db.GetChatByID(dbauthz.AsSystemRestricted(loadCtx), chatID)
}
var (
chatStateMu sync.Mutex
workspaceMu sync.Mutex
@@ -1860,28 +1847,20 @@ func (p *Server) runChat(
return nil, xerrors.New("workspace agent connector is not configured")
}
if !chatSnapshot.WorkspaceAgentID.Valid {
refreshedChat, refreshErr := refreshChatWorkspaceSnapshot(
ctx,
chatSnapshot,
loadChatSnapshot,
)
if refreshErr != nil {
return nil, refreshErr
}
if refreshedChat.WorkspaceAgentID.Valid {
chatStateMu.Lock()
currentChat = refreshedChat
chatSnapshot = refreshedChat
chatStateMu.Unlock()
}
if !chatSnapshot.WorkspaceID.Valid {
return nil, xerrors.New("chat has no workspace")
}
if !chatSnapshot.WorkspaceAgentID.Valid {
//nolint:gocritic // System context needed to look up workspace agents.
agents, err := p.db.GetWorkspaceAgentsInLatestBuildByWorkspaceID(
dbauthz.AsSystemRestricted(ctx),
chatSnapshot.WorkspaceID.UUID,
)
if err != nil || len(agents) == 0 {
return nil, xerrors.New("chat has no workspace agent")
}
agentConn, agentRelease, err := p.agentConnFn(ctx, chatSnapshot.WorkspaceAgentID.UUID)
agentConn, agentRelease, err := p.agentConnFn(ctx, agents[0].ID)
if err != nil {
return nil, xerrors.Errorf("connect to workspace agent: %w", err)
}
@@ -2376,23 +2355,6 @@ func usageNullInt64(value int64, valid bool) sql.NullInt64 {
}
}
func refreshChatWorkspaceSnapshot(
ctx context.Context,
chat database.Chat,
loadChat func(context.Context, uuid.UUID) (database.Chat, error),
) (database.Chat, error) {
if chat.WorkspaceAgentID.Valid || loadChat == nil {
return chat, nil
}
refreshedChat, err := loadChat(ctx, chat.ID)
if err != nil {
return chat, xerrors.Errorf("reload chat workspace state: %w", err)
}
return refreshedChat, nil
}
// resolveInstructions returns the combined system instructions for the
// workspace agent. It reads the home-level (~/.coder/AGENTS.md) and
// working-directory-level (<pwd>/AGENTS.md) instruction files, combines
@@ -2402,10 +2364,19 @@ func (p *Server) resolveInstructions(
chat database.Chat,
getWorkspaceConn func(context.Context) (workspacesdk.AgentConn, error),
) string {
if !chat.WorkspaceAgentID.Valid {
if !chat.WorkspaceID.Valid {
return ""
}
agentID := chat.WorkspaceAgentID.UUID
//nolint:gocritic // System context needed to look up workspace agents.
agents, agentsErr := p.db.GetWorkspaceAgentsInLatestBuildByWorkspaceID(
dbauthz.AsSystemRestricted(ctx),
chat.WorkspaceID.UUID,
)
if agentsErr != nil || len(agents) == 0 {
return ""
}
agentID := agents[0].ID
p.instructionCacheMu.Lock()
cached, ok := p.instructionCache[agentID]
+3 -6
View File
@@ -191,10 +191,6 @@ func CreateWorkspace(options CreateWorkspaceOptions) fantasy.AgentTool {
UUID: workspace.ID,
Valid: true,
},
WorkspaceAgentID: uuid.NullUUID{
UUID: workspaceAgentID,
Valid: workspaceAgentID != uuid.Nil,
},
})
}
@@ -281,14 +277,15 @@ func checkExistingWorkspace(
case database.ProvisionerJobStatusSucceeded:
// Build succeeded — check if agent is reachable.
if chat.WorkspaceAgentID.Valid && agentConnFn != nil {
agents, agentsErr := db.GetWorkspaceAgentsInLatestBuildByWorkspaceID(ctx, ws.ID)
if agentsErr == nil && len(agents) > 0 && agentConnFn != nil {
pingCtx, cancel := context.WithTimeout(
ctx, agentPingTimeout,
)
defer cancel()
conn, release, connErr := agentConnFn(
pingCtx, chat.WorkspaceAgentID.UUID,
pingCtx, agents[0].ID,
)
if connErr == nil {
release()
+2 -3
View File
@@ -240,9 +240,8 @@ func (p *Server) createChildSubagentChat(
}
child, err := p.CreateChat(ctx, CreateOptions{
OwnerID: parent.OwnerID,
WorkspaceID: parent.WorkspaceID,
WorkspaceAgentID: parent.WorkspaceAgentID,
OwnerID: parent.OwnerID,
WorkspaceID: parent.WorkspaceID,
ParentChatID: uuid.NullUUID{
UUID: parent.ID,
Valid: true,