feat(coderd/database): add chat_context_resources table (#26430)

Adds chat_context_resources: a per-chat pinned copy of the agent context
resources a chat is hydrated against. The agent-side table
(workspace_agent_context_resources) is last-writer-wins with no history,
so a chat copies its resources at hydration/refresh to keep a stable view
while the agent drifts.

Schema foundation only (no queries/dbauthz/prepareGeneration/SDK yet).
chat_id FK ON DELETE CASCADE for cleanup parity; no agent FK so the pin
survives agent replacement; PK (chat_id, source); reuses the 000522 enum
types.
This commit is contained in:
Kyle Carberry
2026-06-16 14:28:57 -07:00
committed by GitHub
parent 3e68dd304a
commit 53a6459ecd
10 changed files with 182 additions and 6 deletions
+38
View File
@@ -1748,6 +1748,38 @@ COMMENT ON COLUMN boundary_usage_stats.window_start IS 'Start of the time window
COMMENT ON COLUMN boundary_usage_stats.updated_at IS 'Timestamp of the last update to this row.'; COMMENT ON COLUMN boundary_usage_stats.updated_at IS 'Timestamp of the last update to this row.';
CREATE TABLE chat_context_resources (
chat_id uuid NOT NULL,
source text NOT NULL,
body_kind workspace_agent_context_body_kind NOT NULL,
body jsonb NOT NULL,
content_hash bytea NOT NULL,
size_bytes bigint NOT NULL,
status workspace_agent_context_resource_status NOT NULL,
error text DEFAULT ''::text NOT NULL,
source_path text DEFAULT ''::text NOT NULL,
created_at timestamp with time zone DEFAULT now() NOT NULL,
updated_at timestamp with time zone DEFAULT now() NOT NULL
);
COMMENT ON TABLE chat_context_resources IS 'Per-chat pinned copy of the agent context resources a chat is hydrated against. Copied from workspace_agent_context_resources at chat hydration and context refresh; survives agent replacement and workspace rebuilds.';
COMMENT ON COLUMN chat_context_resources.source IS 'Resource locator: canonical file path for file-backed kinds, or the MCP server name for mcp_server resources.';
COMMENT ON COLUMN chat_context_resources.body_kind IS 'Discriminator for the body JSON shape. Matches the proto oneof variant: instruction_file, skill, mcp_config, mcp_server. PLUGIN/HOOK/SUBAGENT/COMMAND are reserved for the Claude Code plugin RFC.';
COMMENT ON COLUMN chat_context_resources.body IS 'protojson-encoded variant body matching body_kind. Always populated; non-OK statuses use the variant zero value so the wire kind is still attributable.';
COMMENT ON COLUMN chat_context_resources.content_hash IS 'sha256 over the resource''s original bytes (or transport-encoded server tool list).';
COMMENT ON COLUMN chat_context_resources.size_bytes IS 'Original payload size in bytes; populated regardless of status.';
COMMENT ON COLUMN chat_context_resources.status IS 'Per-resource status. ok carries a populated body; oversize, unreadable, invalid, and excluded carry an empty body plus an error string.';
COMMENT ON COLUMN chat_context_resources.error IS 'Per-resource error or warning string. Populated whenever status is non-ok; may also carry a non-fatal warning when status is ok.';
COMMENT ON COLUMN chat_context_resources.source_path IS 'User-declared scan root that produced this resource. Empty for built-in scan roots.';
CREATE TABLE chat_debug_runs ( CREATE TABLE chat_debug_runs (
id uuid DEFAULT gen_random_uuid() NOT NULL, id uuid DEFAULT gen_random_uuid() NOT NULL,
chat_id uuid NOT NULL, chat_id uuid NOT NULL,
@@ -4096,6 +4128,9 @@ ALTER TABLE ONLY boundary_sessions
ALTER TABLE ONLY boundary_usage_stats ALTER TABLE ONLY boundary_usage_stats
ADD CONSTRAINT boundary_usage_stats_pkey PRIMARY KEY (replica_id); ADD CONSTRAINT boundary_usage_stats_pkey PRIMARY KEY (replica_id);
ALTER TABLE ONLY chat_context_resources
ADD CONSTRAINT chat_context_resources_pkey PRIMARY KEY (chat_id, source);
ALTER TABLE ONLY chat_debug_runs ALTER TABLE ONLY chat_debug_runs
ADD CONSTRAINT chat_debug_runs_pkey PRIMARY KEY (id); ADD CONSTRAINT chat_debug_runs_pkey PRIMARY KEY (id);
@@ -4906,6 +4941,9 @@ ALTER TABLE ONLY boundary_sessions
ALTER TABLE ONLY boundary_sessions ALTER TABLE ONLY boundary_sessions
ADD CONSTRAINT boundary_sessions_workspace_agent_id_fkey FOREIGN KEY (workspace_agent_id) REFERENCES workspace_agents(id); ADD CONSTRAINT boundary_sessions_workspace_agent_id_fkey FOREIGN KEY (workspace_agent_id) REFERENCES workspace_agents(id);
ALTER TABLE ONLY chat_context_resources
ADD CONSTRAINT chat_context_resources_chat_id_fkey FOREIGN KEY (chat_id) REFERENCES chats(id) ON DELETE CASCADE;
ALTER TABLE ONLY chat_debug_runs ALTER TABLE ONLY chat_debug_runs
ADD CONSTRAINT chat_debug_runs_chat_id_fkey FOREIGN KEY (chat_id) REFERENCES chats(id) ON DELETE CASCADE; ADD CONSTRAINT chat_debug_runs_chat_id_fkey FOREIGN KEY (chat_id) REFERENCES chats(id) ON DELETE CASCADE;
+1
View File
@@ -14,6 +14,7 @@ const (
ForeignKeyAPIKeysUserIDUUID ForeignKeyConstraint = "api_keys_user_id_uuid_fkey" // ALTER TABLE ONLY api_keys ADD CONSTRAINT api_keys_user_id_uuid_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; ForeignKeyAPIKeysUserIDUUID ForeignKeyConstraint = "api_keys_user_id_uuid_fkey" // ALTER TABLE ONLY api_keys ADD CONSTRAINT api_keys_user_id_uuid_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
ForeignKeyBoundarySessionsOwnerID ForeignKeyConstraint = "boundary_sessions_owner_id_fkey" // ALTER TABLE ONLY boundary_sessions ADD CONSTRAINT boundary_sessions_owner_id_fkey FOREIGN KEY (owner_id) REFERENCES users(id) ON DELETE SET NULL; ForeignKeyBoundarySessionsOwnerID ForeignKeyConstraint = "boundary_sessions_owner_id_fkey" // ALTER TABLE ONLY boundary_sessions ADD CONSTRAINT boundary_sessions_owner_id_fkey FOREIGN KEY (owner_id) REFERENCES users(id) ON DELETE SET NULL;
ForeignKeyBoundarySessionsWorkspaceAgentID ForeignKeyConstraint = "boundary_sessions_workspace_agent_id_fkey" // ALTER TABLE ONLY boundary_sessions ADD CONSTRAINT boundary_sessions_workspace_agent_id_fkey FOREIGN KEY (workspace_agent_id) REFERENCES workspace_agents(id); ForeignKeyBoundarySessionsWorkspaceAgentID ForeignKeyConstraint = "boundary_sessions_workspace_agent_id_fkey" // ALTER TABLE ONLY boundary_sessions ADD CONSTRAINT boundary_sessions_workspace_agent_id_fkey FOREIGN KEY (workspace_agent_id) REFERENCES workspace_agents(id);
ForeignKeyChatContextResourcesChatID ForeignKeyConstraint = "chat_context_resources_chat_id_fkey" // ALTER TABLE ONLY chat_context_resources ADD CONSTRAINT chat_context_resources_chat_id_fkey FOREIGN KEY (chat_id) REFERENCES chats(id) ON DELETE CASCADE;
ForeignKeyChatDebugRunsChatID ForeignKeyConstraint = "chat_debug_runs_chat_id_fkey" // ALTER TABLE ONLY chat_debug_runs ADD CONSTRAINT chat_debug_runs_chat_id_fkey FOREIGN KEY (chat_id) REFERENCES chats(id) ON DELETE CASCADE; ForeignKeyChatDebugRunsChatID ForeignKeyConstraint = "chat_debug_runs_chat_id_fkey" // ALTER TABLE ONLY chat_debug_runs ADD CONSTRAINT chat_debug_runs_chat_id_fkey FOREIGN KEY (chat_id) REFERENCES chats(id) ON DELETE CASCADE;
ForeignKeyChatDebugStepsChatID ForeignKeyConstraint = "chat_debug_steps_chat_id_fkey" // ALTER TABLE ONLY chat_debug_steps ADD CONSTRAINT chat_debug_steps_chat_id_fkey FOREIGN KEY (chat_id) REFERENCES chats(id) ON DELETE CASCADE; ForeignKeyChatDebugStepsChatID ForeignKeyConstraint = "chat_debug_steps_chat_id_fkey" // ALTER TABLE ONLY chat_debug_steps ADD CONSTRAINT chat_debug_steps_chat_id_fkey FOREIGN KEY (chat_id) REFERENCES chats(id) ON DELETE CASCADE;
ForeignKeyChatDiffStatusesChatID ForeignKeyConstraint = "chat_diff_statuses_chat_id_fkey" // ALTER TABLE ONLY chat_diff_statuses ADD CONSTRAINT chat_diff_statuses_chat_id_fkey FOREIGN KEY (chat_id) REFERENCES chats(id) ON DELETE CASCADE; ForeignKeyChatDiffStatusesChatID ForeignKeyConstraint = "chat_diff_statuses_chat_id_fkey" // ALTER TABLE ONLY chat_diff_statuses ADD CONSTRAINT chat_diff_statuses_chat_id_fkey FOREIGN KEY (chat_id) REFERENCES chats(id) ON DELETE CASCADE;
@@ -0,0 +1,4 @@
-- The workspace_agent_context_* enum types are owned by migration
-- 000522 and are still in use by workspace_agent_context_resources, so
-- they are intentionally left in place here.
DROP TABLE IF EXISTS chat_context_resources;
@@ -0,0 +1,30 @@
-- Creates chat_context_resources: a per-chat pinned copy of
-- workspace_agent_context_resources (semantics in COMMENT ON TABLE
-- below). Migration-specific notes: there is deliberately no FK to
-- workspace_agents so the pin survives agent replacement and workspace
-- rebuilds, and the body_kind/status enum types are reused from 000522
-- and must not be recreated here.
CREATE TABLE chat_context_resources (
chat_id UUID NOT NULL REFERENCES chats(id) ON DELETE CASCADE,
source TEXT NOT NULL,
body_kind workspace_agent_context_body_kind NOT NULL,
body JSONB NOT NULL,
content_hash BYTEA NOT NULL,
size_bytes BIGINT NOT NULL,
status workspace_agent_context_resource_status NOT NULL,
error TEXT NOT NULL DEFAULT '',
source_path TEXT NOT NULL DEFAULT '',
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
PRIMARY KEY (chat_id, source)
);
COMMENT ON TABLE chat_context_resources IS 'Per-chat pinned copy of the agent context resources a chat is hydrated against. Copied from workspace_agent_context_resources at chat hydration and context refresh; survives agent replacement and workspace rebuilds.';
COMMENT ON COLUMN chat_context_resources.source IS 'Resource locator: canonical file path for file-backed kinds, or the MCP server name for mcp_server resources.';
COMMENT ON COLUMN chat_context_resources.body_kind IS 'Discriminator for the body JSON shape. Matches the proto oneof variant: instruction_file, skill, mcp_config, mcp_server. PLUGIN/HOOK/SUBAGENT/COMMAND are reserved for the Claude Code plugin RFC.';
COMMENT ON COLUMN chat_context_resources.body IS 'protojson-encoded variant body matching body_kind. Always populated; non-OK statuses use the variant zero value so the wire kind is still attributable.';
COMMENT ON COLUMN chat_context_resources.content_hash IS 'sha256 over the resource''s original bytes (or transport-encoded server tool list).';
COMMENT ON COLUMN chat_context_resources.size_bytes IS 'Original payload size in bytes; populated regardless of status.';
COMMENT ON COLUMN chat_context_resources.status IS 'Per-resource status. ok carries a populated body; oversize, unreadable, invalid, and excluded carry an empty body plus an error string.';
COMMENT ON COLUMN chat_context_resources.error IS 'Per-resource error or warning string. Populated whenever status is non-ok; may also carry a non-fatal warning when status is ok.';
COMMENT ON COLUMN chat_context_resources.source_path IS 'User-declared scan root that produced this resource. Empty for built-in scan roots.';
@@ -0,0 +1,82 @@
-- Pinned context resources covering each non-reserved body kind plus a
-- non-OK status. The earlier chat fixtures already insert at least one row
-- into chats; we attach the resources to the first such chat (ordered
-- deterministically) so migration tests see a non-empty
-- chat_context_resources table without hard-coding a specific chat ID.
INSERT INTO chat_context_resources (
chat_id,
source,
body_kind,
body,
content_hash,
size_bytes,
status,
error,
source_path
)
SELECT
c.id,
v.source,
v.body_kind::workspace_agent_context_body_kind,
v.body::jsonb,
decode(v.content_hash, 'hex'),
v.size_bytes,
v.status::workspace_agent_context_resource_status,
v.error,
v.source_path
FROM (
SELECT id FROM chats ORDER BY created_at, id LIMIT 1
) AS c
CROSS JOIN (
VALUES
(
'/home/coder/workspace/AGENTS.md',
'instruction_file',
'{"content":"aGVsbG8="}',
'1111111111111111111111111111111111111111111111111111111111111111',
5::bigint,
'ok',
'',
''
),
(
'/home/coder/workspace/.agents/skills/example/SKILL.md',
'skill',
'{"meta":"LS0tCm5hbWU6IGV4YW1wbGUKLS0tCmJvZHk=","name":"example","description":"Example skill"}',
'2222222222222222222222222222222222222222222222222222222222222222',
32::bigint,
'ok',
'',
'/home/coder/workspace'
),
(
'/home/coder/workspace/.mcp.json',
'mcp_config',
'{}',
'3333333333333333333333333333333333333333333333333333333333333333',
128::bigint,
'ok',
'',
''
),
(
'mcp:echo',
'mcp_server',
'{"server_name":"echo","description":"echoes input"}',
'4444444444444444444444444444444444444444444444444444444444444444',
256::bigint,
'ok',
'',
'/home/coder/workspace/.mcp.json'
),
(
'/home/coder/workspace/big.md',
'instruction_file',
'{}',
'5555555555555555555555555555555555555555555555555555555555555555',
99999::bigint,
'oversize',
'file exceeds 64KiB per-resource cap',
''
)
) AS v(source, body_kind, body, content_hash, size_bytes, status, error, source_path);
+23
View File
@@ -4809,6 +4809,29 @@ type Chat struct {
ContextError string `db:"context_error" json:"context_error"` ContextError string `db:"context_error" json:"context_error"`
} }
// Per-chat pinned copy of the agent context resources a chat is hydrated against. Copied from workspace_agent_context_resources at chat hydration and context refresh; survives agent replacement and workspace rebuilds.
type ChatContextResource struct {
ChatID uuid.UUID `db:"chat_id" json:"chat_id"`
// Resource locator: canonical file path for file-backed kinds, or the MCP server name for mcp_server resources.
Source string `db:"source" json:"source"`
// Discriminator for the body JSON shape. Matches the proto oneof variant: instruction_file, skill, mcp_config, mcp_server. PLUGIN/HOOK/SUBAGENT/COMMAND are reserved for the Claude Code plugin RFC.
BodyKind WorkspaceAgentContextBodyKind `db:"body_kind" json:"body_kind"`
// protojson-encoded variant body matching body_kind. Always populated; non-OK statuses use the variant zero value so the wire kind is still attributable.
Body json.RawMessage `db:"body" json:"body"`
// sha256 over the resource's original bytes (or transport-encoded server tool list).
ContentHash []byte `db:"content_hash" json:"content_hash"`
// Original payload size in bytes; populated regardless of status.
SizeBytes int64 `db:"size_bytes" json:"size_bytes"`
// Per-resource status. ok carries a populated body; oversize, unreadable, invalid, and excluded carry an empty body plus an error string.
Status WorkspaceAgentContextResourceStatus `db:"status" json:"status"`
// Per-resource error or warning string. Populated whenever status is non-ok; may also carry a non-fatal warning when status is ok.
Error string `db:"error" json:"error"`
// User-declared scan root that produced this resource. Empty for built-in scan roots.
SourcePath string `db:"source_path" json:"source_path"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
}
type ChatDebugRun struct { type ChatDebugRun struct {
ID uuid.UUID `db:"id" json:"id"` ID uuid.UUID `db:"id" json:"id"`
ChatID uuid.UUID `db:"chat_id" json:"chat_id"` ChatID uuid.UUID `db:"chat_id" json:"chat_id"`
+1 -2
View File
@@ -200,8 +200,7 @@ type sqlcQuerier interface {
DeleteOldChatFiles(ctx context.Context, arg DeleteOldChatFilesParams) (int64, error) DeleteOldChatFiles(ctx context.Context, arg DeleteOldChatFilesParams) (int64, error)
// Deletes chats that have been archived for longer than the given // Deletes chats that have been archived for longer than the given
// threshold. Active (non-archived) chats are never deleted. // threshold. Active (non-archived) chats are never deleted.
// Related chat_messages, chat_diff_statuses, and // All chat-scoped child tables are removed via ON DELETE CASCADE.
// chat_queued_messages are removed via ON DELETE CASCADE.
// Parent/root references on child chats are SET NULL. // Parent/root references on child chats are SET NULL.
DeleteOldChats(ctx context.Context, arg DeleteOldChatsParams) (int64, error) DeleteOldChats(ctx context.Context, arg DeleteOldChatsParams) (int64, error)
DeleteOldConnectionLogs(ctx context.Context, arg DeleteOldConnectionLogsParams) (int64, error) DeleteOldConnectionLogs(ctx context.Context, arg DeleteOldConnectionLogsParams) (int64, error)
+1 -2
View File
@@ -6697,8 +6697,7 @@ type DeleteOldChatsParams struct {
// Deletes chats that have been archived for longer than the given // Deletes chats that have been archived for longer than the given
// threshold. Active (non-archived) chats are never deleted. // threshold. Active (non-archived) chats are never deleted.
// Related chat_messages, chat_diff_statuses, and // All chat-scoped child tables are removed via ON DELETE CASCADE.
// chat_queued_messages are removed via ON DELETE CASCADE.
// Parent/root references on child chats are SET NULL. // Parent/root references on child chats are SET NULL.
func (q *sqlQuerier) DeleteOldChats(ctx context.Context, arg DeleteOldChatsParams) (int64, error) { func (q *sqlQuerier) DeleteOldChats(ctx context.Context, arg DeleteOldChatsParams) (int64, error) {
result, err := q.db.ExecContext(ctx, deleteOldChats, arg.BeforeTime, arg.LimitCount) result, err := q.db.ExecContext(ctx, deleteOldChats, arg.BeforeTime, arg.LimitCount)
+1 -2
View File
@@ -2551,8 +2551,7 @@ WHERE id = @id::uuid;
-- name: DeleteOldChats :execrows -- name: DeleteOldChats :execrows
-- Deletes chats that have been archived for longer than the given -- Deletes chats that have been archived for longer than the given
-- threshold. Active (non-archived) chats are never deleted. -- threshold. Active (non-archived) chats are never deleted.
-- Related chat_messages, chat_diff_statuses, and -- All chat-scoped child tables are removed via ON DELETE CASCADE.
-- chat_queued_messages are removed via ON DELETE CASCADE.
-- Parent/root references on child chats are SET NULL. -- Parent/root references on child chats are SET NULL.
WITH deletable AS ( WITH deletable AS (
SELECT id SELECT id
+1
View File
@@ -21,6 +21,7 @@ const (
UniqueBoundaryLogsPkey UniqueConstraint = "boundary_logs_pkey" // ALTER TABLE ONLY boundary_logs ADD CONSTRAINT boundary_logs_pkey PRIMARY KEY (id); UniqueBoundaryLogsPkey UniqueConstraint = "boundary_logs_pkey" // ALTER TABLE ONLY boundary_logs ADD CONSTRAINT boundary_logs_pkey PRIMARY KEY (id);
UniqueBoundarySessionsPkey UniqueConstraint = "boundary_sessions_pkey" // ALTER TABLE ONLY boundary_sessions ADD CONSTRAINT boundary_sessions_pkey PRIMARY KEY (id); UniqueBoundarySessionsPkey UniqueConstraint = "boundary_sessions_pkey" // ALTER TABLE ONLY boundary_sessions ADD CONSTRAINT boundary_sessions_pkey PRIMARY KEY (id);
UniqueBoundaryUsageStatsPkey UniqueConstraint = "boundary_usage_stats_pkey" // ALTER TABLE ONLY boundary_usage_stats ADD CONSTRAINT boundary_usage_stats_pkey PRIMARY KEY (replica_id); UniqueBoundaryUsageStatsPkey UniqueConstraint = "boundary_usage_stats_pkey" // ALTER TABLE ONLY boundary_usage_stats ADD CONSTRAINT boundary_usage_stats_pkey PRIMARY KEY (replica_id);
UniqueChatContextResourcesPkey UniqueConstraint = "chat_context_resources_pkey" // ALTER TABLE ONLY chat_context_resources ADD CONSTRAINT chat_context_resources_pkey PRIMARY KEY (chat_id, source);
UniqueChatDebugRunsPkey UniqueConstraint = "chat_debug_runs_pkey" // ALTER TABLE ONLY chat_debug_runs ADD CONSTRAINT chat_debug_runs_pkey PRIMARY KEY (id); UniqueChatDebugRunsPkey UniqueConstraint = "chat_debug_runs_pkey" // ALTER TABLE ONLY chat_debug_runs ADD CONSTRAINT chat_debug_runs_pkey PRIMARY KEY (id);
UniqueChatDebugStepsPkey UniqueConstraint = "chat_debug_steps_pkey" // ALTER TABLE ONLY chat_debug_steps ADD CONSTRAINT chat_debug_steps_pkey PRIMARY KEY (id); UniqueChatDebugStepsPkey UniqueConstraint = "chat_debug_steps_pkey" // ALTER TABLE ONLY chat_debug_steps ADD CONSTRAINT chat_debug_steps_pkey PRIMARY KEY (id);
UniqueChatDiffStatusesPkey UniqueConstraint = "chat_diff_statuses_pkey" // ALTER TABLE ONLY chat_diff_statuses ADD CONSTRAINT chat_diff_statuses_pkey PRIMARY KEY (chat_id); UniqueChatDiffStatusesPkey UniqueConstraint = "chat_diff_statuses_pkey" // ALTER TABLE ONLY chat_diff_statuses ADD CONSTRAINT chat_diff_statuses_pkey PRIMARY KEY (chat_id);