From 50e2c257bf39913e6daccb0c7feaf4b7228ef345 Mon Sep 17 00:00:00 2001 From: fancyboi999 <135568692+fancyboi999@users.noreply.github.com> Date: Thu, 21 May 2026 18:52:07 +0800 Subject: [PATCH] fix(frontend): parse Task cancelled and polling timed out as terminal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address @ShenAC-SAC's BUG-007 review on #3131. `task_tool.py` actually emits five terminal strings: - `Task Succeeded. Result: …` - `Task failed. …` - `Task timed out. …` - `Task cancelled by user.` ← previously matched none - `Task polling timed out after N minutes …` ← previously matched none The previous cut handled three; the last two fell through to the "unknown content" branch and pushed the subtask card back to `in_progress` even though the backend had already reached a terminal state. Add explicit matches plus regression tests for both. The `in_progress` fallback is now reserved for genuinely unrecognised output (i.e. contract drift), as documented. Refs: bytedance/deer-flow#3107 (BUG-007), bytedance/deer-flow#3131 review --- frontend/src/core/tasks/subtask-result.ts | 10 ++++++++++ .../unit/core/tasks/subtask-result.test.ts | 19 +++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/frontend/src/core/tasks/subtask-result.ts b/frontend/src/core/tasks/subtask-result.ts index 505ed329d..0d018e4ce 100644 --- a/frontend/src/core/tasks/subtask-result.ts +++ b/frontend/src/core/tasks/subtask-result.ts @@ -23,6 +23,8 @@ export interface SubtaskResultUpdate { export const SUCCESS_PREFIX = "Task Succeeded. Result:"; export const FAILURE_PREFIX = "Task failed."; export const TIMEOUT_PREFIX = "Task timed out"; +export const CANCELLED_PREFIX = "Task cancelled by user."; +export const POLLING_TIMEOUT_PREFIX = "Task polling timed out"; export const ERROR_WRAPPER_PATTERN = /^Error\b/i; /** @@ -62,6 +64,14 @@ export function parseSubtaskResult(text: string): SubtaskResultUpdate { return { status: "failed", error: trimmed }; } + if (trimmed.startsWith(CANCELLED_PREFIX)) { + return { status: "failed", error: trimmed }; + } + + if (trimmed.startsWith(POLLING_TIMEOUT_PREFIX)) { + return { status: "failed", error: trimmed }; + } + // ToolErrorHandlingMiddleware-style wrapper, or any other terminal error // signal the backend forwards to the lead agent. if (ERROR_WRAPPER_PATTERN.test(trimmed)) { diff --git a/frontend/tests/unit/core/tasks/subtask-result.test.ts b/frontend/tests/unit/core/tasks/subtask-result.test.ts index fdf48d0c3..80317efb4 100644 --- a/frontend/tests/unit/core/tasks/subtask-result.test.ts +++ b/frontend/tests/unit/core/tasks/subtask-result.test.ts @@ -25,6 +25,25 @@ describe("parseSubtaskResult", () => { expect(parsed.error).toBe("Task timed out after 900s"); }); + it("recognises the cancelled-by-user prefix", () => { + // bytedance/deer-flow#3131 review: this is one of the five terminal + // strings task_tool.py actually emits — the previous cut treated it as + // unrecognised content and pushed the card back to in_progress. + const parsed = parseSubtaskResult("Task cancelled by user."); + expect(parsed.status).toBe("failed"); + expect(parsed.error).toBe("Task cancelled by user."); + }); + + it("recognises the polling-timed-out prefix", () => { + // Emitted by task_tool when the background polling loop runs out of + // budget waiting for the subagent to reach a terminal state. + const parsed = parseSubtaskResult( + "Task polling timed out after 15 minutes. This may indicate the background task is stuck. Status: RUNNING", + ); + expect(parsed.status).toBe("failed"); + expect(parsed.error).toContain("polling timed out"); + }); + it("treats middleware-wrapped tool errors as terminal failures", () => { // bytedance/deer-flow issue #3107 BUG-007: the parent-visible ToolMessage // produced by ToolErrorHandlingMiddleware never matches the three legacy