mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-22 05:22:09 +00:00
fix(goals): forward standing /goal state on auto-compression session rotation (#23530)
When run_agent's _compress_context fires mid-turn it ends the parent
session in SessionDB and creates a new continuation session with a
fresh session_id. The /goal state is keyed on session_id in
state_meta ("goal:<sid>"), so without forwarding the goal silently
disappears: _get_goal_manager() rebinds for the new session_id,
load_goal() returns None, mgr.is_active() is False, and the
continuation loop dies with no user-visible signal.
Fix: in the same SessionDB transaction block that creates the
continuation session, copy state_meta[goal:<old>] →
state_meta[goal:<new>] when present. No-op when the user has no
active goal. Logged at INFO so a stuck loop is debuggable.
Tests cover the round-trip via SessionDB and the no-op path.
Affects all three run-conversation surfaces (CLI, gateway, TUI
gateway) because _compress_context is the single rotation site.
This commit is contained in:
parent
68d081f570
commit
4a080b1d5a
2 changed files with 75 additions and 0 deletions
19
run_agent.py
19
run_agent.py
|
|
@ -10080,6 +10080,25 @@ class AIAgent:
|
||||||
parent_session_id=old_session_id,
|
parent_session_id=old_session_id,
|
||||||
)
|
)
|
||||||
self._session_db_created = True
|
self._session_db_created = True
|
||||||
|
# Forward any standing /goal state from the parent session to
|
||||||
|
# the continuation session so the goal loop survives
|
||||||
|
# auto-compression. Without this rebind, _get_goal_manager()
|
||||||
|
# constructs a fresh manager keyed on the new session_id,
|
||||||
|
# load_goal() returns None, mgr.is_active() is False, and
|
||||||
|
# the loop silently dies mid-task. The goal is stored in
|
||||||
|
# state_meta under "goal:<sid>" by hermes_cli.goals.
|
||||||
|
try:
|
||||||
|
_goal_meta_key_old = f"goal:{old_session_id}"
|
||||||
|
_goal_meta_key_new = f"goal:{self.session_id}"
|
||||||
|
_goal_blob = self._session_db.get_meta(_goal_meta_key_old)
|
||||||
|
if _goal_blob:
|
||||||
|
self._session_db.set_meta(_goal_meta_key_new, _goal_blob)
|
||||||
|
logger.info(
|
||||||
|
"goal: forwarded standing goal from %s → %s on compression",
|
||||||
|
old_session_id, self.session_id,
|
||||||
|
)
|
||||||
|
except Exception as exc:
|
||||||
|
logger.debug("goal forward on compression failed: %s", exc)
|
||||||
# Auto-number the title for the continuation session
|
# Auto-number the title for the continuation session
|
||||||
if old_title:
|
if old_title:
|
||||||
try:
|
try:
|
||||||
|
|
|
||||||
|
|
@ -1071,3 +1071,59 @@ class TestJudgeIndexConversion:
|
||||||
# Other items still pending.
|
# Other items still pending.
|
||||||
assert mgr.state.checklist[1].status == ITEM_PENDING
|
assert mgr.state.checklist[1].status == ITEM_PENDING
|
||||||
assert mgr.state.checklist[2].status == ITEM_PENDING
|
assert mgr.state.checklist[2].status == ITEM_PENDING
|
||||||
|
|
||||||
|
|
||||||
|
# ──────────────────────────────────────────────────────────────────────
|
||||||
|
# Compression session-rotation: goal must follow the new session_id
|
||||||
|
# ──────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
class TestGoalSurvivesCompressionRotation:
|
||||||
|
def test_load_goal_after_session_id_rotates(self, hermes_home):
|
||||||
|
"""When auto-compression rotates the session_id, the goal must be
|
||||||
|
readable from the new session_id (forwarded by run_agent's
|
||||||
|
_compress_context block).
|
||||||
|
|
||||||
|
We don't run the full _compress_context method here — it has
|
||||||
|
~60 dependencies. Instead we mirror exactly what that block does
|
||||||
|
with state_meta and assert the goal manager picks it up.
|
||||||
|
"""
|
||||||
|
from hermes_cli.goals import GoalManager
|
||||||
|
from hermes_state import SessionDB
|
||||||
|
|
||||||
|
# Create a goal under a parent session_id.
|
||||||
|
parent_sid = "parent-rotate-001"
|
||||||
|
mgr = GoalManager(session_id=parent_sid)
|
||||||
|
mgr.set("survive compression")
|
||||||
|
assert mgr.is_active()
|
||||||
|
|
||||||
|
# Simulate the run_agent._compress_context forwarding block:
|
||||||
|
# read goal:<old>, write goal:<new> on the same SessionDB instance.
|
||||||
|
db = SessionDB()
|
||||||
|
new_sid = "child-rotate-001"
|
||||||
|
blob = db.get_meta(f"goal:{parent_sid}")
|
||||||
|
assert blob, "goal must be in state_meta"
|
||||||
|
db.set_meta(f"goal:{new_sid}", blob)
|
||||||
|
|
||||||
|
# New GoalManager for the rotated session_id should load the same goal.
|
||||||
|
mgr2 = GoalManager(session_id=new_sid)
|
||||||
|
assert mgr2.is_active()
|
||||||
|
assert mgr2.state.goal == "survive compression"
|
||||||
|
# Counters/checklist preserved verbatim.
|
||||||
|
assert mgr2.state.turns_used == mgr.state.turns_used
|
||||||
|
assert mgr2.state.checklist == mgr.state.checklist
|
||||||
|
|
||||||
|
def test_no_forward_when_no_goal(self, hermes_home):
|
||||||
|
"""Forwarding is a no-op when the parent session has no goal."""
|
||||||
|
from hermes_state import SessionDB
|
||||||
|
from hermes_cli.goals import load_goal
|
||||||
|
|
||||||
|
db = SessionDB()
|
||||||
|
# Parent has no goal at all.
|
||||||
|
assert db.get_meta("goal:parent-no-goal") is None
|
||||||
|
blob = db.get_meta("goal:parent-no-goal")
|
||||||
|
if blob: # parity with production guard
|
||||||
|
db.set_meta("goal:child-no-goal", blob)
|
||||||
|
|
||||||
|
# Child should still have no goal.
|
||||||
|
assert load_goal("child-no-goal") is None
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue