fix(background-agent): bound session abort waits

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
YeonGyu-Kim
2026-04-04 01:19:52 +09:00
parent 938c920092
commit f5740d68c7
5 changed files with 45 additions and 11 deletions
@@ -0,0 +1,35 @@
import { log } from "../../shared"
import type { OpencodeClient } from "./opencode-client"
export async function abortWithTimeout(
client: OpencodeClient,
sessionID: string,
timeoutMs = 10_000,
): Promise<boolean> {
let timeoutHandle: ReturnType<typeof setTimeout> | undefined
try {
const result = await Promise.race([
client.session.abort({ path: { id: sessionID } }).then(() => "aborted" as const),
new Promise<"timed_out">((resolve) => {
timeoutHandle = setTimeout(() => {
resolve("timed_out")
}, timeoutMs)
}),
])
if (result === "timed_out") {
log("[background-agent] Session abort timed out; continuing cleanup:", {
sessionID,
timeoutMs,
})
return false
}
return true
} finally {
if (timeoutHandle) {
clearTimeout(timeoutHandle)
}
}
}
@@ -10,6 +10,7 @@ import {
selectFallbackProvider, selectFallbackProvider,
} from "../../shared/model-error-classifier" } from "../../shared/model-error-classifier"
import { transformModelForProvider } from "../../shared/provider-model-id-transform" import { transformModelForProvider } from "../../shared/provider-model-id-transform"
import { abortWithTimeout } from "./abort-with-timeout"
export async function tryFallbackRetry(args: { export async function tryFallbackRetry(args: {
task: BackgroundTask task: BackgroundTask
@@ -123,7 +124,7 @@ export async function tryFallbackRetry(args: {
} }
if (previousSessionID) { if (previousSessionID) {
await client.session.abort({ path: { id: previousSessionID } }).catch(() => {}) await abortWithTimeout(client, previousSessionID).catch(() => {})
} }
queue.push({ task, input: retryInput }) queue.push({ task, input: retryInput })
@@ -238,7 +238,7 @@ function stubNotifyParentSession(manager: BackgroundManager): void {
} }
async function flushBackgroundNotifications(): Promise<void> { async function flushBackgroundNotifications(): Promise<void> {
for (let i = 0; i < 6; i++) { for (let i = 0; i < 12; i++) {
await Promise.resolve() await Promise.resolve()
} }
} }
@@ -2570,7 +2570,7 @@ describe("BackgroundManager - Non-blocking Queue Integration", () => {
abortCalled, abortCalled,
new Promise<never>((_, reject) => setTimeout(() => reject(new Error("timeout")), 100)), new Promise<never>((_, reject) => setTimeout(() => reject(new Error("timeout")), 100)),
]) ])
await Promise.resolve() await flushBackgroundNotifications()
// then // then
const updatedTask = manager.getTask(task.id) const updatedTask = manager.getTask(task.id)
+3 -6
View File
@@ -57,6 +57,7 @@ import { join } from "node:path"
import { pruneStaleTasksAndNotifications } from "./task-poller" import { pruneStaleTasksAndNotifications } from "./task-poller"
import { checkAndInterruptStaleTasks } from "./task-poller" import { checkAndInterruptStaleTasks } from "./task-poller"
import { removeTaskToastTracking } from "./remove-task-toast-tracking" import { removeTaskToastTracking } from "./remove-task-toast-tracking"
import { abortWithTimeout } from "./abort-with-timeout"
import { import {
MIN_SESSION_GONE_POLLS, MIN_SESSION_GONE_POLLS,
verifySessionExists as verifySessionStillExists, verifySessionExists as verifySessionStillExists,
@@ -193,9 +194,7 @@ export class BackgroundManager {
private async abortSessionWithLogging(sessionID: string, reason: string): Promise<void> { private async abortSessionWithLogging(sessionID: string, reason: string): Promise<void> {
try { try {
await this.client.session.abort({ await abortWithTimeout(this.client, sessionID)
path: { id: sessionID },
})
} catch (error) { } catch (error) {
log(`[background-agent] Failed to abort session during ${reason}:`, { log(`[background-agent] Failed to abort session during ${reason}:`, {
sessionID, sessionID,
@@ -1985,9 +1984,7 @@ export class BackgroundManager {
if (task.status === "running" && task.sessionID) { if (task.status === "running" && task.sessionID) {
abortRequests.push({ abortRequests.push({
sessionID: task.sessionID, sessionID: task.sessionID,
promise: this.client.session.abort({ promise: abortWithTimeout(this.client, task.sessionID),
path: { id: task.sessionID },
}),
}) })
} }
} }
+3 -2
View File
@@ -13,6 +13,7 @@ import {
TERMINAL_TASK_TTL_MS, TERMINAL_TASK_TTL_MS,
TASK_TTL_MS, TASK_TTL_MS,
} from "./constants" } from "./constants"
import { abortWithTimeout } from "./abort-with-timeout"
import { removeTaskToastTracking } from "./remove-task-toast-tracking" import { removeTaskToastTracking } from "./remove-task-toast-tracking"
import { MIN_SESSION_GONE_POLLS, verifySessionExists } from "./session-existence" import { MIN_SESSION_GONE_POLLS, verifySessionExists } from "./session-existence"
@@ -167,7 +168,7 @@ export async function checkAndInterruptStaleTasks(args: {
onTaskInterrupted(task) onTaskInterrupted(task)
abortPromises.push(client.session.abort({ path: { id: sessionID } })) abortPromises.push(abortWithTimeout(client, sessionID))
log(`[background-agent] Task ${task.id} interrupted: no progress since start`) log(`[background-agent] Task ${task.id} interrupted: no progress since start`)
try { try {
@@ -205,7 +206,7 @@ export async function checkAndInterruptStaleTasks(args: {
onTaskInterrupted(task) onTaskInterrupted(task)
abortPromises.push(client.session.abort({ path: { id: sessionID } })) abortPromises.push(abortWithTimeout(client, sessionID))
log(`[background-agent] Task ${task.id} interrupted: stale timeout`) log(`[background-agent] Task ${task.id} interrupted: stale timeout`)
try { try {