Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions agent/conversation_compression.py
Original file line number Diff line number Diff line change
Expand Up @@ -557,6 +557,26 @@ def _release_lock() -> None:
parent_session_id=old_session_id,
)
agent._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.
# See #48956 (originally #23530, reverted in #23813).
try:
_goal_meta_key_old = f"goal:{old_session_id}"
_goal_meta_key_new = f"goal:{agent.session_id}"
_goal_blob = agent._session_db.get_meta(_goal_meta_key_old)
if _goal_blob:
agent._session_db.set_meta(_goal_meta_key_new, _goal_blob)
logger.info(
"goal: forwarded standing goal from %s → %s on compression",
old_session_id, agent.session_id,
)
except Exception as exc:
logger.debug("goal forward on compression failed: %s", exc)
# Auto-number the title for the continuation session
if old_title:
try:
Expand Down
57 changes: 57 additions & 0 deletions tests/hermes_cli/test_goals.py
Original file line number Diff line number Diff line change
Expand Up @@ -737,3 +737,60 @@ def test_status_line_with_subgoals(self, hermes_home):
mgr.add_subgoal("b")
line = mgr.status_line()
assert "2 subgoals" in line


# ──────────────────────────────────────────────────────────────────────
# Compression session-rotation: goal must follow the new session_id
# See #48956 (originally #23530, reverted in #23813, re-added here).
# ──────────────────────────────────────────────────────────────────────


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 the
_compress_context block in conversation_compression.py).

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 _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/subgoals preserved verbatim.
assert mgr2.state.turns_used == mgr.state.turns_used
assert mgr2.state.subgoals == mgr.state.subgoals

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