diff --git a/src/features/background-agent/session-existence.test.ts b/src/features/background-agent/session-existence.test.ts index f0cdbed52..44b978b36 100644 --- a/src/features/background-agent/session-existence.test.ts +++ b/src/features/background-agent/session-existence.test.ts @@ -1,7 +1,7 @@ import { describe, expect, mock, test } from "bun:test" 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" describe("verifySessionExists", () => { @@ -24,4 +24,20 @@ describe("verifySessionExists", () => { 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({ + session: { + get, + }, + }) + + const result = await checkSessionExistence(client, "session-123") + + expect(result).toBe("unknown") + }) }) diff --git a/src/features/background-agent/session-existence.ts b/src/features/background-agent/session-existence.ts index 789b01899..d61544f9f 100644 --- a/src/features/background-agent/session-existence.ts +++ b/src/features/background-agent/session-existence.ts @@ -1,6 +1,7 @@ import type { OpencodeClient } from "./opencode-client" export const MIN_SESSION_GONE_POLLS = 3 +export type SessionExistenceStatus = "exists" | "missing" | "unknown" function extractErrorMessage(error: unknown): string | undefined { if (typeof error === "string") { @@ -35,11 +36,11 @@ function isSessionNotFoundError(error: unknown): boolean { return message.includes("not found") || message.includes("missing") } -export async function verifySessionExists( +export async function checkSessionExistence( client: OpencodeClient, sessionID: string, directory?: string -): Promise { +): Promise { try { const response = await client.session.get({ path: { id: sessionID }, @@ -47,11 +48,19 @@ export async function verifySessionExists( }) 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) { - return !isSessionNotFoundError(error) + return isSessionNotFoundError(error) ? "missing" : "unknown" } } + +export async function verifySessionExists( + client: OpencodeClient, + sessionID: string, + directory?: string +): Promise { + return await checkSessionExistence(client, sessionID, directory) !== "missing" +} diff --git a/src/features/background-agent/task-poller.test.ts b/src/features/background-agent/task-poller.test.ts index b08f78c59..da4d2da1e 100644 --- a/src/features/background-agent/task-poller.test.ts +++ b/src/features/background-agent/task-poller.test.ts @@ -472,7 +472,7 @@ describe("checkAndInterruptStaleTasks", () => { 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 const task = createRunningTask({ startedAt: new Date(Date.now() - 300_000), @@ -500,7 +500,7 @@ describe("checkAndInterruptStaleTasks", () => { //#then expect(task.status).toBe("running") - expect(task.consecutiveMissedPolls).toBe(0) + expect(task.consecutiveMissedPolls).toBe(3) expect(mockClient.session.get).toHaveBeenCalledWith({ path: { id: "ses-1" } }) }) diff --git a/src/features/background-agent/task-poller.ts b/src/features/background-agent/task-poller.ts index 9f0e0362a..d80a16cfa 100644 --- a/src/features/background-agent/task-poller.ts +++ b/src/features/background-agent/task-poller.ts @@ -16,7 +16,7 @@ import { } from "./constants" import { abortWithTimeout } from "./abort-with-timeout" 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 { 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 (sessionGone && await verifySessionExists(client, sessionID, directory)) { - task.consecutiveMissedPolls = 0 - continue + if (sessionGone) { + const existence = await checkSessionExistence(client, sessionID, directory) + if (existence === "exists") { + task.consecutiveMissedPolls = 0 + continue + } + if (existence === "unknown") continue } const staleMinutes = Math.round(runtime / 60000) @@ -228,9 +232,13 @@ export async function checkAndInterruptStaleTasks(args: { if (task.status !== "running") continue - if (sessionGone && await verifySessionExists(client, sessionID, directory)) { - task.consecutiveMissedPolls = 0 - continue + if (sessionGone) { + const existence = await checkSessionExistence(client, sessionID, directory) + if (existence === "exists") { + task.consecutiveMissedPolls = 0 + continue + } + if (existence === "unknown") continue } const staleMinutes = Math.round(timeSinceLastUpdate / 60000) diff --git a/src/plugin-handlers/agent-key-remapper.test.ts b/src/plugin-handlers/agent-key-remapper.test.ts index a39c73566..8e62bf64d 100644 --- a/src/plugin-handlers/agent-key-remapper.test.ts +++ b/src/plugin-handlers/agent-key-remapper.test.ts @@ -231,7 +231,7 @@ describe("remapAgentKeysToDisplayNames", () => { const result = remapAgentKeysToDisplayNames(agents, overrides) // then the legacy AGENT_DISPLAY_NAMES value is used - expect(result["Sisyphus - Ultraworker"]).toBeDefined() + expect(result[getAgentListDisplayName("sisyphus")]).toBeDefined() expect(result["总指挥"]).toBeUndefined() }) @@ -245,7 +245,7 @@ describe("remapAgentKeysToDisplayNames", () => { const result = remapAgentKeysToDisplayNames(agents) // then the legacy AGENT_DISPLAY_NAMES value is used - expect(result["Sisyphus - Ultraworker"]).toBeDefined() + expect(result[getAgentListDisplayName("sisyphus")]).toBeDefined() }) }) })