fix(cli): sync session_id after compression and preserve original end_reason (#12920)

After context compression (manual /compress or auto), run_agent's
_compress_context ends the current session and creates a new continuation
child session, mutating agent.session_id. The classic CLI held its own
self.session_id that never resynced, so /status showed the ended parent,
the exit-summary --resume hint pointed at a closed row, and any later
end_session() call (from /resume <other> or /branch) targeted the wrong
row AND overwrote the parent's 'compression' end_reason.

This only affected the classic prompt_toolkit CLI. The gateway path was
already fixed in PR #1160 (March 2026); --tui and ACP use different
session plumbing and were unaffected.

Changes:
- cli.py::_manual_compress — sync self.session_id from self.agent.session_id
  after _compress_context, clear _pending_title
- cli.py chat loop — same sync post-run_conversation for auto-compression
- cli.py hermes -q single-query mode — same sync so stderr session_id
  output points at the continuation
- hermes_state.py::end_session — guard UPDATE with 'ended_at IS NULL' so
  the first end_reason wins; reopen_session() remains the explicit
  escape hatch for re-ending a closed row

Tests:
- 3 new in tests/cli/test_manual_compress.py (split sync, no-op guard,
  pending_title behavior)
- 2 new in tests/test_hermes_state.py (preserve compression end_reason
  on double-end; reopen-then-re-end still works)

Closes #12483. Credits @steve5636 for the same-day bug report and
@dieutx for PR #3529 which proposed the CLI sync approach.
This commit is contained in:
Teknium 2026-04-20 01:48:20 -07:00 committed by GitHub
parent f23123e7b4
commit 8a6aa5882e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 140 additions and 2 deletions

View file

@ -383,10 +383,19 @@ class SessionDB:
return session_id
def end_session(self, session_id: str, end_reason: str) -> None:
"""Mark a session as ended."""
"""Mark a session as ended.
No-ops when the session is already ended. The first end_reason wins:
compression-split sessions must keep their ``end_reason = 'compression'``
record even if a later stale ``end_session()`` call (e.g. from a
desynced CLI session_id after ``/resume`` or ``/branch``) targets them
with a different reason. Use ``reopen_session()`` first if you
intentionally need to re-end a closed session with a new reason.
"""
def _do(conn):
conn.execute(
"UPDATE sessions SET ended_at = ?, end_reason = ? WHERE id = ?",
"UPDATE sessions SET ended_at = ?, end_reason = ? "
"WHERE id = ? AND ended_at IS NULL",
(time.time(), end_reason, session_id),
)
self._execute_write(_do)