From 5d242f7546796194b2f399fd39ee436b2367f556 Mon Sep 17 00:00:00 2001 From: Dan Childers Date: Tue, 2 Jun 2026 11:44:52 -0400 Subject: [PATCH] fix: restore qualitative_retry_count propagation in bug implementation loop MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit LangGraph filters state channels based on the type annotation of each node function. implement_task and local_review_changes are both typed as FeatureState, which does not define qualitative_retry_count (a BugState-only field). As a result, LangGraph silently stripped that key from state inputs and outputs for both nodes — writes to qualitative_retry_count from local_review_changes were never checkpointed, and the counter always read as absent (defaulting to 0) on every cycle. This caused an infinite implement → qualitative-review → implement loop after plan approval. The fix is two BugState-typed wrapper functions in build_bug_graph that delegate to the shared implementations. By annotating the wrappers with BugState, LangGraph includes qualitative_retry_count in the I/O schema for those nodes, so the counter now accumulates correctly across the loop. Also fix implement_task returning current_node = "implement_task" (the Python function name rather than the graph node name "implement_bug_fix") in all three return paths. This mismatch caused route_entry to log an unrecognized node warning and restart from triage whenever forge:retry was applied while the workflow was in the implementation stage. Signed-off-by: Dan Childers Co-Authored-By: Claude Sonnet 4.6 --- src/forge/workflow/bug/graph.py | 17 +++++++++++++++-- src/forge/workflow/nodes/implementation.py | 8 ++++---- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/src/forge/workflow/bug/graph.py b/src/forge/workflow/bug/graph.py index 7f6e463..3e84889 100644 --- a/src/forge/workflow/bug/graph.py +++ b/src/forge/workflow/bug/graph.py @@ -51,6 +51,19 @@ _MAX_REFLECTION_COUNT = 3 +# LangGraph filters state channels based on each node function's type annotation. +# implement_task and local_review_changes are typed as FeatureState, which lacks +# bug-specific fields like qualitative_retry_count. These wrappers are typed as +# BugState so LangGraph passes and records the full bug state for these nodes. + + +async def _implement_task_bug(state: BugState) -> BugState: + return await implement_task(state) # type: ignore[return-value] + + +async def _local_review_bug(state: BugState) -> BugState: + return await local_review_changes(state) # type: ignore[return-value] + def route_entry(state: BugState) -> str: """Route workflow based on current progress for resume/retry. @@ -371,8 +384,8 @@ def build_bug_graph() -> StateGraph: # Use the container-based implement_task (same as feature workflow) so the # fix runs inside an isolated Podman container with full tool access. # implement_bug_fix (ForgeAgent-based) is kept only for route_entry backward compat. - graph.add_node("implement_bug_fix", implement_task) - graph.add_node("local_review", local_review_changes) + graph.add_node("implement_bug_fix", _implement_task_bug) + graph.add_node("local_review", _local_review_bug) graph.add_node("update_documentation", update_documentation) graph.add_node("create_pr", create_pull_request) graph.add_node("teardown_workspace", teardown_and_route) diff --git a/src/forge/workflow/nodes/implementation.py b/src/forge/workflow/nodes/implementation.py index bfef01e..d38bffc 100644 --- a/src/forge/workflow/nodes/implementation.py +++ b/src/forge/workflow/nodes/implementation.py @@ -51,7 +51,7 @@ async def implement_task(state: WorkflowState) -> WorkflowState: return { **state, "last_error": "Workspace not set up", - "current_node": "implement_task", + "current_node": "implement_bug_fix", } # Get next task to implement if not set @@ -159,9 +159,9 @@ async def implement_task(state: WorkflowState) -> WorkflowState: **state, "current_task_key": None, "implemented_tasks": implemented, - "current_node": "implement_task", # Loop back for next task + "current_node": "implement_bug_fix", "last_error": None, - "retry_count": 0, # Reset retry count on success + "retry_count": 0, } ) else: @@ -180,7 +180,7 @@ async def implement_task(state: WorkflowState) -> WorkflowState: return { **state, "last_error": str(e), - "current_node": "implement_task", + "current_node": "implement_bug_fix", "retry_count": state.get("retry_count", 0) + 1, } finally: