fix(background-agent): correct stall timeout guidance
This commit is contained in:
committed by
YeonGyu-Kim
parent
189af23e96
commit
b6caa5d3e0
@@ -177,6 +177,7 @@ describe("checkAndInterruptStaleTasks", () => {
|
||||
//#then
|
||||
expect(task.status).toBe("cancelled")
|
||||
expect(task.error).toContain("no activity")
|
||||
expect(task.error).toContain("messageStalenessTimeoutMs")
|
||||
})
|
||||
|
||||
it("should await abort before resolving for no-progress stale interruption", async () => {
|
||||
@@ -403,7 +404,7 @@ describe("checkAndInterruptStaleTasks", () => {
|
||||
progress: undefined,
|
||||
})
|
||||
|
||||
//#when — session is running
|
||||
//#when - session is running
|
||||
await checkAndInterruptStaleTasks({
|
||||
tasks: [task],
|
||||
client: mockClient as never,
|
||||
@@ -419,7 +420,7 @@ describe("checkAndInterruptStaleTasks", () => {
|
||||
})
|
||||
|
||||
it("should NOT cancel healthy task on first missing status poll", async () => {
|
||||
//#given — one missing poll should not be enough to declare the session gone
|
||||
//#given - one missing poll should not be enough to declare the session gone
|
||||
const task = createRunningTask({
|
||||
startedAt: new Date(Date.now() - 300_000),
|
||||
progress: {
|
||||
@@ -445,7 +446,7 @@ describe("checkAndInterruptStaleTasks", () => {
|
||||
})
|
||||
|
||||
it("should NOT cancel task when session.get confirms the session still exists", async () => {
|
||||
//#given — repeated missing polls but direct lookup still succeeds
|
||||
//#given - repeated missing polls but direct lookup still succeeds
|
||||
const task = createRunningTask({
|
||||
startedAt: new Date(Date.now() - 300_000),
|
||||
progress: {
|
||||
@@ -472,7 +473,7 @@ describe("checkAndInterruptStaleTasks", () => {
|
||||
})
|
||||
|
||||
it("should NOT cancel task 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({
|
||||
startedAt: new Date(Date.now() - 300_000),
|
||||
progress: {
|
||||
@@ -504,7 +505,7 @@ describe("checkAndInterruptStaleTasks", () => {
|
||||
})
|
||||
|
||||
it("should use session-gone timeout when session is missing from status map (with progress)", async () => {
|
||||
//#given — lastUpdate 2min ago, session completely gone from status
|
||||
//#given - lastUpdate 2min ago, session completely gone from status
|
||||
const task = createRunningTask({
|
||||
startedAt: new Date(Date.now() - 300_000),
|
||||
progress: {
|
||||
@@ -516,7 +517,7 @@ describe("checkAndInterruptStaleTasks", () => {
|
||||
|
||||
mockClient.session.get.mockRejectedValue(new Error("missing"))
|
||||
|
||||
//#when — empty sessionStatuses (session gone), sessionGoneTimeoutMs = 60s
|
||||
//#when - empty sessionStatuses (session gone), sessionGoneTimeoutMs = 60s
|
||||
await checkAndInterruptStaleTasks({
|
||||
tasks: [task],
|
||||
client: mockClient as never,
|
||||
@@ -526,7 +527,7 @@ describe("checkAndInterruptStaleTasks", () => {
|
||||
sessionStatuses: {},
|
||||
})
|
||||
|
||||
//#then — cancelled because session gone timeout (60s) < timeSinceLastUpdate (120s)
|
||||
//#then - cancelled because session gone timeout (60s) < timeSinceLastUpdate (120s)
|
||||
expect(task.status).toBe("cancelled")
|
||||
expect(task.error).toContain("session gone from status registry")
|
||||
})
|
||||
@@ -571,7 +572,7 @@ describe("checkAndInterruptStaleTasks", () => {
|
||||
})
|
||||
|
||||
it("should use session-gone timeout when session is missing from status map (no progress)", async () => {
|
||||
//#given — task started 2min ago, no progress, session completely gone
|
||||
//#given - task started 2min ago, no progress, session completely gone
|
||||
const task = createRunningTask({
|
||||
startedAt: new Date(Date.now() - 120_000),
|
||||
progress: undefined,
|
||||
@@ -580,7 +581,7 @@ describe("checkAndInterruptStaleTasks", () => {
|
||||
|
||||
mockClient.session.get.mockRejectedValue(new Error("missing"))
|
||||
|
||||
//#when — session gone, sessionGoneTimeoutMs = 60s
|
||||
//#when - session gone, sessionGoneTimeoutMs = 60s
|
||||
await checkAndInterruptStaleTasks({
|
||||
tasks: [task],
|
||||
client: mockClient as never,
|
||||
@@ -590,13 +591,13 @@ describe("checkAndInterruptStaleTasks", () => {
|
||||
sessionStatuses: {},
|
||||
})
|
||||
|
||||
//#then — cancelled because session gone timeout (60s) < runtime (120s)
|
||||
//#then - cancelled because session gone timeout (60s) < runtime (120s)
|
||||
expect(task.status).toBe("cancelled")
|
||||
expect(task.error).toContain("session gone from status registry")
|
||||
})
|
||||
|
||||
it("should NOT use session-gone timeout when session is idle (present in status map)", async () => {
|
||||
//#given — lastUpdate 2min ago, session is idle (present in status but not active)
|
||||
//#given - lastUpdate 2min ago, session is idle (present in status but not active)
|
||||
const task = createRunningTask({
|
||||
startedAt: new Date(Date.now() - 300_000),
|
||||
progress: {
|
||||
@@ -608,7 +609,7 @@ describe("checkAndInterruptStaleTasks", () => {
|
||||
|
||||
mockClient.session.get.mockRejectedValue(new Error("missing"))
|
||||
|
||||
//#when — session is idle (present in map), staleTimeoutMs = 180s
|
||||
//#when - session is idle (present in map), staleTimeoutMs = 180s
|
||||
await checkAndInterruptStaleTasks({
|
||||
tasks: [task],
|
||||
client: mockClient as never,
|
||||
@@ -618,12 +619,12 @@ describe("checkAndInterruptStaleTasks", () => {
|
||||
sessionStatuses: { "ses-1": { type: "idle" } },
|
||||
})
|
||||
|
||||
//#then — still running because normal staleTimeout (180s) > timeSinceLastUpdate (120s)
|
||||
//#then - still running because normal staleTimeout (180s) > timeSinceLastUpdate (120s)
|
||||
expect(task.status).toBe("running")
|
||||
})
|
||||
|
||||
it("should use default session-gone timeout when not configured", async () => {
|
||||
//#given — lastUpdate 2min ago, session gone, no sessionGoneTimeoutMs config
|
||||
//#given - lastUpdate 2min ago, session gone, no sessionGoneTimeoutMs config
|
||||
const task = createRunningTask({
|
||||
startedAt: new Date(Date.now() - 300_000),
|
||||
progress: {
|
||||
@@ -635,7 +636,7 @@ describe("checkAndInterruptStaleTasks", () => {
|
||||
|
||||
mockClient.session.get.mockRejectedValue(new Error("missing"))
|
||||
|
||||
//#when — no config (default sessionGoneTimeoutMs = 60_000)
|
||||
//#when - no config (default sessionGoneTimeoutMs = 60_000)
|
||||
await checkAndInterruptStaleTasks({
|
||||
tasks: [task],
|
||||
client: mockClient as never,
|
||||
@@ -645,13 +646,13 @@ describe("checkAndInterruptStaleTasks", () => {
|
||||
sessionStatuses: {},
|
||||
})
|
||||
|
||||
//#then — cancelled because default session gone timeout (60s) < timeSinceLastUpdate (120s)
|
||||
//#then - cancelled because default session gone timeout (60s) < timeSinceLastUpdate (120s)
|
||||
expect(task.status).toBe("cancelled")
|
||||
expect(task.error).toContain("session gone from status registry")
|
||||
})
|
||||
|
||||
it("should interrupt task when busy session exceeds stale timeout", async () => {
|
||||
//#given — lastUpdate is 5min old and session is still "busy"
|
||||
//#given - lastUpdate is 5min old and session is still "busy"
|
||||
const task = createRunningTask({
|
||||
startedAt: new Date(Date.now() - 300_000),
|
||||
progress: {
|
||||
@@ -660,7 +661,7 @@ describe("checkAndInterruptStaleTasks", () => {
|
||||
},
|
||||
})
|
||||
|
||||
//#when — session status is "busy" (not "running" — OpenCode uses "busy" for active LLM processing)
|
||||
//#when - session status is "busy" (not "running" - OpenCode uses "busy" for active LLM processing)
|
||||
await checkAndInterruptStaleTasks({
|
||||
tasks: [task],
|
||||
client: mockClient as never,
|
||||
@@ -676,7 +677,7 @@ describe("checkAndInterruptStaleTasks", () => {
|
||||
})
|
||||
|
||||
it("should interrupt task when retry session exceeds stale timeout", async () => {
|
||||
//#given — lastUpdate is 5min old but session is retrying
|
||||
//#given - lastUpdate is 5min old but session is retrying
|
||||
const task = createRunningTask({
|
||||
startedAt: new Date(Date.now() - 300_000),
|
||||
progress: {
|
||||
@@ -685,7 +686,7 @@ describe("checkAndInterruptStaleTasks", () => {
|
||||
},
|
||||
})
|
||||
|
||||
//#when — session status is "retry" (OpenCode retries on transient API errors)
|
||||
//#when - session status is "retry" (OpenCode retries on transient API errors)
|
||||
await checkAndInterruptStaleTasks({
|
||||
tasks: [task],
|
||||
client: mockClient as never,
|
||||
@@ -701,13 +702,13 @@ describe("checkAndInterruptStaleTasks", () => {
|
||||
})
|
||||
|
||||
it("should interrupt busy session with no progress after message staleness timeout", async () => {
|
||||
//#given — no progress at all, session is still "busy"
|
||||
//#given - no progress at all, session is still "busy"
|
||||
const task = createRunningTask({
|
||||
startedAt: new Date(Date.now() - 15 * 60 * 1000),
|
||||
progress: undefined,
|
||||
})
|
||||
|
||||
//#when — session is busy
|
||||
//#when - session is busy
|
||||
await checkAndInterruptStaleTasks({
|
||||
tasks: [task],
|
||||
client: mockClient as never,
|
||||
@@ -771,7 +772,7 @@ describe("checkAndInterruptStaleTasks", () => {
|
||||
})
|
||||
|
||||
it('should NOT protect task when session has terminal non-idle status like "interrupted"', async () => {
|
||||
//#given — lastUpdate is 5min old, session is "interrupted" (terminal, not active)
|
||||
//#given - lastUpdate is 5min old, session is "interrupted" (terminal, not active)
|
||||
const task = createRunningTask({
|
||||
startedAt: new Date(Date.now() - 300_000),
|
||||
progress: {
|
||||
@@ -780,7 +781,7 @@ describe("checkAndInterruptStaleTasks", () => {
|
||||
},
|
||||
})
|
||||
|
||||
//#when — session status is "interrupted" (terminal)
|
||||
//#when - session status is "interrupted" (terminal)
|
||||
await checkAndInterruptStaleTasks({
|
||||
tasks: [task],
|
||||
client: mockClient as never,
|
||||
@@ -790,13 +791,13 @@ describe("checkAndInterruptStaleTasks", () => {
|
||||
sessionStatuses: { "ses-1": { type: "interrupted" } },
|
||||
})
|
||||
|
||||
//#then — terminal statuses should not protect from stale timeout
|
||||
//#then - terminal statuses should not protect from stale timeout
|
||||
expect(task.status).toBe("cancelled")
|
||||
expect(task.error).toContain("Stale timeout")
|
||||
})
|
||||
|
||||
it('should NOT protect task when session has unknown status type', async () => {
|
||||
//#given — lastUpdate is 5min old, session has an unknown status
|
||||
//#given - lastUpdate is 5min old, session has an unknown status
|
||||
const task = createRunningTask({
|
||||
startedAt: new Date(Date.now() - 300_000),
|
||||
progress: {
|
||||
@@ -805,7 +806,7 @@ describe("checkAndInterruptStaleTasks", () => {
|
||||
},
|
||||
})
|
||||
|
||||
//#when — session has unknown status type
|
||||
//#when - session has unknown status type
|
||||
await checkAndInterruptStaleTasks({
|
||||
tasks: [task],
|
||||
client: mockClient as never,
|
||||
@@ -815,7 +816,7 @@ describe("checkAndInterruptStaleTasks", () => {
|
||||
sessionStatuses: { "ses-1": { type: "some-weird-status" } },
|
||||
})
|
||||
|
||||
//#then — unknown statuses should not protect from stale timeout
|
||||
//#then - unknown statuses should not protect from stale timeout
|
||||
expect(task.status).toBe("cancelled")
|
||||
expect(task.error).toContain("Stale timeout")
|
||||
})
|
||||
|
||||
@@ -172,7 +172,7 @@ export async function checkAndInterruptStaleTasks(args: {
|
||||
const staleMinutes = Math.round(runtime / 60000)
|
||||
const reason = sessionGone ? "session gone from status registry" : "no activity"
|
||||
task.status = "cancelled"
|
||||
task.error = `Stale timeout (${reason} 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.${sessionGone ? "sessionGoneTimeoutMs" : "staleTimeoutMs"}' in .opencode/${CONFIG_BASENAME}.json.`
|
||||
task.error = `Stale timeout (${reason} 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.${sessionGone ? "sessionGoneTimeoutMs" : "messageStalenessTimeoutMs"}' in .opencode/${CONFIG_BASENAME}.json.`
|
||||
task.completedAt = new Date()
|
||||
|
||||
if (task.concurrencyKey) {
|
||||
|
||||
Reference in New Issue
Block a user