diff --git a/src/features/background-agent/constants.ts b/src/features/background-agent/constants.ts index 9df6c2ff2..9c20c0f61 100644 --- a/src/features/background-agent/constants.ts +++ b/src/features/background-agent/constants.ts @@ -4,8 +4,8 @@ import type { BackgroundTask, LaunchInput } from "./types" export const TASK_TTL_MS = 30 * 60 * 1000 export const TERMINAL_TASK_TTL_MS = 30 * 60 * 1000 export const MIN_STABILITY_TIME_MS = 10 * 1000 -export const DEFAULT_STALE_TIMEOUT_MS = 1_200_000 -export const DEFAULT_MESSAGE_STALENESS_TIMEOUT_MS = 1_800_000 +export const DEFAULT_STALE_TIMEOUT_MS = 2_700_000 +export const DEFAULT_MESSAGE_STALENESS_TIMEOUT_MS = 3_600_000 export const DEFAULT_MAX_TOOL_CALLS = 4000 export const DEFAULT_CIRCUIT_BREAKER_CONSECUTIVE_THRESHOLD = 20 export const DEFAULT_CIRCUIT_BREAKER_ENABLED = true diff --git a/src/features/background-agent/default-message-staleness-timeout.test.ts b/src/features/background-agent/default-message-staleness-timeout.test.ts index b13bf191a..d8b4e6671 100644 --- a/src/features/background-agent/default-message-staleness-timeout.test.ts +++ b/src/features/background-agent/default-message-staleness-timeout.test.ts @@ -21,9 +21,9 @@ function createRunningTask(startedAt: Date): BackgroundTask { } describe("DEFAULT_MESSAGE_STALENESS_TIMEOUT_MS", () => { - test("uses a 30 minute default", () => { + test("uses a 60 minute default", () => { // #given - const expectedTimeout = 30 * 60 * 1000 + const expectedTimeout = 60 * 60 * 1000 // #when const timeout = DEFAULT_MESSAGE_STALENESS_TIMEOUT_MS diff --git a/src/features/background-agent/default-stale-timeout.test.ts b/src/features/background-agent/default-stale-timeout.test.ts index 704a530c9..3f63ca28a 100644 --- a/src/features/background-agent/default-stale-timeout.test.ts +++ b/src/features/background-agent/default-stale-timeout.test.ts @@ -4,9 +4,9 @@ const { describe, expect, test } = require("bun:test") import { DEFAULT_STALE_TIMEOUT_MS } from "./constants" describe("DEFAULT_STALE_TIMEOUT_MS", () => { - test("uses a 20 minute default", () => { + test("uses a 45 minute default", () => { // #given - const expectedTimeout = 20 * 60 * 1000 + const expectedTimeout = 45 * 60 * 1000 // #when const timeout = DEFAULT_STALE_TIMEOUT_MS diff --git a/src/features/background-agent/manager.test.ts b/src/features/background-agent/manager.test.ts index bef56a647..4553cf1cb 100644 --- a/src/features/background-agent/manager.test.ts +++ b/src/features/background-agent/manager.test.ts @@ -1,5 +1,5 @@ declare const require: (name: string) => any -const { describe, test, expect, beforeEach, afterEach } = require("bun:test") +const { describe, test, expect, beforeEach, afterEach, spyOn } = require("bun:test") import { tmpdir } from "node:os" import type { PluginInput } from "@opencode-ai/plugin" import type { BackgroundTask, ResumeInput } from "./types" @@ -2781,6 +2781,18 @@ describe("BackgroundManager - Non-blocking Queue Integration", () => { }) describe("BackgroundManager.checkAndInterruptStaleTasks", () => { + const originalDateNow = Date.now + let fixedTime: number + + beforeEach(() => { + fixedTime = Date.now() + spyOn(globalThis.Date, "now").mockReturnValue(fixedTime) + }) + + afterEach(() => { + Date.now = originalDateNow + }) + test("should NOT interrupt task running less than 30 seconds (min runtime guard)", async () => { const client = { session: { @@ -3027,10 +3039,10 @@ describe("BackgroundManager.checkAndInterruptStaleTasks", () => { prompt: "Test", agent: "test-agent", status: "running", - startedAt: new Date(Date.now() - 25 * 60 * 1000), + startedAt: new Date(Date.now() - 50 * 60 * 1000), progress: { toolCalls: 1, - lastUpdate: new Date(Date.now() - 21 * 60 * 1000), + lastUpdate: new Date(Date.now() - 46 * 60 * 1000), }, } diff --git a/src/features/background-agent/task-poller.test.ts b/src/features/background-agent/task-poller.test.ts index 7895dfe09..ec3d09a74 100644 --- a/src/features/background-agent/task-poller.test.ts +++ b/src/features/background-agent/task-poller.test.ts @@ -1,5 +1,5 @@ declare const require: (name: string) => any -const { describe, it, expect, mock } = require("bun:test") +const { describe, it, expect, mock, spyOn, beforeEach, afterEach } = require("bun:test") import { checkAndInterruptStaleTasks, pruneStaleTasksAndNotifications } from "./task-poller" import type { BackgroundTask } from "./types" @@ -29,6 +29,18 @@ describe("checkAndInterruptStaleTasks", () => { ...overrides, } } + const originalDateNow = Date.now + let fixedTime: number + + beforeEach(() => { + fixedTime = Date.now() + spyOn(globalThis.Date, "now").mockReturnValue(fixedTime) + }) + + afterEach(() => { + Date.now = originalDateNow + }) + it("should interrupt tasks with lastUpdate exceeding stale timeout", async () => { //#given @@ -117,13 +129,13 @@ describe("checkAndInterruptStaleTasks", () => { }) it("should use DEFAULT_MESSAGE_STALENESS_TIMEOUT_MS when messageStalenessTimeoutMs is not configured", async () => { - //#given — task started 35 minutes ago, no config for messageStalenessTimeoutMs + //#given — task started 65 minutes ago, no config for messageStalenessTimeoutMs const task = createRunningTask({ - startedAt: new Date(Date.now() - 35 * 60 * 1000), + startedAt: new Date(Date.now() - 65 * 60 * 1000), progress: undefined, }) - //#when — default is 30 minutes (1_800_000ms) + //#when — default is 60 minutes (3_600_000ms) await checkAndInterruptStaleTasks({ tasks: [task], client: mockClient as never, diff --git a/src/features/background-agent/task-poller.ts b/src/features/background-agent/task-poller.ts index d23a241ae..84ba4c56e 100644 --- a/src/features/background-agent/task-poller.ts +++ b/src/features/background-agent/task-poller.ts @@ -130,7 +130,7 @@ export async function checkAndInterruptStaleTasks(args: { const staleMinutes = Math.round(runtime / 60000) task.status = "cancelled" - task.error = `Stale timeout (no activity for ${staleMinutes}min since start)` + task.error = `Stale timeout (no activity for ${staleMinutes}min since start). This is a FINAL cancellation - do NOT create a replacement task. If the timeout is too short, increase 'background_task.staleTimeoutMs' in .opencode/oh-my-opencode.json.` task.completedAt = new Date() if (task.concurrencyKey) { @@ -159,10 +159,10 @@ export async function checkAndInterruptStaleTasks(args: { if (timeSinceLastUpdate <= staleTimeoutMs) continue if (task.status !== "running") continue - const staleMinutes = Math.round(timeSinceLastUpdate / 60000) - task.status = "cancelled" - task.error = `Stale timeout (no activity for ${staleMinutes}min)` - task.completedAt = new Date() + const staleMinutes = Math.round(timeSinceLastUpdate / 60000) + task.status = "cancelled" + task.error = `Stale timeout (no activity for ${staleMinutes}min). This is a FINAL cancellation - do NOT create a replacement task. If the timeout is too short, increase 'background_task.staleTimeoutMs' in .opencode/oh-my-opencode.json.` + task.completedAt = new Date() if (task.concurrencyKey) { concurrencyManager.release(task.concurrencyKey)