Skip to content

fix(gateway): label goal continuations and avoid silent queued turns - #58039

Open
lkz-de wants to merge 2 commits into
NousResearch:mainfrom
lkz-de:lkz/goal-continuation-queue-notice
Open

fix(gateway): label goal continuations and avoid silent queued turns#58039
lkz-de wants to merge 2 commits into
NousResearch:mainfrom
lkz-de:lkz/goal-continuation-queue-notice

Conversation

@lkz-de

@lkz-de lkz-de commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Summary

This PR makes autonomous /goal follow-up turns clearer to users and avoids a silent edge case when interrupted turns already have queued follow-ups pending.

Problem

Two user-facing problems combined to create a misleading failure mode in chat platforms:

  1. /goal continuation turns were injected with the generic marker [Continuing toward your standing goal], which was easy to confuse with a normal reply.
  2. If a turn was interrupted while follow-up work was already queued, Hermes could return an empty response for the active chat.

That combination made unrelated autonomous follow-up activity in other chats look like a wrong-chat delivery, even when the underlying session/chat routing was correct.

What changed

  • centralize the synthetic goal-continuation marker in hermes_cli.goals.CONTINUATION_MARKER
  • update all standing-goal continuation templates to use the new explicit marker:
    • [Automated goal continuation — Hermes is pursuing your standing goal, not a message you sent]
  • update GatewayRunner._is_goal_continuation_event() to detect synthetic continuation turns via the shared marker instead of a duplicated hardcoded string
  • add GatewayRunner._queue_notice() so queued-turn notices are formatted consistently
  • when a turn is interrupted and queued follow-ups already exist, return a visible queue notice instead of an empty response in both the interrupted-empty-response path and the interrupt-depth-cap fallback
  • update the English and zh-Hans /goal docs to match the new continuation label

Why this is safe

This does not change which chat a message is routed to.
It only:

  • makes autonomous continuation turns self-identifying
  • prevents interrupted queued turns from going silent
  • keeps /goal pause / /goal clear cleanup aligned with the current continuation templates via one shared marker

Tests

Ran:

uv run --extra dev pytest -q tests/gateway/test_gateway_silence_tokens.py tests/gateway/test_goal_status_notice.py tests/cli/test_cli_goal_interrupt.py -k 'goal or queue or silence or interrupt'

Result:

  • 18 passed

Overlap check

Checked before proposing this PR:

  • open PRs: found related but non-overlapping PR fix(goals): fire goal judge after streamed turns (Ralph loop stuck at turns_used=0) #54222 (fix(goals): fire goal judge after streamed turns (Ralph loop stuck at turns_used=0)) — that PR fixes judge execution for streamed /goal turns, while this PR focuses on continuation labeling and queue-notice behavior after interruption
  • open issues: no directly overlapping open issue found via gh search issues '"standing goal" continuation' --repo NousResearch/hermes-agent
  • current main / merged history: /goal and queue-related work exists upstream, but no current-main change found that already adds this explicit continuation marker or this queue-notice-on-silent-interrupt behavior

Notes

The motivating report initially looked like a wrong-chat routing bug on Signal. After tracing logs/session state, the better explanation was:

  • the active chat hit an interrupted/silent queued-turn edge case
  • autonomous standing-goal work in other chats still emitted their own follow-up messages

So this PR intentionally fixes the user-visible ambiguity and silence without overclaiming a routing-layer fix.

@alt-glitch alt-glitch added type/bug Something isn't working comp/gateway Gateway runner, session dispatch, delivery comp/cli CLI entry point, hermes_cli/, setup wizard sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages P2 Medium — degraded but workaround exists labels Jul 4, 2026
@Victornovikov

Copy link
Copy Markdown

Confirmed on Telegram (v0.18.0). When the goal loop stalls there is no indication. Even when continuations do enqueue, the synthetic message looks like a normal reply. The explicit marker and queue notice here would help surface the problem.

@teknium1 teknium1 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks for isolating the continuation-labeling and interrupted-turn UX issues.

Problems

  • The new depth-cap notice can be false. Current gateway/run.py:20076-20087 only retains a full pending_event; a plain pending interrupt string calls adapter.queue_message(), but a repository-wide search found no adapter implementation. The added fallback notice can therefore say “Queued for the next turn” after that string was dropped. The new test in tests/gateway/test_gateway_silence_tokens.py stubs _run_agent, so it does not execute this branch.

Suggested changes

  • Requeue plain pending text as a MessageEvent through _enqueue_fifo before returning the notice, and add a depth-cap regression test for an adapter with _pending_messages and no queue_message. The focused open PR #61027 implements this retained-event/plain-text distinction.

The shared continuation marker and updated docs are otherwise aligned with the current goal templates in hermes_cli/goals.py:71-108 and cleanup predicate in gateway/run.py:4495-4503.

Automated hermes-sweeper review.

Comment thread gateway/run.py
@@ -19025,7 +19048,21 @@ def _stream_confirmed_final_delivery(
merge_pending_message_event(adapter._pending_messages, session_key, pending_event)
elif adapter and hasattr(adapter, 'queue_message'):
adapter.queue_message(session_key, pending)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This notice can be false for the plain pending fallback: the unchanged branch above calls adapter.queue_message(session_key, pending), but normal adapters only expose _pending_messages, so no event is stored and _queue_depth() can be zero. Convert plain pending text to a MessageEvent and enqueue it via _enqueue_fifo before returning this notice.

@teknium1 teknium1 added the sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform label Jul 15, 2026
@lkz-de

lkz-de commented Jul 16, 2026

Copy link
Copy Markdown
Contributor Author

@Victornovikov thanks — that confirmation is useful.

That matches the problem this PR is trying to surface more clearly: when the goal loop stalls, the active chat can end up with no visible indication, and when a continuation does enqueue, the synthetic follow-up can still look like a normal reply instead of autonomous standing-goal work.

This PR tackles those two user-facing failure modes directly by making the continuation marker explicit and by returning a visible queue notice instead of a silent empty turn.

One caveat before asking for maintainer review: the automated review above is right that the depth-cap fallback notice can currently claim a message was queued when a plain pending string was actually dropped. I'll align this PR with the retained-event/plain-text distinction in #61027 (or rebase on it if it lands first) so the notice is only emitted when the message is genuinely requeued.

@lkz-de
lkz-de force-pushed the lkz/goal-continuation-queue-notice branch 2 times, most recently from e15a2c5 to 2fdd708 Compare July 28, 2026 03:05
@lkz-de

lkz-de commented Jul 28, 2026

Copy link
Copy Markdown
Contributor Author

Refreshed this branch onto current main and fixed the failing gateway test by deriving the expected continuation prefix from CONTINUATION_MARKER instead of duplicating the old literal string in the test.

Local verification:

  • python3 -m py_compile gateway/run.py hermes_cli/goals.py
  • scripts/run_tests.sh tests/cli/test_cli_goal_interrupt.py tests/gateway/test_gateway_silence_tokens.py tests/gateway/test_goal_status_notice.py tests/gateway/test_goal_continuation_drain.py -- --tb=short -q

@lkz-de
lkz-de force-pushed the lkz/goal-continuation-queue-notice branch 2 times, most recently from 7cf5e69 to eb68c3f Compare August 1, 2026 16:07
@lkz-de

lkz-de commented Aug 1, 2026

Copy link
Copy Markdown
Contributor Author

Rebased this onto current main at 470cf66b0 and pushed head eb68c3f6ad15f3ebc6f6bebb4ea400a222fdabc0.

Follow-up fix: the interrupt depth-cap fallback now only reports Queued for the next turn. when a follow-up actually survives in the durable queue. Previously the depth-cap path could emit that notice even when a plain pending string had no adapter _pending_messages slot and no queue_message path, so the follow-up was not actually queued. That zero-depth case now returns the fallback result without a queue notice and logs the reason.

Verification across the four touched test files:

  • python3 -m py_compile gateway/run.py tests/gateway/test_goal_status_notice.py
  • uv run --with pytest --with pytest-asyncio --with python-dotenv python -m pytest tests/gateway/test_goal_status_notice.py tests/gateway/test_goal_continuation_drain.py tests/gateway/test_gateway_silence_tokens.py tests/cli/test_cli_goal_interrupt.py -q → 18 passed

@lkz-de
lkz-de force-pushed the lkz/goal-continuation-queue-notice branch from eb68c3f to 4fd19e1 Compare August 10, 2026 11:29
@lkz-de

lkz-de commented Aug 10, 2026

Copy link
Copy Markdown
Contributor Author

Rebased this onto current main at 03fa32c92.

The only manual conflict was an append/append test collision in tests/gateway/test_gateway_silence_tokens.py; the refreshed branch keeps upstream's new hook metadata test and this PR's queued-follow-up notice regression. Current main had already absorbed the other silence-token coverage, so the branch now carries only the remaining goal-continuation/queued-turn delta.

Verification after the rebase:

scripts/run_tests.sh tests/gateway/test_gateway_silence_tokens.py tests/gateway/test_goal_continuation_drain.py tests/gateway/test_goal_status_notice.py tests/cli/test_cli_goal_interrupt.py -- --tb=short -q

Result: 19 tests passed, 0 failed.

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

Labels

comp/cli CLI entry point, hermes_cli/, setup wizard comp/gateway Gateway runner, session dispatch, delivery P2 Medium — degraded but workaround exists sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform 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