Skip to content

fix(tui): persist interrupted session transcript on force-quit / SIGHUP - #50003

Merged
teknium1 merged 1 commit into
mainfrom
hermes/tui-session-persist
Jun 21, 2026
Merged

fix(tui): persist interrupted session transcript on force-quit / SIGHUP#50003
teknium1 merged 1 commit into
mainfrom
hermes/tui-session-persist

Conversation

@teknium1

Copy link
Copy Markdown
Contributor

Summary

TUI conversations survive force-quit (double Ctrl-C), terminal close, and SIGHUP — the interrupted in-flight turn is now flushed to SQLite before the session is finalized.

Previously tui_gateway/server.py:_finalize_session() only ran commit_memory_session + end_session() (marks the row ended, writes no transcript). A force-quit while the agent was mid-turn lost the conversation. This mirrors the CLI fix for the same bug class.

Changes

  • tui_gateway/server.py: _finalize_session() now persists agent._session_messages (falling back to session["history"]) via _persist_session() before close, and fires the on_session_end(interrupted=True) plugin hook so crash-recovery plugins can flush. Empty-guarded.
  • tui_gateway/entry.py: _hard_exit() explicitly calls _shutdown_sessions() before the hard-exit timer fires, so a worker thread holding the GIL / stdout lock can't block atexit from finalizing.
  • tests/tui_gateway/test_finalize_session_persist.py: 11 tests (history persisted, _session_messages priority, memory commit preserved, no-agent/empty/missing-method/double-finalize guards).

Validation

Before After
force-quit / SIGHUP mid-turn transcript lost turn flushed to SQLite
  • Targeted suite: tests/tui_gateway/test_finalize_session_persist.py — 11/11 pass.
  • E2E (real AIAgent + SessionDB, isolated HERMES_HOME): turn-1 flushed → in-flight turn-2 → _finalize_session() → all 4 messages land in state.db.

Salvaged from #22231 by @bogerman1 (commit cherry-picked, authorship preserved).

Infographic

session-persistence-on-hard-exit

Mirror the CLI's exit-path behaviour in the TUI gateway so that
unpersisted conversation messages are flushed to state.db and the
on_session_end plugin hook fires before the session is closed.

Root cause: _finalize_session() only called db.end_session() to
mark the session row as ended, but did NOT flush in-memory messages
via _persist_session() or fire the on_session_end hook.  When the
user force-quit (double Ctrl-C, terminal-close, SIGHUP) while the
agent was mid-turn, messages accumulated since the last persist
point were silently lost.

Changes
-------
tui_gateway/server.py - _finalize_session():
  - Persist unflushed messages via agent._persist_session() before
    db.end_session(). Prefers agent._session_messages (set by the
    last _persist_session call inside run_conversation) over
    session['history'] (stale when agent is mid-turn).
  - Fire on_session_end(interrupted=True) plugin hook so crash-
    recovery plugins can flush buffers, matching cli.py behaviour.

tui_gateway/entry.py - _log_signal():
  - Explicitly call _shutdown_sessions() before sys.exit(0) in the
    SIGHUP/SIGTERM handler as belt-and-suspenders over atexit.

tests/tui_gateway/test_finalize_session_persist.py (new):
  - 11 tests covering: history persistence, _session_messages
    priority, empty-history skip, missing-agent, double-finalize,
    persist-exception resilience, hook firing, hook-exception
    resilience, and db.end_session preservation.

Related
-------
Closes the TUI half of #5021 (CLI already handles this via its
atexit handler).  Also addresses the session-persistence gap
discussed in #18465 and #18269.
@github-actions

Copy link
Copy Markdown
Contributor

🔎 Lint report: hermes/tui-session-persist vs origin/main

ruff

Total: 0 on HEAD, 0 on base (➖ 0)

🆕 New issues: none

✅ Fixed issues: none

Unchanged: 0 pre-existing issues carried over.

ty (type checker)

Total: 11282 on HEAD, 11283 on base (✅ -1)

🆕 New issues (2):

Rule Count
unresolved-import 1
invalid-assignment 1
First entries
tests/tui_gateway/test_finalize_session_persist.py:18: [unresolved-import] unresolved-import: Cannot resolve imported module `pytest`
tests/run_agent/test_credits_notices_toggle.py:76: [invalid-assignment] invalid-assignment: Object of type `None` is not assignable to attribute `_credits_session_start_micros` of type `int`

✅ Fixed issues (2):

Rule Count
unresolved-attribute 2
First entries
run_agent.py:2984: [unresolved-attribute] unresolved-attribute: Object of type `Self@get_credits_spent_micros` has no attribute `_credits_session_start_micros`
tests/run_agent/test_credits_notices_toggle.py:76: [unresolved-attribute] unresolved-attribute: Unresolved attribute `_credits_session_start_micros` on type `AIAgent`

Unchanged: 5914 pre-existing issues carried over.

Diagnostics are surfaced as warnings — this check never fails the build.

@alt-glitch alt-glitch added type/bug Something isn't working comp/tui Terminal UI (ui-tui/ + tui_gateway/) P1 High — major feature broken, no workaround labels Jun 21, 2026
@alt-glitch

Copy link
Copy Markdown
Collaborator

Related: #22231 (the salvaged community PR, authorship preserved) and the same session-persist-on-hard-exit bug class. CLI counterpart: #50004.

@teknium1
teknium1 merged commit c7e8854 into main Jun 21, 2026
35 checks passed
@teknium1
teknium1 deleted the hermes/tui-session-persist branch June 21, 2026 14:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/tui Terminal UI (ui-tui/ + tui_gateway/) P1 High — major feature broken, no workaround type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants