Skip to content

fix(goals): fire goal judge after streamed turns (Ralph loop stuck at turns_used=0) - #54222

Closed
Shinigallo wants to merge 1 commit into
NousResearch:mainfrom
Shinigallo:fix/goal-judge-streamed-turns
Closed

Shinigallo wants to merge 1 commit into
NousResearch:mainfrom
Shinigallo:fix/goal-judge-streamed-turns

Conversation

@Shinigallo

Copy link
Copy Markdown

Problem

When a /goal is set on a platform with streaming enabled (the default on Telegram), the Ralph loop never advances: the goal stays active at turns_used=0, the goal_judge model 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_message reads the turn's final_response from the return value of _handle_message_with_agent:

if _final_text.strip():
    ...
    await self._post_turn_goal_continuation(..., final_response=_final_text)

When the reply is streamed, _handle_message_with_agent returns None (the text was already delivered out-of-band via the stream consumer; the dict it would otherwise return is response, which ends up None on that path). So _final_text is empty, the if _final_text.strip(): gate is false, and _post_turn_goal_continuation — the only place that calls the judge and increments turns_used — is never reached.

Fix

gateway/run.py:

  1. Always invoke the goal-continuation hook when a session resolves, instead of gating on non-empty final_response. It is a cheap no-op when no goal is active (_post_turn_goal_continuation returns early on not mgr.is_active()).
  2. In _post_turn_goal_continuation, when final_response is 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_used advances (was stuck at 0, now 0 → 4+)
  • the goal_judge model is loaded/called on every turn
  • [Continuing toward your standing goal] continuations are enqueued between turns
  • judge verdicts are correct (continue while incomplete, with sensible reasons)
  • transcript recovery returns the exact streamed final response (confirmed against the logged response_len)

Non-goal messages and non-streamed goals are unaffected.

🤖 Generated with Claude Code

… 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>
@alt-glitch alt-glitch added type/bug Something isn't working comp/gateway Gateway runner, session dispatch, delivery sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages P2 Medium — degraded but workaround exists labels Jun 28, 2026

@tonydwb tonydwb left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

@Victornovikov

Copy link
Copy Markdown

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 None from _handle_message_with_agent, _final_text is empty, the strip() gate blocks the continuation hook entirely. First turn runs, then dead silence.

The manual "send continue" workaround is not a fix — it defeats the entire point of an autonomous loop. /goal is broken on every streaming gateway. Would be good to get this merged.

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

Labels

comp/gateway Gateway runner, session dispatch, delivery P2 Medium — degraded but workaround exists sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants