fix: persist per-turn model on chats and queued messages (#24688)

Previously, `chats.last_model_config_id` was not updated when a user
sent a mid-chat message with a different model, and queued messages did
not store their own per-turn model, so promotion ran against whatever
the chat row said at promote time. Chat watch events also did not merge
`last_model_config_id` into the site's root, child, and per-chat
caches, so sidebar labels stayed stale after direct sends and queued
promotions.

- Add nullable `chat_queued_messages.model_config_id`, backfilled from
  `chats.last_model_config_id`. Queued inserts round-trip the effective
  model id at enqueue time.
- In `coderd/x/chatd`, direct sends update `chats.last_model_config_id`
  inside the same transaction that inserts the admitted user message.
  Manual promotion and auto-promotion use the queued row's stored
  `model_config_id`, with a fallback to `chats.last_model_config_id`
for legacy NULL rows during rollout.
`PromoteQueuedOptions.ModelConfigID`
  is now ignored.
- On the site, extract `mergeWatchedChatSummary` and
  `mergeWatchedChatIntoCaches` in `site/src/api/queries/chats.ts` so
  status-change watch events merge `last_model_config_id` into the
  root infinite chat list, the parent-embedded child entry, and the
  per-chat `chatKey(chatId)` cache. `updated_at` guards against stale
  watch payloads clobbering newer cached state, while diff status
  events still merge their PR metadata because they are timestamped
  outside the chat row. Watch timestamps are compared as instants so
  variable fractional precision does not make fresh events look stale.
- Queued promotion validates stored model config IDs before admission.
  Invalid legacy queued IDs fall back to the chat's current model config
  instead of dropping the queued message during auto-promotion.
- Backend and frontend regression coverage added for admission, queue
  promotion (including FIFO across mixed models, legacy NULL fallback,
  and invalid queued model IDs), and chat watch cache merging.

> Mux is acting on Mike's behalf.
This commit is contained in:
Michael Suchacz
2026-04-24 15:36:08 +02:00
committed by GitHub
parent a876287d36
commit c7cac9debe
16 changed files with 1580 additions and 182 deletions
+16 -8
View File
@@ -6753,7 +6753,7 @@ func (q *sqlQuerier) GetChatModelConfigsForTelemetry(ctx context.Context) ([]Get
}
const getChatQueuedMessages = `-- name: GetChatQueuedMessages :many
SELECT id, chat_id, content, created_at FROM chat_queued_messages
SELECT id, chat_id, content, created_at, model_config_id FROM chat_queued_messages
WHERE chat_id = $1
ORDER BY id ASC
`
@@ -6772,6 +6772,7 @@ func (q *sqlQuerier) GetChatQueuedMessages(ctx context.Context, chatID uuid.UUID
&i.ChatID,
&i.Content,
&i.CreatedAt,
&i.ModelConfigID,
); err != nil {
return nil, err
}
@@ -7642,24 +7643,30 @@ func (q *sqlQuerier) InsertChatMessages(ctx context.Context, arg InsertChatMessa
}
const insertChatQueuedMessage = `-- name: InsertChatQueuedMessage :one
INSERT INTO chat_queued_messages (chat_id, content)
VALUES ($1, $2)
RETURNING id, chat_id, content, created_at
INSERT INTO chat_queued_messages (chat_id, content, model_config_id)
VALUES (
$1,
$2,
$3::uuid
)
RETURNING id, chat_id, content, created_at, model_config_id
`
type InsertChatQueuedMessageParams struct {
ChatID uuid.UUID `db:"chat_id" json:"chat_id"`
Content json.RawMessage `db:"content" json:"content"`
ChatID uuid.UUID `db:"chat_id" json:"chat_id"`
Content json.RawMessage `db:"content" json:"content"`
ModelConfigID uuid.NullUUID `db:"model_config_id" json:"model_config_id"`
}
func (q *sqlQuerier) InsertChatQueuedMessage(ctx context.Context, arg InsertChatQueuedMessageParams) (ChatQueuedMessage, error) {
row := q.db.QueryRowContext(ctx, insertChatQueuedMessage, arg.ChatID, arg.Content)
row := q.db.QueryRowContext(ctx, insertChatQueuedMessage, arg.ChatID, arg.Content, arg.ModelConfigID)
var i ChatQueuedMessage
err := row.Scan(
&i.ID,
&i.ChatID,
&i.Content,
&i.CreatedAt,
&i.ModelConfigID,
)
return i, err
}
@@ -7884,7 +7891,7 @@ WHERE id = (
ORDER BY cqm.id ASC
LIMIT 1
)
RETURNING id, chat_id, content, created_at
RETURNING id, chat_id, content, created_at, model_config_id
`
func (q *sqlQuerier) PopNextQueuedMessage(ctx context.Context, chatID uuid.UUID) (ChatQueuedMessage, error) {
@@ -7895,6 +7902,7 @@ func (q *sqlQuerier) PopNextQueuedMessage(ctx context.Context, chatID uuid.UUID)
&i.ChatID,
&i.Content,
&i.CreatedAt,
&i.ModelConfigID,
)
return i, err
}