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
|
//#then
|
||||||
expect(task.status).toBe("cancelled")
|
expect(task.status).toBe("cancelled")
|
||||||
expect(task.error).toContain("no activity")
|
expect(task.error).toContain("no activity")
|
||||||
|
expect(task.error).toContain("messageStalenessTimeoutMs")
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should await abort before resolving for no-progress stale interruption", async () => {
|
it("should await abort before resolving for no-progress stale interruption", async () => {
|
||||||
@@ -403,7 +404,7 @@ describe("checkAndInterruptStaleTasks", () => {
|
|||||||
progress: undefined,
|
progress: undefined,
|
||||||
})
|
})
|
||||||
|
|
||||||
//#when — session is running
|
//#when - session is running
|
||||||
await checkAndInterruptStaleTasks({
|
await checkAndInterruptStaleTasks({
|
||||||
tasks: [task],
|
tasks: [task],
|
||||||
client: mockClient as never,
|
client: mockClient as never,
|
||||||
@@ -419,7 +420,7 @@ describe("checkAndInterruptStaleTasks", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it("should NOT cancel healthy task on first missing status poll", async () => {
|
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({
|
const task = createRunningTask({
|
||||||
startedAt: new Date(Date.now() - 300_000),
|
startedAt: new Date(Date.now() - 300_000),
|
||||||
progress: {
|
progress: {
|
||||||
@@ -445,7 +446,7 @@ describe("checkAndInterruptStaleTasks", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it("should NOT cancel task when session.get confirms the session still exists", async () => {
|
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({
|
const task = createRunningTask({
|
||||||
startedAt: new Date(Date.now() - 300_000),
|
startedAt: new Date(Date.now() - 300_000),
|
||||||
progress: {
|
progress: {
|
||||||
@@ -472,7 +473,7 @@ describe("checkAndInterruptStaleTasks", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it("should NOT cancel task when session.get returns a transient error response", async () => {
|
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({
|
const task = createRunningTask({
|
||||||
startedAt: new Date(Date.now() - 300_000),
|
startedAt: new Date(Date.now() - 300_000),
|
||||||
progress: {
|
progress: {
|
||||||
@@ -504,7 +505,7 @@ describe("checkAndInterruptStaleTasks", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it("should use session-gone timeout when session is missing from status map (with progress)", async () => {
|
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({
|
const task = createRunningTask({
|
||||||
startedAt: new Date(Date.now() - 300_000),
|
startedAt: new Date(Date.now() - 300_000),
|
||||||
progress: {
|
progress: {
|
||||||
@@ -516,7 +517,7 @@ describe("checkAndInterruptStaleTasks", () => {
|
|||||||
|
|
||||||
mockClient.session.get.mockRejectedValue(new Error("missing"))
|
mockClient.session.get.mockRejectedValue(new Error("missing"))
|
||||||
|
|
||||||
//#when — empty sessionStatuses (session gone), sessionGoneTimeoutMs = 60s
|
//#when - empty sessionStatuses (session gone), sessionGoneTimeoutMs = 60s
|
||||||
await checkAndInterruptStaleTasks({
|
await checkAndInterruptStaleTasks({
|
||||||
tasks: [task],
|
tasks: [task],
|
||||||
client: mockClient as never,
|
client: mockClient as never,
|
||||||
@@ -526,7 +527,7 @@ describe("checkAndInterruptStaleTasks", () => {
|
|||||||
sessionStatuses: {},
|
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.status).toBe("cancelled")
|
||||||
expect(task.error).toContain("session gone from status registry")
|
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 () => {
|
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({
|
const task = createRunningTask({
|
||||||
startedAt: new Date(Date.now() - 120_000),
|
startedAt: new Date(Date.now() - 120_000),
|
||||||
progress: undefined,
|
progress: undefined,
|
||||||
@@ -580,7 +581,7 @@ describe("checkAndInterruptStaleTasks", () => {
|
|||||||
|
|
||||||
mockClient.session.get.mockRejectedValue(new Error("missing"))
|
mockClient.session.get.mockRejectedValue(new Error("missing"))
|
||||||
|
|
||||||
//#when — session gone, sessionGoneTimeoutMs = 60s
|
//#when - session gone, sessionGoneTimeoutMs = 60s
|
||||||
await checkAndInterruptStaleTasks({
|
await checkAndInterruptStaleTasks({
|
||||||
tasks: [task],
|
tasks: [task],
|
||||||
client: mockClient as never,
|
client: mockClient as never,
|
||||||
@@ -590,13 +591,13 @@ describe("checkAndInterruptStaleTasks", () => {
|
|||||||
sessionStatuses: {},
|
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.status).toBe("cancelled")
|
||||||
expect(task.error).toContain("session gone from status registry")
|
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 () => {
|
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({
|
const task = createRunningTask({
|
||||||
startedAt: new Date(Date.now() - 300_000),
|
startedAt: new Date(Date.now() - 300_000),
|
||||||
progress: {
|
progress: {
|
||||||
@@ -608,7 +609,7 @@ describe("checkAndInterruptStaleTasks", () => {
|
|||||||
|
|
||||||
mockClient.session.get.mockRejectedValue(new Error("missing"))
|
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({
|
await checkAndInterruptStaleTasks({
|
||||||
tasks: [task],
|
tasks: [task],
|
||||||
client: mockClient as never,
|
client: mockClient as never,
|
||||||
@@ -618,12 +619,12 @@ describe("checkAndInterruptStaleTasks", () => {
|
|||||||
sessionStatuses: { "ses-1": { type: "idle" } },
|
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")
|
expect(task.status).toBe("running")
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should use default session-gone timeout when not configured", async () => {
|
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({
|
const task = createRunningTask({
|
||||||
startedAt: new Date(Date.now() - 300_000),
|
startedAt: new Date(Date.now() - 300_000),
|
||||||
progress: {
|
progress: {
|
||||||
@@ -635,7 +636,7 @@ describe("checkAndInterruptStaleTasks", () => {
|
|||||||
|
|
||||||
mockClient.session.get.mockRejectedValue(new Error("missing"))
|
mockClient.session.get.mockRejectedValue(new Error("missing"))
|
||||||
|
|
||||||
//#when — no config (default sessionGoneTimeoutMs = 60_000)
|
//#when - no config (default sessionGoneTimeoutMs = 60_000)
|
||||||
await checkAndInterruptStaleTasks({
|
await checkAndInterruptStaleTasks({
|
||||||
tasks: [task],
|
tasks: [task],
|
||||||
client: mockClient as never,
|
client: mockClient as never,
|
||||||
@@ -645,13 +646,13 @@ describe("checkAndInterruptStaleTasks", () => {
|
|||||||
sessionStatuses: {},
|
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.status).toBe("cancelled")
|
||||||
expect(task.error).toContain("session gone from status registry")
|
expect(task.error).toContain("session gone from status registry")
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should interrupt task when busy session exceeds stale timeout", async () => {
|
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({
|
const task = createRunningTask({
|
||||||
startedAt: new Date(Date.now() - 300_000),
|
startedAt: new Date(Date.now() - 300_000),
|
||||||
progress: {
|
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({
|
await checkAndInterruptStaleTasks({
|
||||||
tasks: [task],
|
tasks: [task],
|
||||||
client: mockClient as never,
|
client: mockClient as never,
|
||||||
@@ -676,7 +677,7 @@ describe("checkAndInterruptStaleTasks", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it("should interrupt task when retry session exceeds stale timeout", async () => {
|
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({
|
const task = createRunningTask({
|
||||||
startedAt: new Date(Date.now() - 300_000),
|
startedAt: new Date(Date.now() - 300_000),
|
||||||
progress: {
|
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({
|
await checkAndInterruptStaleTasks({
|
||||||
tasks: [task],
|
tasks: [task],
|
||||||
client: mockClient as never,
|
client: mockClient as never,
|
||||||
@@ -701,13 +702,13 @@ describe("checkAndInterruptStaleTasks", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it("should interrupt busy session with no progress after message staleness timeout", async () => {
|
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({
|
const task = createRunningTask({
|
||||||
startedAt: new Date(Date.now() - 15 * 60 * 1000),
|
startedAt: new Date(Date.now() - 15 * 60 * 1000),
|
||||||
progress: undefined,
|
progress: undefined,
|
||||||
})
|
})
|
||||||
|
|
||||||
//#when — session is busy
|
//#when - session is busy
|
||||||
await checkAndInterruptStaleTasks({
|
await checkAndInterruptStaleTasks({
|
||||||
tasks: [task],
|
tasks: [task],
|
||||||
client: mockClient as never,
|
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 () => {
|
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({
|
const task = createRunningTask({
|
||||||
startedAt: new Date(Date.now() - 300_000),
|
startedAt: new Date(Date.now() - 300_000),
|
||||||
progress: {
|
progress: {
|
||||||
@@ -780,7 +781,7 @@ describe("checkAndInterruptStaleTasks", () => {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
//#when — session status is "interrupted" (terminal)
|
//#when - session status is "interrupted" (terminal)
|
||||||
await checkAndInterruptStaleTasks({
|
await checkAndInterruptStaleTasks({
|
||||||
tasks: [task],
|
tasks: [task],
|
||||||
client: mockClient as never,
|
client: mockClient as never,
|
||||||
@@ -790,13 +791,13 @@ describe("checkAndInterruptStaleTasks", () => {
|
|||||||
sessionStatuses: { "ses-1": { type: "interrupted" } },
|
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.status).toBe("cancelled")
|
||||||
expect(task.error).toContain("Stale timeout")
|
expect(task.error).toContain("Stale timeout")
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should NOT protect task when session has unknown status type', async () => {
|
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({
|
const task = createRunningTask({
|
||||||
startedAt: new Date(Date.now() - 300_000),
|
startedAt: new Date(Date.now() - 300_000),
|
||||||
progress: {
|
progress: {
|
||||||
@@ -805,7 +806,7 @@ describe("checkAndInterruptStaleTasks", () => {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
//#when — session has unknown status type
|
//#when - session has unknown status type
|
||||||
await checkAndInterruptStaleTasks({
|
await checkAndInterruptStaleTasks({
|
||||||
tasks: [task],
|
tasks: [task],
|
||||||
client: mockClient as never,
|
client: mockClient as never,
|
||||||
@@ -815,7 +816,7 @@ describe("checkAndInterruptStaleTasks", () => {
|
|||||||
sessionStatuses: { "ses-1": { type: "some-weird-status" } },
|
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.status).toBe("cancelled")
|
||||||
expect(task.error).toContain("Stale timeout")
|
expect(task.error).toContain("Stale timeout")
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -172,7 +172,7 @@ export async function checkAndInterruptStaleTasks(args: {
|
|||||||
const staleMinutes = Math.round(runtime / 60000)
|
const staleMinutes = Math.round(runtime / 60000)
|
||||||
const reason = sessionGone ? "session gone from status registry" : "no activity"
|
const reason = sessionGone ? "session gone from status registry" : "no activity"
|
||||||
task.status = "cancelled"
|
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()
|
task.completedAt = new Date()
|
||||||
|
|
||||||
if (task.concurrencyKey) {
|
if (task.concurrencyKey) {
|
||||||
|
|||||||
Reference in New Issue
Block a user