Files
coder/coderd/database/queries/chats.sql
T
Kyle Carberry 34d9392e37 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>
2026-02-28 16:46:51 -05:00

409 lines
8.5 KiB
SQL

-- name: ArchiveChatByID :exec
UPDATE chats SET archived = true, updated_at = NOW() WHERE id = @id::uuid;
-- name: UnarchiveChatByID :exec
UPDATE chats SET archived = false, updated_at = NOW() WHERE id = @id::uuid;
-- name: DeleteChatMessagesByChatID :exec
DELETE FROM
chat_messages
WHERE
chat_id = @chat_id::uuid;
-- name: DeleteChatMessagesAfterID :exec
DELETE FROM
chat_messages
WHERE
chat_id = @chat_id::uuid
AND id > @after_id::bigint;
-- name: GetChatByID :one
SELECT
*
FROM
chats
WHERE
id = @id::uuid;
-- name: GetChatMessageByID :one
SELECT
*
FROM
chat_messages
WHERE
id = @id::bigint;
-- name: GetChatMessagesByChatID :many
SELECT
*
FROM
chat_messages
WHERE
chat_id = @chat_id::uuid
AND visibility IN ('user', 'both')
ORDER BY
created_at ASC;
-- name: GetChatMessagesForPromptByChatID :many
WITH latest_compressed_summary AS (
SELECT
id
FROM
chat_messages
WHERE
chat_id = @chat_id::uuid
AND role = 'system'
AND visibility IN ('model', 'both')
AND compressed = TRUE
ORDER BY
created_at DESC,
id DESC
LIMIT
1
)
SELECT
*
FROM
chat_messages
WHERE
chat_id = @chat_id::uuid
AND visibility IN ('model', 'both')
AND (
(
role = 'system'
AND compressed = FALSE
)
OR (
compressed = FALSE
AND (
NOT EXISTS (
SELECT
1
FROM
latest_compressed_summary
)
OR id > (
SELECT
id
FROM
latest_compressed_summary
)
)
)
OR id = (
SELECT
id
FROM
latest_compressed_summary
)
)
ORDER BY
created_at ASC,
id ASC;
-- name: GetChatsByOwnerID :many
SELECT
*
FROM
chats
WHERE
owner_id = @owner_id::uuid
AND archived = false
ORDER BY
updated_at DESC;
-- name: ListChildChatsByParentID :many
SELECT
*
FROM
chats
WHERE
parent_chat_id = @parent_chat_id::uuid
ORDER BY
created_at ASC;
-- name: ListChatsByRootID :many
SELECT
*
FROM
chats
WHERE
root_chat_id = @root_chat_id::uuid
ORDER BY
created_at ASC;
-- name: InsertChat :one
INSERT INTO chats (
owner_id,
workspace_id,
parent_chat_id,
root_chat_id,
last_model_config_id,
title
) VALUES (
@owner_id::uuid,
sqlc.narg('workspace_id')::uuid,
sqlc.narg('parent_chat_id')::uuid,
sqlc.narg('root_chat_id')::uuid,
@last_model_config_id::uuid,
@title::text
)
RETURNING
*;
-- name: InsertChatMessage :one
WITH updated_chat AS (
UPDATE
chats
SET
last_model_config_id = sqlc.narg('model_config_id')::uuid
WHERE
id = @chat_id::uuid
AND sqlc.narg('model_config_id')::uuid IS NOT NULL
)
INSERT INTO chat_messages (
chat_id,
model_config_id,
role,
content,
visibility,
input_tokens,
output_tokens,
total_tokens,
reasoning_tokens,
cache_creation_tokens,
cache_read_tokens,
context_limit,
compressed
) VALUES (
@chat_id::uuid,
sqlc.narg('model_config_id')::uuid,
@role::text,
sqlc.narg('content')::jsonb,
@visibility::chat_message_visibility,
sqlc.narg('input_tokens')::bigint,
sqlc.narg('output_tokens')::bigint,
sqlc.narg('total_tokens')::bigint,
sqlc.narg('reasoning_tokens')::bigint,
sqlc.narg('cache_creation_tokens')::bigint,
sqlc.narg('cache_read_tokens')::bigint,
sqlc.narg('context_limit')::bigint,
COALESCE(sqlc.narg('compressed')::boolean, FALSE)
)
RETURNING
*;
-- name: UpdateChatMessageByID :one
UPDATE
chat_messages
SET
model_config_id = COALESCE(sqlc.narg('model_config_id')::uuid, model_config_id),
content = sqlc.narg('content')::jsonb
WHERE
id = @id::bigint
RETURNING
*;
-- name: UpdateChatByID :one
UPDATE
chats
SET
title = @title::text,
updated_at = NOW()
WHERE
id = @id::uuid
RETURNING
*;
-- name: UpdateChatWorkspace :one
UPDATE
chats
SET
workspace_id = sqlc.narg('workspace_id')::uuid,
updated_at = NOW()
WHERE
id = @id::uuid
RETURNING
*;
-- name: AcquireChat :one
-- Acquires a pending chat for processing. Uses SKIP LOCKED to prevent
-- multiple replicas from acquiring the same chat.
UPDATE
chats
SET
status = 'running'::chat_status,
started_at = @started_at::timestamptz,
heartbeat_at = @started_at::timestamptz,
updated_at = @started_at::timestamptz,
worker_id = @worker_id::uuid
WHERE
id = (
SELECT
id
FROM
chats
WHERE
status = 'pending'::chat_status
ORDER BY
updated_at ASC
FOR UPDATE
SKIP LOCKED
LIMIT
1
)
RETURNING
*;
-- name: UpdateChatStatus :one
UPDATE
chats
SET
status = @status::chat_status,
worker_id = sqlc.narg('worker_id')::uuid,
started_at = sqlc.narg('started_at')::timestamptz,
heartbeat_at = sqlc.narg('heartbeat_at')::timestamptz,
last_error = sqlc.narg('last_error')::text,
updated_at = NOW()
WHERE
id = @id::uuid
RETURNING
*;
-- name: GetStaleChats :many
-- Find chats that appear stuck (running but heartbeat has expired).
-- Used for recovery after coderd crashes or long hangs.
SELECT
*
FROM
chats
WHERE
status = 'running'::chat_status
AND heartbeat_at < @stale_threshold::timestamptz;
-- name: UpdateChatHeartbeat :execrows
-- Bumps the heartbeat timestamp for a running chat so that other
-- replicas know the worker is still alive.
UPDATE
chats
SET
heartbeat_at = NOW()
WHERE
id = @id::uuid
AND worker_id = @worker_id::uuid
AND status = 'running'::chat_status;
-- name: GetChatDiffStatusByChatID :one
SELECT
*
FROM
chat_diff_statuses
WHERE
chat_id = @chat_id::uuid;
-- name: GetChatDiffStatusesByChatIDs :many
SELECT
*
FROM
chat_diff_statuses
WHERE
chat_id = ANY(@chat_ids::uuid[]);
-- name: UpsertChatDiffStatusReference :one
INSERT INTO chat_diff_statuses (
chat_id,
url,
git_branch,
git_remote_origin,
stale_at
) VALUES (
@chat_id::uuid,
sqlc.narg('url')::text,
@git_branch::text,
@git_remote_origin::text,
@stale_at::timestamptz
)
ON CONFLICT (chat_id) DO UPDATE
SET
url = CASE
WHEN EXCLUDED.url IS NOT NULL THEN EXCLUDED.url
ELSE chat_diff_statuses.url
END,
git_branch = CASE
WHEN EXCLUDED.git_branch != '' THEN EXCLUDED.git_branch
ELSE chat_diff_statuses.git_branch
END,
git_remote_origin = CASE
WHEN EXCLUDED.git_remote_origin != '' THEN EXCLUDED.git_remote_origin
ELSE chat_diff_statuses.git_remote_origin
END,
stale_at = EXCLUDED.stale_at,
updated_at = NOW()
RETURNING
*;
-- name: UpsertChatDiffStatus :one
INSERT INTO chat_diff_statuses (
chat_id,
url,
pull_request_state,
changes_requested,
additions,
deletions,
changed_files,
refreshed_at,
stale_at
) VALUES (
@chat_id::uuid,
sqlc.narg('url')::text,
sqlc.narg('pull_request_state')::text,
@changes_requested::boolean,
@additions::integer,
@deletions::integer,
@changed_files::integer,
@refreshed_at::timestamptz,
@stale_at::timestamptz
)
ON CONFLICT (chat_id) DO UPDATE
SET
url = EXCLUDED.url,
pull_request_state = EXCLUDED.pull_request_state,
changes_requested = EXCLUDED.changes_requested,
additions = EXCLUDED.additions,
deletions = EXCLUDED.deletions,
changed_files = EXCLUDED.changed_files,
refreshed_at = EXCLUDED.refreshed_at,
stale_at = EXCLUDED.stale_at,
updated_at = NOW()
RETURNING
*;
-- name: InsertChatQueuedMessage :one
INSERT INTO chat_queued_messages (chat_id, content)
VALUES (@chat_id, @content)
RETURNING *;
-- name: GetChatQueuedMessages :many
SELECT * FROM chat_queued_messages
WHERE chat_id = @chat_id
ORDER BY id ASC;
-- name: DeleteChatQueuedMessage :exec
DELETE FROM chat_queued_messages WHERE id = @id AND chat_id = @chat_id;
-- name: DeleteAllChatQueuedMessages :exec
DELETE FROM chat_queued_messages WHERE chat_id = @chat_id;
-- name: PopNextQueuedMessage :one
DELETE FROM chat_queued_messages
WHERE id = (
SELECT cqm.id FROM chat_queued_messages cqm
WHERE cqm.chat_id = @chat_id
ORDER BY cqm.id ASC
LIMIT 1
)
RETURNING *;
-- name: GetChatByIDForUpdate :one
SELECT * FROM chats WHERE id = @id::uuid FOR UPDATE;