When I run the OpenHands/software-agent-sdk throug...
# proj-agent
l
When I run the OpenHands/software-agent-sdk through OpenHands/agent-canvas and use the “goal” function I get: “Goal: … Round 4/10 interrupted ” and I need to click the “resume” button to get it to resume. That kinda defeats the purpose of the goal command, since a goal is supposed to keep working until it’s finished. @fresh-machine-37269 why do we observe this behavior based on your read of the codebase, and should it be improved?
👀 1
f
🤖 On it! View progress here: oss-agent-canvas.ngrok.dev/conversations/…
✅ Done! I have a complete picture of the root cause. Let me write up my answer. The behavior you're seeing is a real design tension between two systems in the software-agent-sdk. Here's what's happening under the hood: The goal loop vs. the stuck detector The agent-server's
_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.
l
@fresh-machine-37269 open an issue and also a PR fixing it.
👀 1
f
⏳ Still working — this is taking longer than this run allows. Follow along at the link above; mention me again for an update.
c
Interesting one! If the agent is right, this is a good solution - ‘stuck’ shouldn’t be a terminal state if inside the goal, just terminal for the respective agent turn => continue to judge
l
c
Do you have the trajectory of this run?
l
I do but it has some data I can’t share
c
OK, I’m just curious if it’s the
file editor
tool definition, we saw that multiple times
For some reason, GLM has its own ‘opinion’ about the parameters to send to the tool. And it’s really curious that it chokes there, because I saw z.ai has evaluated GLM on OpenHands… 🤔
On a related, but different note: what do you think if we switch to Deepseek v4 Flash for the Slack agent? It seems, from what we see so far, that GLM is good as reviewer, but that’s a different automation than this one where we ask it random work
l
This was gpt terra codex acp on the failed one
aha 1
so it might be an intersection with acp actually @fresh-machine-37269 does that change your assessment?
👀 1
f
🤖 On it! View progress here: oss-agent-canvas.ngrok.dev/conversations/…
c
Just for clarity, I was referring to
oss-openhands
f
✅ Done! Yes — it does change the assessment, and meaningfully. The earlier GLM hypothesis (wrong params sent to the SDK's
file_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`:
Copy code
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).
l
Oh, I see