fix(state): resolve compression chain tip in resolve_resume_session_id

After context compression, the parent session holds pre-compression messages
and a child (or deeper descendant) holds the continuation.
resolve_resume_session_id() short-circuited when the input session already
had messages (row is not None -> return session_id), causing REST API
endpoints, gateway resume, and CLI resume to serve stale parent messages.

Remove the early-return. Walk the full descendant chain, record the
deepest node that has messages (best), and return best if not None
else the original session_id (preserving the empty-chain fallback).

Callers (api_server.py, web_server.py, cli_agent_setup_mixin.py,
cli_commands_mixin.py) all use the resolved != input -> redirect pattern
and are transparent to this change.
This commit is contained in:
PINKIIILQWQ 2026-06-12 23:52:59 +08:00 committed by Teknium
parent 208f0d7c3b
commit abd6b85200
2 changed files with 47 additions and 28 deletions

View file

@ -3119,9 +3119,14 @@ class SessionDB:
it before compression. See #15000.
This helper walks ``parent_session_id`` forward from ``session_id`` and
returns the first descendant in the chain that has at least one message
row. If the original session already has messages, or no descendant
has any, the original ``session_id`` is returned unchanged.
returns the descendant in the chain that has the **most recent** messages.
Unlike the original logic, it does NOT short-circuit when the starting
session already has messages a descendant that was created by
compression may hold the continuation content and should be preferred
by the WebUI and gateway for ``--resume`` and session loading.
If no descendant (including the starting session) has any messages,
the original ``session_id`` is returned unchanged.
The chain is always walked via the child whose ``started_at`` is
latest; that matches the single-chain shape that compression creates.
@ -3149,22 +3154,23 @@ class SessionDB:
session_id = tip
with self._lock:
# If this session already has messages, nothing to redirect.
try:
row = self._conn.execute(
"SELECT 1 FROM messages WHERE session_id = ? LIMIT 1",
(session_id,),
).fetchone()
except Exception:
return session_id
if row is not None:
return session_id
# Walk descendants: at each step, pick the most-recently-started
# child session; stop once we find one with messages.
current = session_id
seen = {current}
best = None # tracks the last (deepest) node with messages
for _ in range(32):
# Check if the current node has messages.
try:
row = self._conn.execute(
"SELECT 1 FROM messages WHERE session_id = ? LIMIT 1",
(current,),
).fetchone()
except Exception:
return session_id
if row is not None:
best = current
# Walk to the most-recently-started child.
try:
child_row = self._conn.execute(
"SELECT id FROM sessions "
@ -3175,22 +3181,14 @@ class SessionDB:
except Exception:
return session_id
if child_row is None:
return session_id
break
child_id = child_row["id"] if hasattr(child_row, "keys") else child_row[0]
if not child_id or child_id in seen:
return session_id
break
seen.add(child_id)
try:
msg_row = self._conn.execute(
"SELECT 1 FROM messages WHERE session_id = ? LIMIT 1",
(child_id,),
).fetchone()
except Exception:
return session_id
if msg_row is not None:
return child_id
current = child_id
return session_id
return best if best is not None else session_id
def get_messages_as_conversation(
self,

View file

@ -138,3 +138,24 @@ def test_prefers_most_recent_child_when_fork_exists(db):
])
db.append_message("newer_fork", role="user", content="x")
assert db.resolve_resume_session_id("parent") == "newer_fork"
def test_redirects_from_message_bearing_parent_to_child(db):
"""Fix for problem 2: parent has messages AND child also has messages.
After context compression the parent holds old messages but the child
is the active continuation session. resolve_resume_session_id should
prefer the latest descendant with messages, not short-circuit on the
parent.
"""
_make_chain(db, [
("original", None),
("continued", "original"),
])
# Both parent and child have messages
db.append_message("original", role="user", content="old msg")
db.append_message("original", role="assistant", content="old reply")
db.append_message("continued", role="user", content="new msg")
db.append_message("continued", role="assistant", content="new reply")
assert db.resolve_resume_session_id("original") == "continued"