fix(state): exclude delegate/branch/tool children from resume walk + reconcile salvaged fixes

Follow-up to the salvage of #45035 + #48682. The two PRs touched different
functions (resolve_resume_session_id vs get_compression_tip) but #45035's
descendant walk followed ANY parent_session_id child, so a delegate/subagent
child could hijack the resume target. Apply the same _branched_from /
_delegate_from / source!='tool' exclusion the rest of hermes_state.py uses,
so the resume walk only follows genuine compression continuations.

Also updates the unrealistic delegation test fixture to carry the real
_delegate_from marker, and updates 3 list_sessions_rich test mocks for the
order_by_last_active kwarg #48682 added.

AUTHOR_MAP: map PINKIIILQWQ + ailang323 salvage authors.
This commit is contained in:
teknium1 2026-06-25 16:02:09 -07:00 committed by Teknium
parent 6d9ca04574
commit 6dfb8326f5
4 changed files with 24 additions and 5 deletions

View file

@ -3198,11 +3198,19 @@ class SessionDB:
if row is not None:
best = current
# Walk to the most-recently-started child.
# Walk to the most-recently-started child — but skip explicit
# branch (`_branched_from`), delegate/subagent (`_delegate_from`),
# and tool children. They also carry a ``parent_session_id`` yet
# are NOT compression continuations; following them would hijack
# the resume target to an unrelated session (e.g. a subagent
# run). This mirrors the child-exclusion in ``get_compression_tip``.
try:
child_row = self._conn.execute(
"SELECT id FROM sessions "
"WHERE parent_session_id = ? "
" AND json_extract(COALESCE(model_config, '{}'), '$._branched_from') IS NULL "
" AND json_extract(COALESCE(model_config, '{}'), '$._delegate_from') IS NULL "
" AND COALESCE(source, '') != 'tool' "
"ORDER BY started_at DESC, id DESC LIMIT 1",
(current,),
).fetchone()

View file

@ -81,6 +81,9 @@ AUTHOR_MAP = {
"joaomarcosdias444@gmail.com": "JoaoMarcos44",
"286497132+srojk34@users.noreply.github.com": "srojk34",
"srojk34@users.noreply.github.com": "srojk34", # legacy prefix-less noreply (PR #50098 salvage; #38763)
"pinkiilqwq@users.noreply.github.com": "PINKIIILQWQ", # PR #45035 salvage (resume-to-tip; #38763)
"pink@PinkdeMacBook-Air.local": "PINKIIILQWQ", # PR #45035 local git identity (resume-to-tip; #38763)
"ailang323@163.com": "ailang323", # PR #48682 salvage (compression-tip predicate; #38763)
"59806492+sitkarev@users.noreply.github.com": "sitkarev",
"zheng@omegasys.eu": "omegazheng",
"220877172+james47kjv@users.noreply.github.com": "james47kjv",

View file

@ -116,7 +116,15 @@ def test_compression_tip_not_confused_with_delegation_child(db):
base = int(time.time()) - 10_000
db.create_session("conv", source="cli")
db.append_message("conv", role="user", content="parent turn")
db.create_session("subagent", source="cli", parent_session_id="conv")
# Real delegate/subagent sessions carry the `_delegate_from` marker
# (set in delegate_tool.py) — that marker, not timing, is what
# distinguishes them from a compression continuation.
db.create_session(
"subagent",
source="subagent",
parent_session_id="conv",
model_config={"_delegate_from": "conv"},
)
db.append_message("subagent", role="assistant", content="delegated work")
conn = db._conn
assert conn is not None

View file

@ -6100,7 +6100,7 @@ def test_session_most_recent_returns_first_non_denied(monkeypatch):
"""Drops `tool` rows like session.list does, returns the first hit."""
class _DB:
def list_sessions_rich(self, *, source=None, limit=200):
def list_sessions_rich(self, *, source=None, limit=200, order_by_last_active=False):
return [
{"id": "tool-1", "source": "tool", "title": "noise", "started_at": 100},
{"id": "tui-1", "source": "tui", "title": "real", "started_at": 99},
@ -6119,7 +6119,7 @@ def test_session_most_recent_returns_first_non_denied(monkeypatch):
def test_session_most_recent_returns_null_when_only_tool_rows(monkeypatch):
class _DB:
def list_sessions_rich(self, *, source=None, limit=200):
def list_sessions_rich(self, *, source=None, limit=200, order_by_last_active=False):
return [{"id": "tool-1", "source": "tool", "started_at": 1}]
monkeypatch.setattr(server, "_get_db", lambda: _DB())
@ -6137,7 +6137,7 @@ def test_session_most_recent_folds_db_exception_into_null_result(monkeypatch):
'no answer' (Copilot review on #17130)."""
class _BrokenDB:
def list_sessions_rich(self, *, source=None, limit=200):
def list_sessions_rich(self, *, source=None, limit=200, order_by_last_active=False):
raise RuntimeError("db locked")
monkeypatch.setattr(server, "_get_db", lambda: _BrokenDB())