From ad3c45ec2a6cac6c5348dd4d9660020be307c29c Mon Sep 17 00:00:00 2001 From: bboonnee Date: Thu, 13 Aug 2026 18:59:04 +0800 Subject: [PATCH] fix: manual_resume auto-continue allowlist missing reviewing_planner_output --- src/backend_server.py | 15 ++++++++++++++- src/test_backend_server.py | 26 ++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/src/backend_server.py b/src/backend_server.py index 1663a7c..cbf0f72 100644 --- a/src/backend_server.py +++ b/src/backend_server.py @@ -561,8 +561,21 @@ class _Handler(BaseHTTPRequestHandler): # path, still didn't know that, so a manual Resume on a task stuck # in 'validating' flipped the status back with nothing ever # actually continuing it. + # + # 'reviewing_planner_output' added 2026-08-13 (real incident, task + # b243dfe8): same exact bug, one status later -- Org Graph + # unification Phase 1 (2026-08-08) added this status to + # task_execution._RESUMABLE_STATUSES, but nobody updated THIS + # separate allowlist to match, so a manual_resume off + # blocked_pending_human onto 'reviewing_planner_output' flipped the + # DB status and spawned nothing -- the task sat completely idle + # (zero new execution_event rows, no claude subprocess) looking + # exactly like it was "stuck again", when actually nothing was + # running at all. Any status added to _RESUMABLE_STATUSES going + # forward needs to land here too -- the two lists have now drifted + # out of sync twice for the identical reason. if pre_resume_reason not in ("budget_exceeded", "loop_limit_reached") and result.get("new_status") in ( - "queued", "planning_brd_prd_sdd", "coding", "validating", + "queued", "planning_brd_prd_sdd", "reviewing_planner_output", "coding", "validating", ): task_row = self.server.conn.execute( "SELECT project_id, goal_text FROM task WHERE task_id = ?", (task_id,) diff --git a/src/test_backend_server.py b/src/test_backend_server.py index ed365a6..df0ef15 100644 --- a/src/test_backend_server.py +++ b/src/test_backend_server.py @@ -912,6 +912,32 @@ class TaskExecutionRouteTest(unittest.TestCase): self.assertEqual(status, 200) mock_start.assert_called_once_with(self.server.db_path, "task-1", "/tmp/proj-1", "the goal") + def test_manual_resume_from_reviewing_planner_output_auto_continues(self): + # 2026-08-13 real incident (task b243dfe8): same exact bug as + # 'validating' above, one status later -- Org Graph unification + # Phase 1 (2026-08-08) added 'reviewing_planner_output' to + # task_execution._RESUMABLE_STATUSES, but this separate allowlist + # was never updated to match. manual_resume flipped the DB status + # back and returned 200, but spawned no background thread -- the + # task sat completely idle (zero new execution_event rows, no real + # subprocess) for minutes, looking exactly like a fresh hang even + # though the actual underlying bug (a different one, already fixed + # the same day) was long gone. + self.server.conn.execute( + "UPDATE task SET status = 'blocked_pending_human', blocked_reason = 'execution_error' " + "WHERE task_id = 'task-1'" + ) + self.server.conn.execute( + "INSERT INTO execution_event (event_id, project_id, task_id, event_type, payload) " + "VALUES ('evt-1', 'proj-1', 'task-1', 'task_status_changed', " + "'{\"from_status\": \"reviewing_planner_output\", \"to_status\": \"blocked_pending_human\"}')" + ) + self.server.conn.commit() + with patch.object(backend_server.task_execution, "start_task_execution_in_background") as mock_start: + status, body = self._post("/api/tasks/task-1/route", {"decided_route": "manual_resume"}) + self.assertEqual(status, 200) + mock_start.assert_called_once_with(self.server.db_path, "task-1", "/tmp/proj-1", "the goal") + def test_execute_already_in_flight_returns_409_not_500(self): with patch.object( backend_server.task_execution, "start_task_execution_in_background", -- libgit2 0.21.4