From 1a705cbca55c12fd5c9f8107b0d6d2b32324085b Mon Sep 17 00:00:00 2001 From: Shoubhit Dash Date: Wed, 4 Mar 2026 11:21:31 +0530 Subject: [PATCH] fix task_status stale terminal reads --- packages/opencode/src/tool/task_status.ts | 59 ++++++++---- .../opencode/test/tool/task_status.test.ts | 94 ++++++++++++++++++- 2 files changed, 133 insertions(+), 20 deletions(-) diff --git a/packages/opencode/src/tool/task_status.ts b/packages/opencode/src/tool/task_status.ts index 22fdbb20b6..1b5f963ebe 100644 --- a/packages/opencode/src/tool/task_status.ts +++ b/packages/opencode/src/tool/task_status.ts @@ -44,35 +44,58 @@ async function inspect(taskID: string) { } } + let latestUser: MessageV2.User | undefined + let latestAssistant: + | { + info: MessageV2.Assistant + parts: MessageV2.Part[] + } + | undefined for await (const item of MessageV2.stream(taskID)) { - if (item.info.role !== "assistant") continue - - const text = item.parts.findLast((part) => part.type === "text")?.text ?? "" - if (item.info.error) { - const summary = errorText(item.info.error) - return { - state: "error" as const, - text: text || summary, - } - } - - const done = item.info.finish && !["tool-calls", "unknown"].includes(item.info.finish) - if (done) { - return { - state: "completed" as const, - text, + if (!latestUser && item.info.role === "user") latestUser = item.info + if (!latestAssistant && item.info.role === "assistant") { + latestAssistant = { + info: item.info, + parts: item.parts, } } + if (latestUser && latestAssistant) break + } + if (!latestAssistant) { return { state: "running" as const, - text: text || "Task is still running.", + text: "Task has started but has not produced output yet.", + } + } + + if (latestUser && latestUser.id > latestAssistant.info.id) { + return { + state: "running" as const, + text: "Task is starting.", + } + } + + const text = latestAssistant.parts.findLast((part) => part.type === "text")?.text ?? "" + if (latestAssistant.info.error) { + const summary = errorText(latestAssistant.info.error) + return { + state: "error" as const, + text: text || summary, + } + } + + const done = latestAssistant.info.finish && !["tool-calls", "unknown"].includes(latestAssistant.info.finish) + if (done) { + return { + state: "completed" as const, + text, } } return { state: "running" as const, - text: "Task has started but has not produced output yet.", + text: text || "Task is still running.", } } diff --git a/packages/opencode/test/tool/task_status.test.ts b/packages/opencode/test/tool/task_status.test.ts index 33b741e6c9..3ffa943a9a 100644 --- a/packages/opencode/test/tool/task_status.test.ts +++ b/packages/opencode/test/tool/task_status.test.ts @@ -18,6 +18,22 @@ const ctx = { ask: async () => {}, } +async function user(sessionID: string) { + await Session.updateMessage({ + id: Identifier.ascending("message"), + sessionID, + role: "user", + time: { + created: Date.now(), + }, + agent: "build", + model: { + providerID: "test-provider", + modelID: "test-model", + }, + }) +} + async function assistant(input: { sessionID: string; text: string; error?: string }) { const msg = await Session.updateMessage({ id: Identifier.ascending("message"), @@ -116,13 +132,13 @@ describe("tool.task_status", () => { const tool = await TaskStatusTool.init() SessionStatus.set(session.id, { type: "busy" }) - setTimeout(async () => { + const transition = Bun.sleep(150).then(async () => { SessionStatus.set(session.id, { type: "idle" }) await assistant({ sessionID: session.id, text: "finished later", }) - }, 150) + }) const result = await tool.execute( { @@ -133,9 +149,83 @@ describe("tool.task_status", () => { ctx, ) + await transition expect(result.output).toContain("state: completed") expect(result.output).toContain("finished later") }, }) }) + + test("returns error when child run fails", async () => { + await using tmp = await tmpdir({ git: true }) + + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const session = await Session.create({}) + const tool = await TaskStatusTool.init() + + await assistant({ + sessionID: session.id, + text: "", + error: "child failed", + }) + + const result = await tool.execute({ task_id: session.id }, ctx) + expect(result.output).toContain("state: error") + expect(result.output).toContain("child failed") + expect(result.metadata.state).toBe("error") + }, + }) + }) + + test("wait=true times out with timed_out metadata", async () => { + await using tmp = await tmpdir({ git: true }) + + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const session = await Session.create({}) + const tool = await TaskStatusTool.init() + + SessionStatus.set(session.id, { type: "busy" }) + const result = await tool.execute( + { + task_id: session.id, + wait: true, + timeout_ms: 80, + }, + ctx, + ) + + expect(result.output).toContain("Timed out after 80ms") + expect(result.metadata.timed_out).toBe(true) + expect(result.metadata.state).toBe("running") + SessionStatus.set(session.id, { type: "idle" }) + }, + }) + }) + + test("returns running for resumed task with a newer user turn", async () => { + await using tmp = await tmpdir({ git: true }) + + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const session = await Session.create({}) + const tool = await TaskStatusTool.init() + + await user(session.id) + await assistant({ + sessionID: session.id, + text: "old done", + }) + await user(session.id) + + const result = await tool.execute({ task_id: session.id }, ctx) + expect(result.output).toContain("state: running") + expect(result.output).toContain("Task is starting.") + }, + }) + }) })