fix(executor): Handle REVIEW status when stopping graph executions

Critical bug fix: stopping a graph in REVIEW status caused timeouts and orphaned reviews.

## Bugs Fixed

### 1. REVIEW Status Not Handled
Before:
- stop_graph_execution() only handled QUEUED, INCOMPLETE, RUNNING, COMPLETED, FAILED
- REVIEW status → waited 15 seconds → TimeoutError
- Graph remained stuck in REVIEW status

After:
- REVIEW status treated like QUEUED/INCOMPLETE (terminate immediately)
- No need to wait for executor since execution is paused
- Clean termination without timeouts

### 2. Orphaned Pending Reviews
Before:
- Stopping graph → status = TERMINATED
- Pending reviews remained in WAITING status
- User saw reviews for terminated execution in UI
- Could not approve/reject (backend validation rejects)
- Reviews stuck until manual cleanup

After:
- When stopping REVIEW execution, clean up pending reviews
- Mark all WAITING reviews as REJECTED
- reviewMessage: 'Execution was stopped by user'
- processed: true, reviewedAt: now()
- No orphaned reviews in UI

### 3. Subagent Reviews
Before:
- Parent graph with child (subagent) executions
- Child paused for HITL review
- Stop parent → recursively stops child
- Child reviews orphaned (same bugs as above)

After:
- Cascade stop properly handles child REVIEW status
- All child reviews cleaned up recursively
- Clean shutdown of entire execution tree

## Implementation

Changes to stop_graph_execution():
1. Added ExecutionStatus.REVIEW to immediate termination list
2. Check if status == REVIEW before marking TERMINATED
3. Update all WAITING reviews to REJECTED with message
4. Log cleanup for debugging
5. Then terminate execution normally

Cascade behavior preserved:
- Still recursively stops all child executions
- Each child's reviews cleaned up individually
- Parent waits for all children to complete cleanup
This commit is contained in:
Zamil Majdy
2026-01-22 18:27:08 -05:00
parent aea97db485
commit 67e6a8841c

View File

@@ -4,6 +4,7 @@ import threading
import time
from collections import defaultdict
from concurrent.futures import Future
from datetime import datetime, timezone
from typing import Mapping, Optional, cast
from pydantic import BaseModel, JsonValue, ValidationError
@@ -749,9 +750,33 @@ async def stop_graph_execution(
if graph_exec.status in [
ExecutionStatus.QUEUED,
ExecutionStatus.INCOMPLETE,
ExecutionStatus.REVIEW,
]:
# If the graph is still on the queue, we can prevent them from being executed
# by setting the status to TERMINATED.
# If the graph is queued/incomplete/paused for review, terminate immediately
# No need to wait for executor since it's not actively running
# If graph is in REVIEW status, clean up pending reviews before terminating
if graph_exec.status == ExecutionStatus.REVIEW:
from prisma.enums import ReviewStatus
from prisma.models import PendingHumanReview
# Mark all pending reviews as rejected/cancelled
await PendingHumanReview.prisma().update_many(
where={
"graphExecId": graph_exec_id,
"status": ReviewStatus.WAITING,
},
data={
"status": ReviewStatus.REJECTED,
"reviewMessage": "Execution was stopped by user",
"processed": True,
"reviewedAt": datetime.now(timezone.utc),
},
)
logger.info(
f"Cleaned up pending reviews for stopped execution {graph_exec_id}"
)
graph_exec.status = ExecutionStatus.TERMINATED
await asyncio.gather(