fix: prevent background agent race condition in session prompt (#2932)

Added await for session ready state before sending prompt in
background-agent/manager.ts. Also improved image resizer error handling.

132 tests pass, tsc clean.

Closes #2932
This commit is contained in:
YeonGyu-Kim
2026-04-07 15:24:02 +09:00
parent be562905d6
commit 5622d154fd
9 changed files with 777 additions and 11 deletions
@@ -1431,6 +1431,46 @@ describe("BackgroundManager.tryCompleteTask", () => {
expect(task.concurrencyKey).toBeUndefined()
})
test("should mark task as error when startTask throws after session creation", async () => {
//#given - startTask creates session but fails before sending prompt
const concurrencyKey = "anthropic/claude-opus-4-6"
const task = createMockTask({
id: "task-zombie-session",
parentSessionID: "parent-zombie",
status: "pending",
agent: "explore",
})
delete (task as Partial<BackgroundTask>).sessionID
const input = {
description: task.description,
prompt: task.prompt,
agent: task.agent,
parentSessionID: task.parentSessionID,
parentMessageID: task.parentMessageID,
model: { providerID: "anthropic", modelID: "claude-opus-4-6" },
}
getTaskMap(manager).set(task.id, task)
getQueuesByKey(manager).set(concurrencyKey, [{ task, input }])
;(manager as unknown as { startTask: (item: { task: BackgroundTask; input: typeof input }) => Promise<void> }).startTask = async (item) => {
item.task.status = "running"
item.task.sessionID = "ses_zombie_child"
item.task.startedAt = new Date()
item.task.concurrencyKey = concurrencyKey
throw new Error("crash between session creation and prompt send")
}
//#when
await processKeyForTest(manager, concurrencyKey)
//#then - task must be marked as error, not left in running zombie state
expect(task.status).toBe("error")
expect(task.error).toContain("crash between session creation and prompt send")
expect(task.completedAt).toBeDefined()
})
test("should release queue slot when queued task is already interrupt", async () => {
// given
const concurrencyKey = "anthropic/claude-opus-4-6"
+24 -1
View File
@@ -371,7 +371,7 @@ export class BackgroundManager {
this.markPreStartDescendantReservation(task)
// Trigger processing (fire-and-forget)
this.processKey(key)
void this.processKey(key)
return { ...task }
} catch (error) {
@@ -408,12 +408,35 @@ export class BackgroundManager {
} catch (error) {
log("[background-agent] Error starting task:", error)
this.rollbackPreStartDescendantReservation(item.task)
// Mark task as error so the parent polling loop detects the failure
// instead of leaving it in a zombie "running" state with no prompt sent
item.task.status = "error"
item.task.error = error instanceof Error ? error.message : String(error)
item.task.completedAt = new Date()
if (item.task.concurrencyKey) {
this.concurrencyManager.release(item.task.concurrencyKey)
item.task.concurrencyKey = undefined
} else {
this.concurrencyManager.release(key)
}
if (item.task.rootSessionID) {
this.unregisterRootDescendant(item.task.rootSessionID)
}
removeTaskToastTracking(item.task.id)
// Abort the orphaned session if one was created before the error
if (item.task.sessionID) {
await this.abortSessionWithLogging(item.task.sessionID, "startTask error cleanup")
}
this.markForNotification(item.task)
this.enqueueNotificationForParent(item.task.parentSessionID, () => this.notifyParentSession(item.task)).catch(err => {
log("[background-agent] Failed to notify on startTask error:", err)
})
}
}
} finally {