fix(background-agent): preserve missed polls on lookup errors

This commit is contained in:
YeonGyu-Kim
2026-05-21 13:13:14 +09:00
parent 98d475f95e
commit 0feb125079
4 changed files with 48 additions and 15 deletions
@@ -1,7 +1,7 @@
import { describe, expect, mock, test } from "bun:test" import { describe, expect, mock, test } from "bun:test"
import type { OpencodeClient } from "./opencode-client" import type { OpencodeClient } from "./opencode-client"
import { verifySessionExists } from "./session-existence" import { checkSessionExistence, verifySessionExists } from "./session-existence"
import { unsafeTestValue } from "../../../test-support/unsafe-test-value" import { unsafeTestValue } from "../../../test-support/unsafe-test-value"
describe("verifySessionExists", () => { describe("verifySessionExists", () => {
@@ -24,4 +24,20 @@ describe("verifySessionExists", () => {
query: { directory: "/project/root" }, query: { directory: "/project/root" },
}) })
}) })
test("classifies transient lookup errors as unknown", async () => {
const get = mock(async () => ({
error: { message: "Network timeout", status: 500 },
data: undefined,
}))
const client = unsafeTestValue<OpencodeClient>({
session: {
get,
},
})
const result = await checkSessionExistence(client, "session-123")
expect(result).toBe("unknown")
})
}) })
@@ -1,6 +1,7 @@
import type { OpencodeClient } from "./opencode-client" import type { OpencodeClient } from "./opencode-client"
export const MIN_SESSION_GONE_POLLS = 3 export const MIN_SESSION_GONE_POLLS = 3
export type SessionExistenceStatus = "exists" | "missing" | "unknown"
function extractErrorMessage(error: unknown): string | undefined { function extractErrorMessage(error: unknown): string | undefined {
if (typeof error === "string") { if (typeof error === "string") {
@@ -35,11 +36,11 @@ function isSessionNotFoundError(error: unknown): boolean {
return message.includes("not found") || message.includes("missing") return message.includes("not found") || message.includes("missing")
} }
export async function verifySessionExists( export async function checkSessionExistence(
client: OpencodeClient, client: OpencodeClient,
sessionID: string, sessionID: string,
directory?: string directory?: string
): Promise<boolean> { ): Promise<SessionExistenceStatus> {
try { try {
const response = await client.session.get({ const response = await client.session.get({
path: { id: sessionID }, path: { id: sessionID },
@@ -47,11 +48,19 @@ export async function verifySessionExists(
}) })
if (response.error !== undefined && response.error !== null) { if (response.error !== undefined && response.error !== null) {
return !isSessionNotFoundError(response.error) return isSessionNotFoundError(response.error) ? "missing" : "unknown"
} }
return response.data != null return response.data != null ? "exists" : "missing"
} catch (error) { } catch (error) {
return !isSessionNotFoundError(error) return isSessionNotFoundError(error) ? "missing" : "unknown"
} }
} }
export async function verifySessionExists(
client: OpencodeClient,
sessionID: string,
directory?: string
): Promise<boolean> {
return await checkSessionExistence(client, sessionID, directory) !== "missing"
}
@@ -472,7 +472,7 @@ describe("checkAndInterruptStaleTasks", () => {
expect(mockClient.session.get).toHaveBeenCalledWith({ path: { id: "ses-1" } }) expect(mockClient.session.get).toHaveBeenCalledWith({ path: { id: "ses-1" } })
}) })
it("should NOT cancel task when session.get returns a transient error response", async () => { it("should NOT cancel or reset missed polls when session.get returns a transient error response", async () => {
//#given - repeated missing polls but lookup failed with a retryable transport error //#given - repeated missing polls but lookup failed with a retryable transport error
const task = createRunningTask({ const task = createRunningTask({
startedAt: new Date(Date.now() - 300_000), startedAt: new Date(Date.now() - 300_000),
@@ -500,7 +500,7 @@ describe("checkAndInterruptStaleTasks", () => {
//#then //#then
expect(task.status).toBe("running") expect(task.status).toBe("running")
expect(task.consecutiveMissedPolls).toBe(0) expect(task.consecutiveMissedPolls).toBe(3)
expect(mockClient.session.get).toHaveBeenCalledWith({ path: { id: "ses-1" } }) expect(mockClient.session.get).toHaveBeenCalledWith({ path: { id: "ses-1" } })
}) })
+15 -7
View File
@@ -16,7 +16,7 @@ import {
} from "./constants" } from "./constants"
import { abortWithTimeout } from "./abort-with-timeout" 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 { checkSessionExistence, MIN_SESSION_GONE_POLLS } from "./session-existence"
import { isActiveSessionStatus } from "./session-status-classifier" import { isActiveSessionStatus } from "./session-status-classifier"
import { getSessionActivityFromClient, type SessionActivityResolver } from "./session-activity" import { getSessionActivityFromClient, type SessionActivityResolver } from "./session-activity"
@@ -178,9 +178,13 @@ export async function checkAndInterruptStaleTasks(args: {
if (activityRefresh.type === "activity" && now - activityRefresh.activityTime <= effectiveTimeout) continue if (activityRefresh.type === "activity" && now - activityRefresh.activityTime <= effectiveTimeout) continue
} }
if (sessionGone && await verifySessionExists(client, sessionID, directory)) { if (sessionGone) {
task.consecutiveMissedPolls = 0 const existence = await checkSessionExistence(client, sessionID, directory)
continue if (existence === "exists") {
task.consecutiveMissedPolls = 0
continue
}
if (existence === "unknown") continue
} }
const staleMinutes = Math.round(runtime / 60000) const staleMinutes = Math.round(runtime / 60000)
@@ -228,9 +232,13 @@ export async function checkAndInterruptStaleTasks(args: {
if (task.status !== "running") continue if (task.status !== "running") continue
if (sessionGone && await verifySessionExists(client, sessionID, directory)) { if (sessionGone) {
task.consecutiveMissedPolls = 0 const existence = await checkSessionExistence(client, sessionID, directory)
continue if (existence === "exists") {
task.consecutiveMissedPolls = 0
continue
}
if (existence === "unknown") continue
} }
const staleMinutes = Math.round(timeSinceLastUpdate / 60000) const staleMinutes = Math.round(timeSinceLastUpdate / 60000)