Skip to content

fix(goal): don't halt the goal loop on a STUCK run - #4381

Open
all-hands-bot wants to merge 1 commit into
mainfrom
fix/goal-loop-stuck-interruption
Open

fix(goal): don't halt the goal loop on a STUCK run#4381
all-hands-bot wants to merge 1 commit into
mainfrom
fix/goal-loop-stuck-interruption

Conversation

@all-hands-bot

@all-hands-bot all-hands-bot commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator

Summary

Fixes #4380

The /goal loop was halting as interrupted whenever an individual agent run ended in STUCK status, forcing a manual "resume" click. This defeated the purpose of the goal command — a goal is supposed to keep working until finished.

Root cause

_run_goal_loop treated STUCK the same as PAUSED/ERROR — a hard terminal stop:

if status in (PAUSED, ERROR, STUCK):
    await _emit_status(active=False, status="interrupted")
    return

STUCK comes from the StuckDetector, a heuristic that fires on repetitive action/observation patterns with aggressive thresholds (action_observation=4, action_error=3, monologue=3). These were tuned for single-turn chat. During a goal loop the agent intentionally iterates — re-running a test, retrying an edit — so it regularly trips the detector during legitimate work, ending the run as STUCK and halting the whole goal prematurely.

Fix

Remove STUCK from the set of statuses that terminate the goal loop. Only PAUSED and ERROR (real stop signals) halt it. A STUCK run now proceeds to the judge path, which either marks the goal complete or sends a followup nudge that breaks the agent out of any genuine loop. Worst case the goal caps out at max_iterations (bounded), which is strictly better than halting early at round 4.

The goal loop already has an authoritative judge LLM (GoalController + judge_goal) that audits completion each round — a far better progress signal than the stuck detector's repetition heuristic. This change lets the judge do its job instead of being preempted by the heuristic.

Test

Added test_goal_loop_continues_past_stuck_run, which forces the first run to end STUCK and asserts the loop continues to the judge and completes (rather than emitting interrupted). Verified the test fails before the fix (assert 'interrupted' == 'complete') and passes after.

All 18 existing goal-loop tests and 18 SDK goal tests continue to pass.


This PR was created by an AI agent (OpenHands) on behalf of a user.


Agent Server images for this PR

GHCR package: https://github.com/OpenHands/agent-sdk/pkgs/container/agent-server

Variants & Base Images

Variant Architectures Base Image Docs / Tags
java amd64, arm64 eclipse-temurin:17-jdk Link
python amd64, arm64 nikolaik/python-nodejs:python3.13-nodejs22-slim Link
golang amd64, arm64 golang:1.21-bookworm Link

Pull (multi-arch manifest)

# Each variant is a multi-arch manifest supporting both amd64 and arm64
docker pull ghcr.io/openhands/agent-server:4ee3376-python

Run

docker run -it --rm \
  -p 8000:8000 \
  --name agent-server-4ee3376-python \
  ghcr.io/openhands/agent-server:4ee3376-python

All tags pushed for this build

ghcr.io/openhands/agent-server:4ee3376-golang-amd64
ghcr.io/openhands/agent-server:4ee3376ff87b54c9874acc9cea6d83e24647227e-golang-amd64
ghcr.io/openhands/agent-server:fix-goal-loop-stuck-interruption-golang-amd64
ghcr.io/openhands/agent-server:4ee3376-golang_tag_1.21-bookworm-amd64
ghcr.io/openhands/agent-server:4ee3376-golang-arm64
ghcr.io/openhands/agent-server:4ee3376ff87b54c9874acc9cea6d83e24647227e-golang-arm64
ghcr.io/openhands/agent-server:fix-goal-loop-stuck-interruption-golang-arm64
ghcr.io/openhands/agent-server:4ee3376-golang_tag_1.21-bookworm-arm64
ghcr.io/openhands/agent-server:4ee3376-java-amd64
ghcr.io/openhands/agent-server:4ee3376ff87b54c9874acc9cea6d83e24647227e-java-amd64
ghcr.io/openhands/agent-server:fix-goal-loop-stuck-interruption-java-amd64
ghcr.io/openhands/agent-server:4ee3376-eclipse-temurin_tag_17-jdk-amd64
ghcr.io/openhands/agent-server:4ee3376-java-arm64
ghcr.io/openhands/agent-server:4ee3376ff87b54c9874acc9cea6d83e24647227e-java-arm64
ghcr.io/openhands/agent-server:fix-goal-loop-stuck-interruption-java-arm64
ghcr.io/openhands/agent-server:4ee3376-eclipse-temurin_tag_17-jdk-arm64
ghcr.io/openhands/agent-server:4ee3376-python-amd64
ghcr.io/openhands/agent-server:4ee3376ff87b54c9874acc9cea6d83e24647227e-python-amd64
ghcr.io/openhands/agent-server:fix-goal-loop-stuck-interruption-python-amd64
ghcr.io/openhands/agent-server:4ee3376-nikolaik_s_python-nodejs_tag_python3.13-nodejs22-slim-amd64
ghcr.io/openhands/agent-server:4ee3376-python-arm64
ghcr.io/openhands/agent-server:4ee3376ff87b54c9874acc9cea6d83e24647227e-python-arm64
ghcr.io/openhands/agent-server:fix-goal-loop-stuck-interruption-python-arm64
ghcr.io/openhands/agent-server:4ee3376-nikolaik_s_python-nodejs_tag_python3.13-nodejs22-slim-arm64
ghcr.io/openhands/agent-server:4ee3376-golang
ghcr.io/openhands/agent-server:4ee3376ff87b54c9874acc9cea6d83e24647227e-golang
ghcr.io/openhands/agent-server:fix-goal-loop-stuck-interruption-golang
ghcr.io/openhands/agent-server:4ee3376-golang_tag_1.21-bookworm
ghcr.io/openhands/agent-server:4ee3376-java
ghcr.io/openhands/agent-server:4ee3376ff87b54c9874acc9cea6d83e24647227e-java
ghcr.io/openhands/agent-server:fix-goal-loop-stuck-interruption-java
ghcr.io/openhands/agent-server:4ee3376-eclipse-temurin_tag_17-jdk
ghcr.io/openhands/agent-server:4ee3376-python
ghcr.io/openhands/agent-server:4ee3376ff87b54c9874acc9cea6d83e24647227e-python
ghcr.io/openhands/agent-server:fix-goal-loop-stuck-interruption-python
ghcr.io/openhands/agent-server:4ee3376-nikolaik_s_python-nodejs_tag_python3.13-nodejs22-slim

About Multi-Architecture Support

  • Each variant tag (e.g., 4ee3376-python) is a multi-arch manifest supporting both amd64 and arm64
  • Docker automatically pulls the correct architecture for your platform
  • Individual architecture tags (e.g., 4ee3376-python-amd64) are also available if needed

The goal loop treated a STUCK run (the stuck-detector heuristic firing)
the same as PAUSED/ERROR -- a hard terminal stop that emitted
"interrupted" and required a manual resume. This defeated the purpose
of the goal command: a goal is supposed to keep working until finished.

The stuck detector is a heuristic tuned for single-turn chat and often
fires during legitimate iteration (re-running a test, retrying an edit).
The goal loop already has an authoritative judge LLM that audits
completion each round, which is a far better progress signal. So a STUCK
run should not terminate the whole goal; only PAUSED/ERROR (real stop
signals) should.

Now a STUCK run proceeds to the judge, which either marks the goal
complete or sends a followup nudge that breaks the agent out of any
genuine loop. Worst case it caps out at max_iterations (bounded), which
is strictly better than halting early.

Co-authored-by: openhands <openhands@all-hands.dev>
@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Python API breakage checks — ✅ PASSED

Result:PASSED

Action log

@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

REST API breakage checks (OpenAPI) — ✅ PASSED

Result:PASSED

Action log

@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Coverage

Coverage Report •
FileStmtsMissCoverMissing
openhands-agent-server/openhands/agent_server
   event_service.py85812685%162–163, 289, 293, 298, 325, 332, 366, 369–370, 374–375, 386, 392, 402–406, 409–412, 483, 504–505, 579, 633, 653, 660, 684–685, 689, 697, 700, 716, 748, 759, 766, 772, 837–838, 916, 946, 949, 1009, 1030, 1154, 1266–1269, 1273, 1302, 1306, 1313, 1327, 1342, 1389–1391, 1471, 1496, 1502, 1504, 1514, 1516, 1523, 1533, 1535–1536, 1540, 1554–1559, 1561, 1588, 1593–1599, 1603–1606, 1614–1617, 1663–1665, 1719–1720, 1722–1729, 1731–1732, 1741–1742, 1744–1745, 1752–1753, 1755–1756, 1767, 1792, 1798, 1804, 1813–1814, 1836, 1846
TOTAL394401650258% 

@all-hands-bot

Copy link
Copy Markdown
Collaborator Author

👋 This PR needs a couple of things fixed before OpenHands can review it:

  • the PR description's HUMAN: section needs at least 20 characters describing what you tested, not just the template placeholder

Push an update once this is addressed and this check re-runs automatically.

This is an automated check - no AI was used to generate this comment.

@neubig
neubig self-requested a review August 5, 2026 17:37
# let it decide continue-vs-stop (sending a followup nudge
# that breaks the agent out of any genuine loop). Only
# PAUSED/ERROR (real stop signals) terminate the goal.
logger.info("Goal loop continuing past stuck run")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The risk with this is what the agent noticed in the linked issue: that if the LLM truly is and remains stuck on some basin, then it will continue up to max_iterations. I think maybe we can try it; but it’s good to be aware of that and keep an eye on it?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Goal loop halts as "interrupted" on a STUCK run, defeating the goal command

3 participants