fix(agent): chat console keeps its thread across turns and reloads (#1859)
Three user-reported failures in the assistant panel, one root cause each: 1. "The chat asks what I'm referring to" when continuing a thread. The single-call console (general.help, AskConsole -> /api/agent/ask) was stateless since the 08-20 model-agnostic cutover: conversationId was only the tool actor id, so every turn was answered blind, reload or not. The provider-agnostic GenerateTextRequest gains an optional `history` (real message turns before the prompt, in both the Anthropic-family and the OpenAI-compatible adapter; absent/empty leaves the request byte-identical to the single-turn call). The route loads the thread's earlier turns server-side (loadChatHistory: text only, hidden and tool rows dropped, alternation repaired, newest 16 rows / 10k chars) before writing the new question, and hands them to the model. 2. A full page reload (the deploy prompt's "Ladda om") closed the docked panel and dropped the thread from view. The panel now remembers its open thread per tab in sessionStorage (lib/agent-panel/session-restore) and the provider reopens it on mount; the sheet loads it exactly like a pick from "Tidigare konversationer". Close and "Ny konversation" forget it; a thread that no longer opens is dropped instead of retried on every reload. 3. "Can't type any more" once the update banner shows. DeployReloadPrompt's full-width wrapper sits at z-[60] after the panel in DOM order and swallowed clicks on the panel's composer; only the card takes input now. Claude-Session: https://claude.ai/code/session_01VjoXN3xdNZrHZeYA6qMi3g Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Jakob Wennberg
Claude Fable 5
parent
f75ea2384d
commit
3edbf0a2e3
@@ -20,10 +20,12 @@ vi.mock('@/lib/ai', () => ({ getAiStatus: () => aiStatus() }))
|
||||
const answer = vi.fn()
|
||||
vi.mock('@/lib/agent/ask/ask-service', () => ({ answerAssistantQuestion: (...a: unknown[]) => answer(...a) }))
|
||||
const resolveConv = vi.fn()
|
||||
const loadHistory = vi.fn()
|
||||
const persistUser = vi.fn()
|
||||
const persistAssistant = vi.fn()
|
||||
vi.mock('@/lib/agent/ask/persist', () => ({
|
||||
resolveChatConversation: (...a: unknown[]) => resolveConv(...a),
|
||||
loadChatHistory: (...a: unknown[]) => loadHistory(...a),
|
||||
persistUserTurn: (...a: unknown[]) => persistUser(...a),
|
||||
persistAssistantTurn: (...a: unknown[]) => persistAssistant(...a),
|
||||
}))
|
||||
@@ -42,6 +44,7 @@ beforeEach(() => {
|
||||
aiStatus.mockReturnValue({ configured: true, assistantAvailable: false, provider: 'openai-compatible' })
|
||||
answer.mockResolvedValue({ answer: 'Svar', model: 'qwen3.8' })
|
||||
resolveConv.mockResolvedValue({ ok: true, conversationId: 'conv-9', created: true })
|
||||
loadHistory.mockResolvedValue([])
|
||||
persistUser.mockResolvedValue(undefined)
|
||||
persistAssistant.mockResolvedValue(undefined)
|
||||
})
|
||||
@@ -130,6 +133,37 @@ describe('POST /api/agent/ask', () => {
|
||||
expect(persistUser).toHaveBeenCalledWith(supabase, 'conv-9', 'Hur gick juli?')
|
||||
expect(answer).toHaveBeenCalled()
|
||||
expect(persistAssistant).toHaveBeenCalledWith(supabase, 'conv-9', 'Svar')
|
||||
// A thread created by this very request has no earlier turns to load.
|
||||
expect(loadHistory).not.toHaveBeenCalled()
|
||||
expect(answer.mock.calls[0][0].history).toEqual([])
|
||||
})
|
||||
|
||||
it('a resumed thread answers with its earlier turns, read before the new question is written', async () => {
|
||||
resolveConv.mockResolvedValue({ ok: true, conversationId: 'conv-7', created: false })
|
||||
const history = [
|
||||
{ role: 'user', text: 'Vad är min största utgift?' },
|
||||
{ role: 'assistant', text: '12 345 kr på 5010.' },
|
||||
]
|
||||
const order: string[] = []
|
||||
loadHistory.mockImplementation(async () => {
|
||||
order.push('load')
|
||||
return history
|
||||
})
|
||||
persistUser.mockImplementation(async () => {
|
||||
order.push('persist')
|
||||
})
|
||||
const res = await POST(
|
||||
createMockRequest('/x', {
|
||||
method: 'POST',
|
||||
body: body({ persist: true, conversation_id: '11111111-1111-4111-8111-111111111111', question: 'Och förra månaden?' }),
|
||||
}),
|
||||
)
|
||||
expect(res.status).toBe(200)
|
||||
expect(loadHistory).toHaveBeenCalledWith(supabase, 'conv-7')
|
||||
expect(order).toEqual(['load', 'persist'])
|
||||
expect(answer).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ conversationId: 'conv-7', question: 'Och förra månaden?', history }),
|
||||
)
|
||||
})
|
||||
|
||||
it('resumes with a supplied conversation_id', async () => {
|
||||
|
||||
@@ -13,6 +13,7 @@ import { answerAssistantQuestion } from '@/lib/agent/ask/ask-service'
|
||||
import { EmptyModelAnswerError } from '@/lib/agent/ask/errors'
|
||||
import {
|
||||
resolveChatConversation,
|
||||
loadChatHistory,
|
||||
persistUserTurn,
|
||||
persistAssistantTurn,
|
||||
} from '@/lib/agent/ask/persist'
|
||||
@@ -152,6 +153,11 @@ export async function POST(request: Request): Promise<Response> {
|
||||
}
|
||||
const { conversationId } = resolved
|
||||
|
||||
// A resumed thread carries its earlier turns into the model call; read
|
||||
// them BEFORE the new question is written so it is not sent twice. A
|
||||
// thread created just now has nothing to load.
|
||||
const history = resolved.created ? [] : await loadChatHistory(supabase, conversationId)
|
||||
|
||||
await persistUserTurn(supabase, conversationId, parsed.data.question)
|
||||
|
||||
const result = await answerAssistantQuestion({
|
||||
@@ -162,6 +168,7 @@ export async function POST(request: Request): Promise<Response> {
|
||||
question: parsed.data.question,
|
||||
pageContext: parsed.data.context,
|
||||
tier: parsed.data.tier,
|
||||
history,
|
||||
})
|
||||
|
||||
// answerAssistantQuestion throws EmptyModelAnswerError on an empty answer,
|
||||
|
||||
Reference in New Issue
Block a user