From da5a92ecf9de3da9b462c1d6fc83f7a350c19a8d Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Thu, 30 Jul 2026 19:31:09 -0500 Subject: [PATCH] fix(runs): persist the resolved modelId onto the chat row MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit handleStartChatRun resolved modelId and threaded it into the workflow input, but called provisionRunSession without it, so createSessionWithInitialChat inserted the chat with no model_id. The generation path reads the model from the chat row, so chats.model_id recorded the default for every headless run regardless of what the task configured. Measured on prod over 30 days: chats.model_id reads anthropic/claude-haiku-4.5 for 100% of scheduled-generation rooms, while the generation metadata shows 746 runs on sonnet-5, 238 on kimi-k3 and 55 on gpt-5.4-mini. The runs were correct; the column was not — so every audit keyed on it was wrong. chat#1918 --- .../runs/__tests__/handleStartChatRun.test.ts | 7 +++++++ .../runs/__tests__/provisionRunSession.test.ts | 11 +++++++++++ lib/chat/runs/handleStartChatRun.ts | 1 + lib/chat/runs/provisionRunSession.ts | 4 ++++ .../createSessionWithInitialChat.test.ts | 13 +++++++++++++ lib/sessions/createSessionWithInitialChat.ts | 15 ++++++++++++++- 6 files changed, 50 insertions(+), 1 deletion(-) diff --git a/lib/chat/runs/__tests__/handleStartChatRun.test.ts b/lib/chat/runs/__tests__/handleStartChatRun.test.ts index 3f1c8555..d66023a7 100644 --- a/lib/chat/runs/__tests__/handleStartChatRun.test.ts +++ b/lib/chat/runs/__tests__/handleStartChatRun.test.ts @@ -121,4 +121,11 @@ describe("handleStartChatRun", () => { expect(mintEphemeralAccountKey).not.toHaveBeenCalled(); expect(deleteApiKey).not.toHaveBeenCalled(); }); + + it("passes the resolved modelId to provisionRunSession so chats.model_id is accurate (chat#1918)", async () => { + await handleStartChatRun(req()); + expect(provisionRunSession).toHaveBeenCalledWith( + expect.objectContaining({ modelId: expect.any(String) }), + ); + }); }); diff --git a/lib/chat/runs/__tests__/provisionRunSession.test.ts b/lib/chat/runs/__tests__/provisionRunSession.test.ts index 3dd3a13b..ff51f732 100644 --- a/lib/chat/runs/__tests__/provisionRunSession.test.ts +++ b/lib/chat/runs/__tests__/provisionRunSession.test.ts @@ -77,4 +77,15 @@ describe("provisionRunSession", () => { expect(result.session).toEqual(updated); expect(discoverSkills).toHaveBeenCalled(); }); + + it("forwards modelId to createSessionWithInitialChat (chat#1918)", async () => { + await provisionRunSession({ + accountId: "acc-1", + title: "T", + modelId: "anthropic/claude-sonnet-5", + }); + expect(createSessionWithInitialChat).toHaveBeenCalledWith( + expect.objectContaining({ modelId: "anthropic/claude-sonnet-5" }), + ); + }); }); diff --git a/lib/chat/runs/handleStartChatRun.ts b/lib/chat/runs/handleStartChatRun.ts index 41b68d1c..a7dc7468 100644 --- a/lib/chat/runs/handleStartChatRun.ts +++ b/lib/chat/runs/handleStartChatRun.ts @@ -45,6 +45,7 @@ export async function handleStartChatRun(request: NextRequest): Promise { const created = await createSessionWithInitialChat({ accountId, title, chatTitle: "Scheduled generation", artistId, + modelId, }); if (created.ok === false) { throw new Error( diff --git a/lib/sessions/__tests__/createSessionWithInitialChat.test.ts b/lib/sessions/__tests__/createSessionWithInitialChat.test.ts index f30286ff..ba960c27 100644 --- a/lib/sessions/__tests__/createSessionWithInitialChat.test.ts +++ b/lib/sessions/__tests__/createSessionWithInitialChat.test.ts @@ -60,4 +60,17 @@ describe("createSessionWithInitialChat", () => { expect(r).toEqual({ ok: false, reason: "insert" }); expect(deleteSessionById).toHaveBeenCalledWith("sess-1"); }); + + it("persists modelId onto the chat row so the run's model is recorded (chat#1918)", async () => { + await createSessionWithInitialChat({ ...args, modelId: "moonshotai/kimi-k3" }); + expect(insertChat).toHaveBeenCalledWith( + expect.objectContaining({ model_id: "moonshotai/kimi-k3" }), + ); + }); + + it("omits model_id when no modelId is supplied", async () => { + await createSessionWithInitialChat(args); + const row = vi.mocked(insertChat).mock.calls[0]![0] as Record; + expect(row.model_id).toBeUndefined(); + }); }); diff --git a/lib/sessions/createSessionWithInitialChat.ts b/lib/sessions/createSessionWithInitialChat.ts index 766c2053..897a95b4 100644 --- a/lib/sessions/createSessionWithInitialChat.ts +++ b/lib/sessions/createSessionWithInitialChat.ts @@ -35,12 +35,20 @@ export async function createSessionWithInitialChat({ title, chatTitle, artistId, + modelId, }: { accountId: string; workspaceAccountId?: string; title: string; chatTitle: string; artistId?: string; + /** + * Model the run will execute on. Persisted onto the chat so `chats.model_id` + * reflects reality — the generation path reads the model from the chat row, + * so leaving it unset made every headless run record the default regardless + * of what the task configured (chat#1918). + */ + modelId?: string; }): Promise { const cloneUrl = await ensurePersonalRepo({ accountId: workspaceAccountId ?? accountId }); if (!cloneUrl) return { ok: false, reason: "repo" }; @@ -50,7 +58,12 @@ export async function createSessionWithInitialChat({ ); if (!session) return { ok: false, reason: "insert" }; - const chat = await insertChat({ id: generateUUID(), session_id: session.id, title: chatTitle }); + const chat = await insertChat({ + id: generateUUID(), + session_id: session.id, + title: chatTitle, + ...(modelId ? { model_id: modelId } : {}), + }); if (!chat) { const rolledBack = await deleteSessionById(session.id); if (!rolledBack) {