fix(tui): persist interrupted session transcript on force-quit / SIGHUP - #50003
Merged
Conversation
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.
Contributor
🔎 Lint report:
|
| 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.
Collaborator
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 rancommit_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 persistsagent._session_messages(falling back tosession["history"]) via_persist_session()before close, and fires theon_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_messagespriority, memory commit preserved, no-agent/empty/missing-method/double-finalize guards).Validation
tests/tui_gateway/test_finalize_session_persist.py— 11/11 pass.AIAgent+SessionDB, isolatedHERMES_HOME): turn-1 flushed → in-flight turn-2 →_finalize_session()→ all 4 messages land instate.db.Salvaged from #22231 by @bogerman1 (commit cherry-picked, authorship preserved).
Infographic