limited-student-10747
08/05/2026, 4:01 PMfresh-machine-37269
08/05/2026, 4:01 PMfresh-machine-37269
08/05/2026, 4:14 PM_run_goal_loop (in event_service.py) works like this each round:
1. Send the objective/followup nudge as a user message
2. Call self.run() to let the agent work
3. Check the conversation's execution status
4. If status is PAUSED, ERROR, or STUCK → emit "interrupted" and return (halting the loop)
5. Otherwise, let the judge LLM audit the transcript and decide continue-vs-stop
The catch is the stuck detector (stuck_detector.py), which runs inside each run() and checks after every agent.step(). Its default thresholds are quite aggressive (types.py):
• `action_observation`: 4 (same action + same observation 4×)
• `action_error`: 3 (same action failing 3×)
• `monologue`: 3 (3 consecutive agent messages)
The detector only looks at events after the last user message, so its window resets each goal round. But within a single round, the agent takes many steps. During normal iterative work — re-running a test, retrying an edit, exploring a file again — it's easy to trip one of these thresholds. When that happens, the run breaks out as STUCK, the goal loop sees STUCK, and it lumps it together with `PAUSED`/`ERROR` into a single terminal "interrupted" state, requiring you to click resume.
Why it's a design tension
The goal loop already has a judge LLM that authoritatively evaluates whether the objective is complete and what's missing — a much better progress signal than the stuck detector's heuristic. But the loop currently treats STUCK (a heuristic guess, often a false positive during legitimate iteration) the same as PAUSED (a deliberate user stop) and ERROR (a real failure). So the stuck detector's hard halt is layered on top of the judge, which defeats the "keep working until finished" contract you're expecting.
Should it be improved? Yes. Your intuition is right. The cleanest fix would be to stop treating STUCK as terminal inside the goal loop — only PAUSED (and arguably ERROR) should hard-stop it. For STUCK, the loop should proceed to the judge/continue path, since the judge is the authoritative completion signal. At minimum, a STUCK mid-goal-loop could trigger a followup nudge (re-prompt) rather than a full halt.
Conflating "the agent repeated an action 3 times" with "stop the whole goal" is the bug, essentially. The goal command's purpose is to iterate until done; the stuck detector's defaults were tuned for single-turn chat, not for goal-driven multi-round work.
I'd recommend opening an issue (or PR) on OpenHands/software-agent-sdk proposing that _run_goal_loop distinguish STUCK from `PAUSED`/`ERROR` — letting the judge decide on STUCK rather than halting.limited-student-10747
08/05/2026, 4:20 PMfresh-machine-37269
08/05/2026, 4:49 PMcalm-artist-7206
08/05/2026, 4:51 PMlimited-student-10747
08/05/2026, 5:15 PMcalm-artist-7206
08/05/2026, 5:51 PMlimited-student-10747
08/05/2026, 5:53 PMcalm-artist-7206
08/05/2026, 5:53 PMfile editor tool definition, we saw that multiple timescalm-artist-7206
08/05/2026, 5:55 PMcalm-artist-7206
08/05/2026, 5:56 PMlimited-student-10747
08/05/2026, 6:05 PMlimited-student-10747
08/05/2026, 6:05 PMfresh-machine-37269
08/05/2026, 6:05 PMcalm-artist-7206
08/05/2026, 6:10 PMoss-openhandsfresh-machine-37269
08/05/2026, 6:12 PMfile_editor tool, model-specific formatting choke) doesn't hold here. With ACP the SDK doesn't own the tool schema — it proxies a remote agent (Codex), so the "file editor tool definition" mismatch isn't the failure mode.
What I see in the code instead: the goal loop in event_service._run_goal_loop halts early and emits interrupted whenever the run settles in PAUSED, ERROR, or `STUCK`:
status = await self._get_execution_status()
if status in (PAUSED, ERROR, STUCK):
await _emit_status(active=False, status="interrupted")
return
For ACP+Codex there are a few ACP-specific ways to land there that a plain GLM run wouldn't hit:
1. ACP turn errors — _emit_turn_error sets ERROR on any non-timeout exception out of the prompt (JSON-RPC -32603, transient server errors, auth). The prompt loop does retry retriable `-32603`s, but a non-retriable or final error marks the whole turn ERROR → goal loop calls it interrupted.
2. ACP prompt timeout — _emit_turn_timeout sets ERROR when the server goes idle past acp_prompt_timeout. Codex can stall/finish-silently, and this surfaces as ERROR → interrupted, even if the work was actually done.
3. Stuck-detector misfire on ACP turns — every ACP step() emits a `FinishAction`+`Observation` (one full remote turn). The SDK StuckDetector scenario 1 (repeating identical action+observation pairs) can trip on repeated identical finish turns → STUCK → interrupted. This is independent of the model.
4. ACP internal-rerun / interrupt — the send_message(run=True) supersede path interrupts the in-flight prompt and re-runs; if that leaves the conversation PAUSED, the goal loop treats it as interrupted.
So the PR's core idea — stuck shouldn't be a terminal state inside a goal, just terminal for that agent turn → continue to judge — is still the right direction, but with ACP it's not only STUCK that needs this treatment. The fix should arguably also let the judge run after an `ERROR`/`PAUSED` ACP turn (the agent may have produced real evidence before the turn broke), or at least distinguish transient ACP errors from genuine failures, instead of blanket-marking the goal interrupted on the first non-FINISHED status.
One caveat: I couldn't pull the exact PR 4381 diff (GitHub rate-limited the API and it's not in this checkout's git history), so I can't confirm whether the current fix already covers the ACP `ERROR`/`PAUSED` paths or only the STUCK one. If it only handles STUCK, the ACP+Codex failure you saw would still slip through — which would explain why it finished, got judged as stuck/interrupted, and needed resume. Worth checking the PR covers all three statuses (or retries transient ACP errors before halting the goal).limited-student-10747
08/05/2026, 6:14 PM