mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-06 08:01:35 +00:00
fix: keep a detached job's card unsettled in a mid-turn save
This commit is contained in:
@@ -4597,10 +4597,15 @@ export class AIChatManager {
|
||||
// through here first.
|
||||
private settledToolDisplay = (
|
||||
messages: DisplayMessage[],
|
||||
messageText: string
|
||||
messageText: string,
|
||||
shouldSettle: (message: ToolDisplayMessage) => boolean = () => true
|
||||
): DisplayMessage[] =>
|
||||
messages.map((message) => {
|
||||
if (message.role === 'tool' && (message.isLoading || message.isQueued)) {
|
||||
if (
|
||||
message.role === 'tool' &&
|
||||
(message.isLoading || message.isQueued) &&
|
||||
shouldSettle(message)
|
||||
) {
|
||||
// Stopping the turn does not stop the job, and between Run and the job's id
|
||||
// there is no way to know whether the server queued one: nothing threads the
|
||||
// abort into that request, so it lands either way. That window says so
|
||||
@@ -4652,9 +4657,19 @@ export class AIChatManager {
|
||||
/** What the transcript would be if the turn stopped here — for the writes that fire
|
||||
* mid-turn without ending it. Loading is a property of this page: reloading resolves
|
||||
* no card, so one stored still pending comes back asking for input nothing can
|
||||
* deliver. Settles the stored copy only; the live turn keeps its cards. */
|
||||
#interruptedSnapshot = (): DisplayMessage[] =>
|
||||
this.settledToolDisplay(this.displayMessages, 'Interrupted')
|
||||
* deliver. Settles the stored copy only; the live turn keeps its cards.
|
||||
*
|
||||
* Except a detached job's card, which the poller does resolve after a reload: settling
|
||||
* that one stores an "Interrupted" error, and the patch a completed job merges in
|
||||
* carries no error to clear it with. */
|
||||
#interruptedSnapshot = (): DisplayMessage[] => {
|
||||
const detached = new Set(this.backgroundJobs.filter((j) => j.detached).map((j) => j.toolCallId))
|
||||
return this.settledToolDisplay(
|
||||
this.displayMessages,
|
||||
'Interrupted',
|
||||
(message) => !detached.has(message.tool_call_id)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export const aiChatManager = new AIChatManager()
|
||||
|
||||
@@ -3872,6 +3872,22 @@ describe('AIChatManager background job completion', () => {
|
||||
expect(manager.pendingJobNotes[0]).toContain('"rowCount": 2')
|
||||
})
|
||||
|
||||
// Detaching persists while the card is still loading. Storing it as interrupted would
|
||||
// stick, because the patch a completed job merges in carries no error to clear.
|
||||
it("stores a detached job's card unsettled, so a later success is not left an error", async () => {
|
||||
const manager = new AIChatManager()
|
||||
manager.registerJob(datatableJob)
|
||||
manager.applyToolStatus('tc-1', { content: 'running in background', isLoading: true })
|
||||
const saveChat = vi.spyOn(manager.historyManager, 'saveChat').mockResolvedValue(undefined)
|
||||
|
||||
manager.markJobDetached('job-1')
|
||||
await vi.waitFor(() => expect(saveChat).toHaveBeenCalled())
|
||||
|
||||
const stored = (saveChat.mock.calls.at(-1)?.[0] as any[]).find((m) => m.tool_call_id === 'tc-1')
|
||||
expect(stored.error).toBeUndefined()
|
||||
expect(stored.content).toBe('running in background')
|
||||
})
|
||||
|
||||
it('skips reconstruction and emits no note for a canceled detached job', async () => {
|
||||
const manager = new AIChatManager()
|
||||
manager.registerJob(datatableJob)
|
||||
|
||||
Reference in New Issue
Block a user