refactor(background-agent): optimize lifecycle and simplify tools (#1411)
* refactor(background-agent): optimize cache timer lifecycle and result handling Ultraworked with Sisyphus Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai> * refactor(background-task): simplify tool implementation and expand test coverage Ultraworked with Sisyphus Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai> * fix(background-task): fix BackgroundCancel tool parameter handling Correct parameter names and types in BackgroundCancel tool to match actual usage patterns. Add comprehensive test coverage for parameter validation. --------- Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
@@ -184,6 +184,10 @@ function getTaskMap(manager: BackgroundManager): Map<string, BackgroundTask> {
|
||||
return (manager as unknown as { tasks: Map<string, BackgroundTask> }).tasks
|
||||
}
|
||||
|
||||
function getPendingByParent(manager: BackgroundManager): Map<string, Set<string>> {
|
||||
return (manager as unknown as { pendingByParent: Map<string, Set<string>> }).pendingByParent
|
||||
}
|
||||
|
||||
async function tryCompleteTaskForTest(manager: BackgroundManager, task: BackgroundTask): Promise<boolean> {
|
||||
return (manager as unknown as { tryCompleteTask: (task: BackgroundTask, source: string) => Promise<boolean> })
|
||||
.tryCompleteTask(task, "test")
|
||||
@@ -1454,6 +1458,44 @@ describe("BackgroundManager - Non-blocking Queue Integration", () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe("cancelTask", () => {
|
||||
test("should cancel running task and release concurrency", async () => {
|
||||
// given
|
||||
const manager = createBackgroundManager()
|
||||
stubNotifyParentSession(manager)
|
||||
|
||||
const concurrencyManager = getConcurrencyManager(manager)
|
||||
const concurrencyKey = "test-provider/test-model"
|
||||
await concurrencyManager.acquire(concurrencyKey)
|
||||
|
||||
const task = createMockTask({
|
||||
id: "task-cancel-running",
|
||||
sessionID: "session-cancel-running",
|
||||
parentSessionID: "parent-cancel",
|
||||
status: "running",
|
||||
concurrencyKey,
|
||||
})
|
||||
|
||||
getTaskMap(manager).set(task.id, task)
|
||||
const pendingByParent = getPendingByParent(manager)
|
||||
pendingByParent.set(task.parentSessionID, new Set([task.id]))
|
||||
|
||||
// when
|
||||
const cancelled = await manager.cancelTask(task.id, { source: "test" })
|
||||
|
||||
// then
|
||||
const updatedTask = manager.getTask(task.id)
|
||||
expect(cancelled).toBe(true)
|
||||
expect(updatedTask?.status).toBe("cancelled")
|
||||
expect(updatedTask?.completedAt).toBeInstanceOf(Date)
|
||||
expect(updatedTask?.concurrencyKey).toBeUndefined()
|
||||
expect(concurrencyManager.getCount(concurrencyKey)).toBe(0)
|
||||
|
||||
const pendingSet = pendingByParent.get(task.parentSessionID)
|
||||
expect(pendingSet?.has(task.id) ?? false).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe("multiple keys process in parallel", () => {
|
||||
test("should process different concurrency keys in parallel", async () => {
|
||||
// given
|
||||
|
||||
@@ -830,6 +830,72 @@ export class BackgroundManager {
|
||||
}
|
||||
}
|
||||
|
||||
async cancelTask(
|
||||
taskId: string,
|
||||
options?: { source?: string; reason?: string; abortSession?: boolean }
|
||||
): Promise<boolean> {
|
||||
const task = this.tasks.get(taskId)
|
||||
if (!task || (task.status !== "running" && task.status !== "pending")) {
|
||||
return false
|
||||
}
|
||||
|
||||
const source = options?.source ?? "cancel"
|
||||
const abortSession = options?.abortSession !== false
|
||||
const reason = options?.reason
|
||||
|
||||
if (task.status === "pending") {
|
||||
const key = task.model
|
||||
? `${task.model.providerID}/${task.model.modelID}`
|
||||
: task.agent
|
||||
const queue = this.queuesByKey.get(key)
|
||||
if (queue) {
|
||||
const index = queue.findIndex(item => item.task.id === taskId)
|
||||
if (index !== -1) {
|
||||
queue.splice(index, 1)
|
||||
if (queue.length === 0) {
|
||||
this.queuesByKey.delete(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
log("[background-agent] Cancelled pending task:", { taskId, key })
|
||||
}
|
||||
|
||||
task.status = "cancelled"
|
||||
task.completedAt = new Date()
|
||||
if (reason) {
|
||||
task.error = reason
|
||||
}
|
||||
|
||||
if (task.concurrencyKey) {
|
||||
this.concurrencyManager.release(task.concurrencyKey)
|
||||
task.concurrencyKey = undefined
|
||||
}
|
||||
|
||||
const existingTimer = this.completionTimers.get(task.id)
|
||||
if (existingTimer) {
|
||||
clearTimeout(existingTimer)
|
||||
this.completionTimers.delete(task.id)
|
||||
}
|
||||
|
||||
this.cleanupPendingByParent(task)
|
||||
this.markForNotification(task)
|
||||
|
||||
if (abortSession && task.sessionID) {
|
||||
this.client.session.abort({
|
||||
path: { id: task.sessionID },
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
try {
|
||||
await this.notifyParentSession(task)
|
||||
log(`[background-agent] Task cancelled via ${source}:`, task.id)
|
||||
} catch (err) {
|
||||
log("[background-agent] Error in notifyParentSession for cancelled task:", { taskId: task.id, error: err })
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels a pending task by removing it from queue and marking as cancelled.
|
||||
* Does NOT abort session (no session exists yet) or release concurrency slot (wasn't acquired).
|
||||
@@ -840,29 +906,7 @@ export class BackgroundManager {
|
||||
return false
|
||||
}
|
||||
|
||||
// Find and remove from queue
|
||||
const key = task.model
|
||||
? `${task.model.providerID}/${task.model.modelID}`
|
||||
: task.agent
|
||||
const queue = this.queuesByKey.get(key)
|
||||
if (queue) {
|
||||
const index = queue.findIndex(item => item.task.id === taskId)
|
||||
if (index !== -1) {
|
||||
queue.splice(index, 1)
|
||||
if (queue.length === 0) {
|
||||
this.queuesByKey.delete(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Mark as cancelled
|
||||
task.status = "cancelled"
|
||||
task.completedAt = new Date()
|
||||
|
||||
// Clean up pendingByParent
|
||||
this.cleanupPendingByParent(task)
|
||||
|
||||
log("[background-agent] Cancelled pending task:", { taskId, key })
|
||||
void this.cancelTask(taskId, { source: "cancelPendingTask", abortSession: false })
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user