feat(db): add created_by column to chat_messages table (#22940)

Adds a `created_by` column (nullable UUID) to the `chat_messages` table
to track which user created each message. Only user-sent messages
populate this field; assistant, tool, system, and summary messages leave
it null.

The column is threaded through the full stack: SQL migration, query
updates, generated Go/TypeScript types, db2sdk conversion, chatd
(including subagent paths), and API handlers. All API handlers that
insert user messages now pass the authenticated user's ID as
`created_by`.

No foreign key constraint was added, matching the existing pattern used
by `chat_model_configs.created_by`.
This commit is contained in:
Kyle Carberry
2026-03-11 07:00:38 -07:00
committed by GitHub
parent c7c789f9e4
commit bb59477648
12 changed files with 66 additions and 15 deletions
+16 -1
View File
@@ -192,6 +192,7 @@ const (
// SendMessageOptions controls user message insertion with busy-state behavior.
type SendMessageOptions struct {
ChatID uuid.UUID
CreatedBy uuid.UUID
Content []fantasy.Content
ContentFileIDs map[int]uuid.UUID
ModelConfigID *uuid.UUID
@@ -209,6 +210,7 @@ type SendMessageResult struct {
// EditMessageOptions controls in-place user message edits.
type EditMessageOptions struct {
ChatID uuid.UUID
CreatedBy uuid.UUID
EditedMessageID int64
Content []fantasy.Content
ContentFileIDs map[int]uuid.UUID
@@ -223,6 +225,7 @@ type EditMessageResult struct {
// PromoteQueuedOptions controls queued-message promotion.
type PromoteQueuedOptions struct {
ChatID uuid.UUID
CreatedBy uuid.UUID
QueuedMessageID int64
ModelConfigID *uuid.UUID
}
@@ -266,7 +269,8 @@ func (p *Server) CreateChat(ctx context.Context, opts CreateOptions) (database.C
return xerrors.Errorf("marshal system prompt: %w", err)
}
_, err = tx.InsertChatMessage(ctx, database.InsertChatMessageParams{
ChatID: insertedChat.ID,
ChatID: insertedChat.ID,
CreatedBy: uuid.NullUUID{},
ModelConfigID: uuid.NullUUID{
UUID: opts.ModelConfigID,
Valid: true,
@@ -303,6 +307,7 @@ func (p *Server) CreateChat(ctx context.Context, opts CreateOptions) (database.C
},
Role: "user",
Content: userContent,
CreatedBy: uuid.NullUUID{UUID: opts.OwnerID, Valid: opts.OwnerID != uuid.Nil},
Visibility: database.ChatMessageVisibilityBoth,
InputTokens: sql.NullInt64{},
OutputTokens: sql.NullInt64{},
@@ -421,6 +426,7 @@ func (p *Server) SendMessage(
lockedChat,
modelConfigID,
content,
opts.CreatedBy,
)
if err != nil {
return err
@@ -736,6 +742,7 @@ func (p *Server) PromoteQueued(
RawMessage: targetContent,
Valid: len(targetContent) > 0,
},
opts.CreatedBy,
)
if err != nil {
return err
@@ -881,12 +888,14 @@ func insertUserMessageAndSetPending(
lockedChat database.Chat,
modelConfigID uuid.UUID,
content pqtype.NullRawMessage,
createdBy uuid.UUID,
) (database.ChatMessage, database.Chat, error) {
message, err := insertChatMessageWithStore(ctx, store, database.InsertChatMessageParams{
ChatID: lockedChat.ID,
ModelConfigID: uuid.NullUUID{UUID: modelConfigID, Valid: true},
Role: "user",
Content: content,
CreatedBy: uuid.NullUUID{UUID: createdBy, Valid: createdBy != uuid.Nil},
Visibility: database.ChatMessageVisibilityBoth,
InputTokens: sql.NullInt64{},
OutputTokens: sql.NullInt64{},
@@ -1948,6 +1957,7 @@ func (p *Server) processChat(ctx context.Context, chat database.Chat) {
RawMessage: nextQueued.Content,
Valid: len(nextQueued.Content) > 0,
},
CreatedBy: uuid.NullUUID{UUID: chat.OwnerID, Valid: chat.OwnerID != uuid.Nil},
Visibility: database.ChatMessageVisibilityBoth,
InputTokens: sql.NullInt64{},
OutputTokens: sql.NullInt64{},
@@ -2296,6 +2306,7 @@ func (p *Server) runChat(
hasUsage := step.Usage != (fantasy.Usage{})
assistantMessage, insertErr := tx.InsertChatMessage(persistCtx, database.InsertChatMessageParams{
ChatID: chat.ID,
CreatedBy: uuid.NullUUID{},
ModelConfigID: uuid.NullUUID{UUID: modelConfig.ID, Valid: true},
Role: string(fantasy.MessageRoleAssistant),
Content: assistantContent,
@@ -2329,6 +2340,7 @@ func (p *Server) runChat(
toolMessage, insertErr := tx.InsertChatMessage(persistCtx, database.InsertChatMessageParams{
ChatID: chat.ID,
CreatedBy: uuid.NullUUID{},
ModelConfigID: uuid.NullUUID{UUID: modelConfig.ID, Valid: true},
Role: string(fantasy.MessageRoleTool),
Content: resultContent,
@@ -2613,6 +2625,7 @@ func (p *Server) persistChatContextSummary(
txErr := p.db.InTx(func(tx database.Store) error {
_, txErr := tx.InsertChatMessage(ctx, database.InsertChatMessageParams{
ChatID: chatID,
CreatedBy: uuid.NullUUID{},
ModelConfigID: uuid.NullUUID{UUID: modelConfigID, Valid: true},
Role: string(fantasy.MessageRoleUser),
Content: pqtype.NullRawMessage{
@@ -2635,6 +2648,7 @@ func (p *Server) persistChatContextSummary(
assistantMessage, txErr := tx.InsertChatMessage(ctx, database.InsertChatMessageParams{
ChatID: chatID,
CreatedBy: uuid.NullUUID{},
ModelConfigID: uuid.NullUUID{UUID: modelConfigID, Valid: true},
Role: string(fantasy.MessageRoleAssistant),
Content: assistantContent,
@@ -2658,6 +2672,7 @@ func (p *Server) persistChatContextSummary(
toolMessage, txErr := tx.InsertChatMessage(ctx, database.InsertChatMessageParams{
ChatID: chatID,
CreatedBy: uuid.NullUUID{},
ModelConfigID: uuid.NullUUID{UUID: modelConfigID, Valid: true},
Role: string(fantasy.MessageRoleTool),
Content: toolResult,
+7
View File
@@ -289,8 +289,15 @@ func (p *Server) sendSubagentMessage(
return database.Chat{}, ErrSubagentNotDescendant
}
// Look up the target chat to get the owner for CreatedBy.
targetChat, err := p.db.GetChatByID(ctx, targetChatID)
if err != nil {
return database.Chat{}, xerrors.Errorf("get target chat: %w", err)
}
sendResult, err := p.SendMessage(ctx, SendMessageOptions{
ChatID: targetChatID,
CreatedBy: targetChat.OwnerID,
Content: []fantasy.Content{fantasy.TextContent{Text: message}},
BusyBehavior: busyBehavior,
})
+6
View File
@@ -606,6 +606,7 @@ func (api *API) unarchiveChat(rw http.ResponseWriter, r *http.Request) {
// EXPERIMENTAL: this endpoint is experimental and is subject to change.
func (api *API) postChatMessages(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
apiKey := httpmw.APIKey(r)
chat := httpmw.ChatParam(r)
chatID := chat.ID
@@ -635,6 +636,7 @@ func (api *API) postChatMessages(rw http.ResponseWriter, r *http.Request) {
ctx,
chatd.SendMessageOptions{
ChatID: chatID,
CreatedBy: apiKey.UserID,
Content: contentBlocks,
ContentFileIDs: contentFileIDs,
ModelConfigID: req.ModelConfigID,
@@ -672,6 +674,7 @@ func (api *API) postChatMessages(rw http.ResponseWriter, r *http.Request) {
// EXPERIMENTAL: this endpoint is experimental and is subject to change.
func (api *API) patchChatMessage(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
apiKey := httpmw.APIKey(r)
chat := httpmw.ChatParam(r)
if api.chatDaemon == nil {
@@ -708,6 +711,7 @@ func (api *API) patchChatMessage(rw http.ResponseWriter, r *http.Request) {
editResult, editErr := api.chatDaemon.EditMessage(ctx, chatd.EditMessageOptions{
ChatID: chat.ID,
CreatedBy: apiKey.UserID,
EditedMessageID: messageID,
Content: contentBlocks,
ContentFileIDs: contentFileIDs,
@@ -774,6 +778,7 @@ func (api *API) deleteChatQueuedMessage(rw http.ResponseWriter, r *http.Request)
// EXPERIMENTAL: this endpoint is experimental and is subject to change.
func (api *API) promoteChatQueuedMessage(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
apiKey := httpmw.APIKey(r)
chat := httpmw.ChatParam(r)
chatID := chat.ID
@@ -797,6 +802,7 @@ func (api *API) promoteChatQueuedMessage(rw http.ResponseWriter, r *http.Request
promoteResult, txErr := api.chatDaemon.PromoteQueued(ctx, chatd.PromoteQueuedOptions{
ChatID: chatID,
CreatedBy: apiKey.UserID,
QueuedMessageID: queuedMessageID,
})
+5
View File
@@ -1059,9 +1059,14 @@ func ChatMessage(m database.ChatMessage) codersdk.ChatMessage {
if !m.ModelConfigID.Valid {
modelConfigID = nil
}
createdBy := &m.CreatedBy.UUID
if !m.CreatedBy.Valid {
createdBy = nil
}
msg := codersdk.ChatMessage{
ID: m.ID,
ChatID: m.ChatID,
CreatedBy: createdBy,
ModelConfigID: modelConfigID,
CreatedAt: m.CreatedAt,
Role: m.Role,
+2 -1
View File
@@ -1215,7 +1215,8 @@ CREATE TABLE chat_messages (
cache_creation_tokens bigint,
cache_read_tokens bigint,
context_limit bigint,
compressed boolean DEFAULT false NOT NULL
compressed boolean DEFAULT false NOT NULL,
created_by uuid
);
CREATE SEQUENCE chat_messages_id_seq
@@ -0,0 +1 @@
ALTER TABLE chat_messages DROP COLUMN created_by;
@@ -0,0 +1 @@
ALTER TABLE chat_messages ADD COLUMN created_by uuid;
+1
View File
@@ -3952,6 +3952,7 @@ type ChatMessage struct {
CacheReadTokens sql.NullInt64 `db:"cache_read_tokens" json:"cache_read_tokens"`
ContextLimit sql.NullInt64 `db:"context_limit" json:"context_limit"`
Compressed bool `db:"compressed" json:"compressed"`
CreatedBy uuid.NullUUID `db:"created_by" json:"created_by"`
}
type ChatModelConfig struct {
+23 -13
View File
@@ -3342,7 +3342,7 @@ func (q *sqlQuerier) GetChatDiffStatusesByChatIDs(ctx context.Context, chatIds [
const getChatMessageByID = `-- name: GetChatMessageByID :one
SELECT
id, chat_id, model_config_id, created_at, role, content, visibility, input_tokens, output_tokens, total_tokens, reasoning_tokens, cache_creation_tokens, cache_read_tokens, context_limit, compressed
id, chat_id, model_config_id, created_at, role, content, visibility, input_tokens, output_tokens, total_tokens, reasoning_tokens, cache_creation_tokens, cache_read_tokens, context_limit, compressed, created_by
FROM
chat_messages
WHERE
@@ -3368,13 +3368,14 @@ func (q *sqlQuerier) GetChatMessageByID(ctx context.Context, id int64) (ChatMess
&i.CacheReadTokens,
&i.ContextLimit,
&i.Compressed,
&i.CreatedBy,
)
return i, err
}
const getChatMessagesByChatID = `-- name: GetChatMessagesByChatID :many
SELECT
id, chat_id, model_config_id, created_at, role, content, visibility, input_tokens, output_tokens, total_tokens, reasoning_tokens, cache_creation_tokens, cache_read_tokens, context_limit, compressed
id, chat_id, model_config_id, created_at, role, content, visibility, input_tokens, output_tokens, total_tokens, reasoning_tokens, cache_creation_tokens, cache_read_tokens, context_limit, compressed, created_by
FROM
chat_messages
WHERE
@@ -3415,6 +3416,7 @@ func (q *sqlQuerier) GetChatMessagesByChatID(ctx context.Context, arg GetChatMes
&i.CacheReadTokens,
&i.ContextLimit,
&i.Compressed,
&i.CreatedBy,
); err != nil {
return nil, err
}
@@ -3446,7 +3448,7 @@ WITH latest_compressed_summary AS (
1
)
SELECT
id, chat_id, model_config_id, created_at, role, content, visibility, input_tokens, output_tokens, total_tokens, reasoning_tokens, cache_creation_tokens, cache_read_tokens, context_limit, compressed
id, chat_id, model_config_id, created_at, role, content, visibility, input_tokens, output_tokens, total_tokens, reasoning_tokens, cache_creation_tokens, cache_read_tokens, context_limit, compressed, created_by
FROM
chat_messages
WHERE
@@ -3511,6 +3513,7 @@ func (q *sqlQuerier) GetChatMessagesForPromptByChatID(ctx context.Context, chatI
&i.CacheReadTokens,
&i.ContextLimit,
&i.Compressed,
&i.CreatedBy,
); err != nil {
return nil, err
}
@@ -3654,7 +3657,7 @@ func (q *sqlQuerier) GetChatsByOwnerID(ctx context.Context, arg GetChatsByOwnerI
const getLastChatMessageByRole = `-- name: GetLastChatMessageByRole :one
SELECT
id, chat_id, model_config_id, created_at, role, content, visibility, input_tokens, output_tokens, total_tokens, reasoning_tokens, cache_creation_tokens, cache_read_tokens, context_limit, compressed
id, chat_id, model_config_id, created_at, role, content, visibility, input_tokens, output_tokens, total_tokens, reasoning_tokens, cache_creation_tokens, cache_read_tokens, context_limit, compressed, created_by
FROM
chat_messages
WHERE
@@ -3690,6 +3693,7 @@ func (q *sqlQuerier) GetLastChatMessageByRole(ctx context.Context, arg GetLastCh
&i.CacheReadTokens,
&i.ContextLimit,
&i.Compressed,
&i.CreatedBy,
)
return i, err
}
@@ -3809,13 +3813,14 @@ WITH updated_chat AS (
UPDATE
chats
SET
last_model_config_id = $2::uuid
last_model_config_id = $3::uuid
WHERE
id = $1::uuid
AND $2::uuid IS NOT NULL
AND $3::uuid IS NOT NULL
)
INSERT INTO chat_messages (
chat_id,
created_by,
model_config_id,
role,
content,
@@ -3831,24 +3836,26 @@ INSERT INTO chat_messages (
) VALUES (
$1::uuid,
$2::uuid,
$3::text,
$4::jsonb,
$5::chat_message_visibility,
$6::bigint,
$3::uuid,
$4::text,
$5::jsonb,
$6::chat_message_visibility,
$7::bigint,
$8::bigint,
$9::bigint,
$10::bigint,
$11::bigint,
$12::bigint,
COALESCE($13::boolean, FALSE)
$13::bigint,
COALESCE($14::boolean, FALSE)
)
RETURNING
id, chat_id, model_config_id, created_at, role, content, visibility, input_tokens, output_tokens, total_tokens, reasoning_tokens, cache_creation_tokens, cache_read_tokens, context_limit, compressed
id, chat_id, model_config_id, created_at, role, content, visibility, input_tokens, output_tokens, total_tokens, reasoning_tokens, cache_creation_tokens, cache_read_tokens, context_limit, compressed, created_by
`
type InsertChatMessageParams struct {
ChatID uuid.UUID `db:"chat_id" json:"chat_id"`
CreatedBy uuid.NullUUID `db:"created_by" json:"created_by"`
ModelConfigID uuid.NullUUID `db:"model_config_id" json:"model_config_id"`
Role string `db:"role" json:"role"`
Content pqtype.NullRawMessage `db:"content" json:"content"`
@@ -3866,6 +3873,7 @@ type InsertChatMessageParams struct {
func (q *sqlQuerier) InsertChatMessage(ctx context.Context, arg InsertChatMessageParams) (ChatMessage, error) {
row := q.db.QueryRowContext(ctx, insertChatMessage,
arg.ChatID,
arg.CreatedBy,
arg.ModelConfigID,
arg.Role,
arg.Content,
@@ -3896,6 +3904,7 @@ func (q *sqlQuerier) InsertChatMessage(ctx context.Context, arg InsertChatMessag
&i.CacheReadTokens,
&i.ContextLimit,
&i.Compressed,
&i.CreatedBy,
)
return i, err
}
@@ -4130,7 +4139,7 @@ SET
WHERE
id = $3::bigint
RETURNING
id, chat_id, model_config_id, created_at, role, content, visibility, input_tokens, output_tokens, total_tokens, reasoning_tokens, cache_creation_tokens, cache_read_tokens, context_limit, compressed
id, chat_id, model_config_id, created_at, role, content, visibility, input_tokens, output_tokens, total_tokens, reasoning_tokens, cache_creation_tokens, cache_read_tokens, context_limit, compressed, created_by
`
type UpdateChatMessageByIDParams struct {
@@ -4158,6 +4167,7 @@ func (q *sqlQuerier) UpdateChatMessageByID(ctx context.Context, arg UpdateChatMe
&i.CacheReadTokens,
&i.ContextLimit,
&i.Compressed,
&i.CreatedBy,
)
return i, err
}
+2
View File
@@ -192,6 +192,7 @@ WITH updated_chat AS (
)
INSERT INTO chat_messages (
chat_id,
created_by,
model_config_id,
role,
content,
@@ -206,6 +207,7 @@ INSERT INTO chat_messages (
compressed
) VALUES (
@chat_id::uuid,
sqlc.narg('created_by')::uuid,
sqlc.narg('model_config_id')::uuid,
@role::text,
sqlc.narg('content')::jsonb,
+1
View File
@@ -49,6 +49,7 @@ type Chat struct {
type ChatMessage struct {
ID int64 `json:"id"`
ChatID uuid.UUID `json:"chat_id" format:"uuid"`
CreatedBy *uuid.UUID `json:"created_by,omitempty" format:"uuid"`
ModelConfigID *uuid.UUID `json:"model_config_id,omitempty" format:"uuid"`
CreatedAt time.Time `json:"created_at" format:"date-time"`
Role string `json:"role"`
+1
View File
@@ -1150,6 +1150,7 @@ export const ChatInputPartTypes: ChatInputPartType[] = [
export interface ChatMessage {
readonly id: number;
readonly chat_id: string;
readonly created_by?: string;
readonly model_config_id?: string;
readonly created_at: string;
readonly role: string;