fix: refresh lastUpdate on all message.part.updated events, not just tool events
Reasoning/thinking models (Oracle, Claude Opus) were being killed by the stale timeout because lastUpdate was only refreshed on tool-type events. During extended thinking, no tool events fire, so after 3 minutes the task was incorrectly marked as stale and aborted. Move progress initialization and lastUpdate refresh before the tool-type conditional so any message.part.updated event (text, thinking, tool) keeps the task alive.
This commit is contained in:
parent
a809ac3dfc
commit
4ab93c0cf7
@ -69,13 +69,14 @@ export function handleBackgroundEvent(args: {
|
|||||||
const type = getString(props, "type")
|
const type = getString(props, "type")
|
||||||
const tool = getString(props, "tool")
|
const tool = getString(props, "tool")
|
||||||
|
|
||||||
|
if (!task.progress) {
|
||||||
|
task.progress = { toolCalls: 0, lastUpdate: new Date() }
|
||||||
|
}
|
||||||
|
task.progress.lastUpdate = new Date()
|
||||||
|
|
||||||
if (type === "tool" || tool) {
|
if (type === "tool" || tool) {
|
||||||
if (!task.progress) {
|
|
||||||
task.progress = { toolCalls: 0, lastUpdate: new Date() }
|
|
||||||
}
|
|
||||||
task.progress.toolCalls += 1
|
task.progress.toolCalls += 1
|
||||||
task.progress.lastTool = tool
|
task.progress.lastTool = tool
|
||||||
task.progress.lastUpdate = new Date()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -3045,3 +3045,161 @@ describe("BackgroundManager.handleEvent - early session.idle deferral", () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe("BackgroundManager.handleEvent - non-tool event lastUpdate", () => {
|
||||||
|
test("should update lastUpdate on text-type message.part.updated event", () => {
|
||||||
|
//#given - a running task with stale lastUpdate
|
||||||
|
const client = {
|
||||||
|
session: {
|
||||||
|
prompt: async () => ({}),
|
||||||
|
promptAsync: async () => ({}),
|
||||||
|
abort: async () => ({}),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
const manager = new BackgroundManager({ client, directory: tmpdir() } as unknown as PluginInput)
|
||||||
|
|
||||||
|
const oldUpdate = new Date(Date.now() - 300_000)
|
||||||
|
const task: BackgroundTask = {
|
||||||
|
id: "task-text-1",
|
||||||
|
sessionID: "session-text-1",
|
||||||
|
parentSessionID: "parent-1",
|
||||||
|
parentMessageID: "msg-1",
|
||||||
|
description: "Thinking task",
|
||||||
|
prompt: "Think deeply",
|
||||||
|
agent: "oracle",
|
||||||
|
status: "running",
|
||||||
|
startedAt: new Date(Date.now() - 600_000),
|
||||||
|
progress: {
|
||||||
|
toolCalls: 2,
|
||||||
|
lastUpdate: oldUpdate,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
getTaskMap(manager).set(task.id, task)
|
||||||
|
|
||||||
|
//#when - a text-type message.part.updated event arrives
|
||||||
|
manager.handleEvent({
|
||||||
|
type: "message.part.updated",
|
||||||
|
properties: { sessionID: "session-text-1", type: "text" },
|
||||||
|
})
|
||||||
|
|
||||||
|
//#then - lastUpdate should be refreshed, toolCalls should NOT change
|
||||||
|
expect(task.progress!.lastUpdate.getTime()).toBeGreaterThan(oldUpdate.getTime())
|
||||||
|
expect(task.progress!.toolCalls).toBe(2)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("should update lastUpdate on thinking-type message.part.updated event", () => {
|
||||||
|
//#given - a running task with stale lastUpdate
|
||||||
|
const client = {
|
||||||
|
session: {
|
||||||
|
prompt: async () => ({}),
|
||||||
|
promptAsync: async () => ({}),
|
||||||
|
abort: async () => ({}),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
const manager = new BackgroundManager({ client, directory: tmpdir() } as unknown as PluginInput)
|
||||||
|
|
||||||
|
const oldUpdate = new Date(Date.now() - 300_000)
|
||||||
|
const task: BackgroundTask = {
|
||||||
|
id: "task-thinking-1",
|
||||||
|
sessionID: "session-thinking-1",
|
||||||
|
parentSessionID: "parent-1",
|
||||||
|
parentMessageID: "msg-1",
|
||||||
|
description: "Reasoning task",
|
||||||
|
prompt: "Reason about architecture",
|
||||||
|
agent: "oracle",
|
||||||
|
status: "running",
|
||||||
|
startedAt: new Date(Date.now() - 600_000),
|
||||||
|
progress: {
|
||||||
|
toolCalls: 0,
|
||||||
|
lastUpdate: oldUpdate,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
getTaskMap(manager).set(task.id, task)
|
||||||
|
|
||||||
|
//#when - a thinking-type message.part.updated event arrives
|
||||||
|
manager.handleEvent({
|
||||||
|
type: "message.part.updated",
|
||||||
|
properties: { sessionID: "session-thinking-1", type: "thinking" },
|
||||||
|
})
|
||||||
|
|
||||||
|
//#then - lastUpdate should be refreshed, toolCalls should remain 0
|
||||||
|
expect(task.progress!.lastUpdate.getTime()).toBeGreaterThan(oldUpdate.getTime())
|
||||||
|
expect(task.progress!.toolCalls).toBe(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("should initialize progress on first non-tool event", () => {
|
||||||
|
//#given - a running task with NO progress field
|
||||||
|
const client = {
|
||||||
|
session: {
|
||||||
|
prompt: async () => ({}),
|
||||||
|
promptAsync: async () => ({}),
|
||||||
|
abort: async () => ({}),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
const manager = new BackgroundManager({ client, directory: tmpdir() } as unknown as PluginInput)
|
||||||
|
|
||||||
|
const task: BackgroundTask = {
|
||||||
|
id: "task-init-1",
|
||||||
|
sessionID: "session-init-1",
|
||||||
|
parentSessionID: "parent-1",
|
||||||
|
parentMessageID: "msg-1",
|
||||||
|
description: "New task",
|
||||||
|
prompt: "Start thinking",
|
||||||
|
agent: "oracle",
|
||||||
|
status: "running",
|
||||||
|
startedAt: new Date(Date.now() - 60_000),
|
||||||
|
}
|
||||||
|
getTaskMap(manager).set(task.id, task)
|
||||||
|
|
||||||
|
//#when - a text-type event arrives before any tool call
|
||||||
|
manager.handleEvent({
|
||||||
|
type: "message.part.updated",
|
||||||
|
properties: { sessionID: "session-init-1", type: "text" },
|
||||||
|
})
|
||||||
|
|
||||||
|
//#then - progress should be initialized with toolCalls: 0 and fresh lastUpdate
|
||||||
|
expect(task.progress).toBeDefined()
|
||||||
|
expect(task.progress!.toolCalls).toBe(0)
|
||||||
|
expect(task.progress!.lastUpdate.getTime()).toBeGreaterThan(Date.now() - 5000)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("should NOT mark thinking model as stale when text events refresh lastUpdate", async () => {
|
||||||
|
//#given - a running task where text events keep lastUpdate fresh
|
||||||
|
const client = {
|
||||||
|
session: {
|
||||||
|
prompt: async () => ({}),
|
||||||
|
promptAsync: async () => ({}),
|
||||||
|
abort: async () => ({}),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
const manager = new BackgroundManager({ client, directory: tmpdir() } as unknown as PluginInput, { staleTimeoutMs: 180_000 })
|
||||||
|
stubNotifyParentSession(manager)
|
||||||
|
|
||||||
|
const task: BackgroundTask = {
|
||||||
|
id: "task-alive-1",
|
||||||
|
sessionID: "session-alive-1",
|
||||||
|
parentSessionID: "parent-1",
|
||||||
|
parentMessageID: "msg-1",
|
||||||
|
description: "Long thinking task",
|
||||||
|
prompt: "Deep reasoning",
|
||||||
|
agent: "oracle",
|
||||||
|
status: "running",
|
||||||
|
startedAt: new Date(Date.now() - 600_000),
|
||||||
|
progress: {
|
||||||
|
toolCalls: 0,
|
||||||
|
lastUpdate: new Date(Date.now() - 300_000),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
getTaskMap(manager).set(task.id, task)
|
||||||
|
|
||||||
|
//#when - a text event arrives, then stale check runs
|
||||||
|
manager.handleEvent({
|
||||||
|
type: "message.part.updated",
|
||||||
|
properties: { sessionID: "session-alive-1", type: "text" },
|
||||||
|
})
|
||||||
|
await manager["checkAndInterruptStaleTasks"]()
|
||||||
|
|
||||||
|
//#then - task should still be running (text event refreshed lastUpdate)
|
||||||
|
expect(task.status).toBe("running")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|||||||
@ -662,16 +662,17 @@ export class BackgroundManager {
|
|||||||
this.idleDeferralTimers.delete(task.id)
|
this.idleDeferralTimers.delete(task.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (partInfo?.type === "tool" || partInfo?.tool) {
|
if (!task.progress) {
|
||||||
if (!task.progress) {
|
task.progress = {
|
||||||
task.progress = {
|
toolCalls: 0,
|
||||||
toolCalls: 0,
|
lastUpdate: new Date(),
|
||||||
lastUpdate: new Date(),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
task.progress.lastUpdate = new Date()
|
||||||
|
|
||||||
|
if (partInfo?.type === "tool" || partInfo?.tool) {
|
||||||
task.progress.toolCalls += 1
|
task.progress.toolCalls += 1
|
||||||
task.progress.lastTool = partInfo.tool
|
task.progress.lastTool = partInfo.tool
|
||||||
task.progress.lastUpdate = new Date()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user