fix(goals): fire goal judge after streamed turns (Ralph loop stuck at turns_used=0) - #54222
Closed
Shinigallo wants to merge 1 commit into
Closed
Shinigallo wants to merge 1 commit into
Shinigallo wants to merge 1 commit into
Conversation
… turns_used=0) The gateway goal-continuation hook in _handle_message read the turn's final_response from _handle_message_with_agent's return value. When the reply is streamed (the default on Telegram), that inner handler returns None — the text was already delivered out-of-band — so _final_text was empty and the hook skipped _post_turn_goal_continuation entirely. Result: every streamed /goal stalled at turns_used=0; the goal_judge model never ran and no continuation was ever enqueued. Fix: - Always invoke the goal-continuation hook when a session resolves (cheap no-op when no goal is active), instead of gating on non-empty final text. - In _post_turn_goal_continuation, when final_response is empty, recover the turn's last assistant message from the persisted transcript via the new _recover_last_assistant_text() helper before judging; still skip genuinely empty (interrupted/errored) turns. Verified end-to-end over Telegram: turns_used advances (0 -> 4+), the qwen3.5:9b judge fires each turn, and [Continuing toward your standing goal] continuations are enqueued. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
tonydwb
reviewed
Jun 28, 2026
tonydwb
left a comment
There was a problem hiding this comment.
Fixes a real bug where streamed /goal loops stalled at turns_used=0 (68 additions). Adds transcript recovery to read the actual assistant response when final_response is empty. Well-commented with clear explanation of the failure mode. Clean fix.
Reviewed by Hermes Agent
|
Confirmed on v0.18.0 over Telegram. Goal state from state_meta: {"turns_used": 0, "last_verdict": null, "last_turn_at": 0.0}Judge never fires. Streaming returns The manual "send continue" workaround is not a fix — it defeats the entire point of an autonomous loop. |
This was referenced Jul 5, 2026
7 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
When a
/goalis set on a platform with streaming enabled (the default on Telegram), the Ralph loop never advances: the goal staysactiveatturns_used=0, thegoal_judgemodel is never called, and no[Continuing toward your standing goal]continuation is ever enqueued. The agent does the work for the first turn, but the loop silently stalls.This affects any streamed
/goal, not an edge case.Root cause
The goal-continuation hook in
_handle_messagereads the turn'sfinal_responsefrom the return value of_handle_message_with_agent:When the reply is streamed,
_handle_message_with_agentreturnsNone(the text was already delivered out-of-band via the stream consumer; the dict it would otherwise return isresponse, which ends upNoneon that path). So_final_textis empty, theif _final_text.strip():gate is false, and_post_turn_goal_continuation— the only place that calls the judge and incrementsturns_used— is never reached.Fix
gateway/run.py:final_response. It is a cheap no-op when no goal is active (_post_turn_goal_continuationreturns early onnot mgr.is_active())._post_turn_goal_continuation, whenfinal_responseis empty, recover the turn's last assistant message from the persisted transcript (SessionDB.get_messages) via a new_recover_last_assistant_text()helper before judging. Genuinely empty turns (interrupted / errored) are still skipped so the judge isn't asked to evaluate nothing.No behavior change for non-streamed turns or for non-goal messages.
Testing
Verified end-to-end over Telegram with a multi-turn
/goal:turns_usedadvances (was stuck at0, now0 → 4+)goal_judgemodel is loaded/called on every turn[Continuing toward your standing goal]continuations are enqueued between turnscontinuewhile incomplete, with sensible reasons)response_len)Non-goal messages and non-streamed goals are unaffected.
🤖 Generated with Claude Code