mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-21 00:02:30 +00:00
fix: follow the turn running now when the listing named one already over
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
60b45118ff
commit
fb7f9ee4f5
+17
-8
@@ -234,15 +234,24 @@ class ChatImpl implements Chat {
|
||||
await this.#selecting
|
||||
if (!this.#turnActive(turn)) return
|
||||
// A listing is a snapshot: the turn it named can have ended and another one started
|
||||
// since. Following it would drop the newer turn's rows and leave the chat idle while
|
||||
// that turn runs, so it is left to the next listing to name the turn that runs now.
|
||||
const newerTurn = this.#state.messages.some(
|
||||
(m) => m.role === 'user' && m.seq !== undefined && m.seq > userSeq
|
||||
// since. The newest user message this chat holds names the turn to follow instead —
|
||||
// following the one the listing named would drop the newer turn's rows and leave the
|
||||
// chat idle while it runs.
|
||||
const newest = this.#state.messages.reduce<ChatMessage | undefined>(
|
||||
(found, m) =>
|
||||
m.role === 'user' && m.seq !== undefined && m.seq > (found?.seq ?? userSeq) ? m : found,
|
||||
undefined
|
||||
)
|
||||
if (newerTurn) {
|
||||
if (this.#turn === turn) this.#turn = undefined
|
||||
this.#set({ status: 'idle' })
|
||||
return
|
||||
if (newest) {
|
||||
if (!newest.jobId) {
|
||||
// Its row is written and its run is not named yet: there is nothing to follow, so
|
||||
// it is left to the next listing to name the turn that runs now.
|
||||
if (this.#turn === turn) this.#turn = undefined
|
||||
this.#set({ status: 'idle' })
|
||||
return
|
||||
}
|
||||
turn.jobId = newest.jobId
|
||||
userSeq = newest.seq!
|
||||
}
|
||||
// The stream replays the turn from its start, so the rows it already wrote go and
|
||||
// come back as it replays them. The message that started it stays: it is the turn's
|
||||
|
||||
@@ -966,25 +966,64 @@ describe('createChat with server history', () => {
|
||||
expect(state.conversations.map((c) => c.id)).toEqual(['conv'])
|
||||
})
|
||||
|
||||
test('a running turn the listing named is not followed once a newer one has started', async () => {
|
||||
test('a listing that names a turn already over follows the one running now', async () => {
|
||||
const { fetch, calls } = fetchMock(
|
||||
(c) =>
|
||||
c.url.pathname === '/api/w/ws/jobs_u/getupdate_sse/job-2'
|
||||
? sse([
|
||||
{
|
||||
type: 'update',
|
||||
new_result_stream: ndjson({ type: 'token_delta', content: 'second answer' }),
|
||||
stream_offset: 1,
|
||||
completed: true,
|
||||
only_result: { windmill_chat_answer: 'second answer' }
|
||||
}
|
||||
])
|
||||
: undefined,
|
||||
(c) =>
|
||||
c.url.pathname.endsWith('/jobs_u/get/job-2')
|
||||
? json({ flow_status: { modules: [{ job: 'step-2' }] } })
|
||||
: undefined,
|
||||
(c) => {
|
||||
if (!c.url.pathname.endsWith('/messages')) return undefined
|
||||
const after = c.url.searchParams.get('after_seq')
|
||||
if (after === '52') return json([messageRow(53, 'assistant', 'second answer', { job_id: 'step-2' })])
|
||||
return json([
|
||||
messageRow(50, 'user', 'first'),
|
||||
messageRow(51, 'assistant', 'first answer'),
|
||||
messageRow(52, 'user', 'second', { job_id: 'job-2' })
|
||||
])
|
||||
}
|
||||
)
|
||||
const chat = createChat(options({}, fetch))
|
||||
await chat.selectConversation('conv')
|
||||
// The listing named the first turn; it ended and the second one started before this select.
|
||||
await chat.resumeTurn({ jobId: 'job-1', userSeq: 50 })
|
||||
expect(calls.some((c) => c.url.pathname.includes('getupdate_sse/job-1'))).toBe(false)
|
||||
expect(calls.some((c) => c.url.pathname.includes('getupdate_sse/job-2'))).toBe(true)
|
||||
expect(chat.getState().messages.map((m) => [m.serverId, m.content])).toEqual([
|
||||
['row-50', 'first'],
|
||||
['row-51', 'first answer'],
|
||||
['row-52', 'second'],
|
||||
['row-53', 'second answer']
|
||||
])
|
||||
expect(chat.getState().status).toBe('idle')
|
||||
expect(chat.getState().error).toBeUndefined()
|
||||
})
|
||||
|
||||
test('a newer message whose run is not named yet leaves the chat free', async () => {
|
||||
const { fetch, calls } = fetchMock(
|
||||
(c) =>
|
||||
c.url.pathname.endsWith('/messages')
|
||||
? json([
|
||||
messageRow(50, 'user', 'first'),
|
||||
messageRow(51, 'assistant', 'first answer'),
|
||||
messageRow(52, 'user', 'second')
|
||||
])
|
||||
? json([messageRow(50, 'user', 'first'), messageRow(52, 'user', 'second')])
|
||||
: undefined
|
||||
)
|
||||
const chat = createChat(options({}, fetch))
|
||||
await chat.selectConversation('conv')
|
||||
// The listing named the first turn; it ended and another one started before this select.
|
||||
await chat.resumeTurn({ jobId: 'job-1', userSeq: 50 })
|
||||
expect(chat.getState().status).toBe('idle')
|
||||
expect(chat.getState().messages.map((m) => m.serverId)).toEqual(['row-50', 'row-51', 'row-52'])
|
||||
expect(calls.some((c) => c.url.pathname.includes('getupdate_sse'))).toBe(false)
|
||||
// The chat is free: a message sent now starts its own turn rather than being refused.
|
||||
// A message sent now starts its own turn rather than being refused.
|
||||
expect(chat.getState().error).toBeUndefined()
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user