Repository navigation
fix(tui): /resume finds and resumes this workspace's saved sessions - #959
Merged
Merged
Conversation
The TUI's Sessions picker (/resume, /sessions, /switch) said "0 resumable" in every workspace. Three defects stacked: - gatewayClient answered session.list locally with an empty list (a "Phase 2" stub since #572). - session.resume was stubbed to return the current session, so resuming by id blanked the screen and kept the old conversation. - The backend's list_sessions returned an arbitrary slice. Session files carry updated_at as a float or an ISO string, and sorting the mix raised inside a catch-all. Backend: - list_sessions sorts on a normalized epoch (NaN and unreadable values fall back to mtime), scopes to the caller's cwd, excludes the live session, clamps the limit, and runs off the loop. - resume can return transcript rows (include_messages): prompts, replies and tool calls. Injected reminders, meta messages, tool results and reasoning are left out. - resume validates the id and resolves /resume <title> off the loop; a blank title matches nothing. - The resume reply carries the restored-tasks notice. - New delete_session control for the picker's `d d`. TUI: - session.list and session.resume ride those controls. Tool rows are summarized with the live toolContext, and notices are appended. - The model is refreshed after a resume. - The resume's session.stats fires after the picker's reset (the timer is armed after the last await). - gateway.ready fires once per spawned backend, so a resume that re-sends init can't forge a session over the repaint. - A failed resume with no live session falls back to a fresh one instead of leaving prompts queued. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Test Results 5 files 1 021 suites 21m 42s ⏱️ Results for commit 67c4398. |
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
After a reinstall,
/resumein the TUI showed the Sessions picker with "0 live · 0 resumable / no other sessions", even though the workspace had a saved 788-message session. Three separate defects stacked up to produce that.gatewayClientansweredsession.listlocally with an empty list, left as a "Phase 2" stub since Port hermes-agent TUI to clawcodex + Claude Code theme (fix Terminal.app input lag) #572./resume,/sessionsand/switchall open this picker, so it said "0 resumable" in every workspace.session.resumewas stubbed to return the current session./resume <id>, or picking a row, blanked the screen and kept the current conversation. Startup resume and crash recovery go through the same stub.updated_ateither as a float (the agent-server writer, 3,540 files on the reporting user's machine) or as an ISO string (the older writer, 1,017 files). Sorting that mix raisedTypeErrorinside a blanketexcept Exception: pass, solist_sessionsreturned files in directory order, truncated to the limit.Fix
Backend (
agent_server.py)list_sessionssorts on a normalized epoch. Floats are used as-is and ISO strings are parsed. Anything unreadable or non-finite (NaN would also break the JSON reply) falls back to the file's mtime.cwd) and leaves out the live session.limitto 1–200.resumewithinclude_messagesreturns the conversation as the TUI's transcript rows: prompts, replies, and tool calls.<system-reminder>prompts (the same rows the web client hides), meta messages, compact summaries, tool results and reasoning items.resumechecks that the id is a plain file stem (no path pieces) before touching the disk.resumealso accepts a session title (/resume <title>), matched case-insensitively against this workspace's renamed sessions. The title lookup reads every saved session, so it runs off the event loop.delete_sessioncontrol (d din the picker). It reuses the web gateway's id-validated delete and refuses the live session.TUI
session.listnow asks the backend'slist_sessionsfor this workspace. A refused list shows its error instead of "0 resumable". The 1.5 s live-session poll stays local.session.resume <id>now calls the realresumecontrol:toolContextas live rows. Mode, goal and cron notices follow as system rows.session.statsfires after the picker'sresetSession(), whose contract is that the resume re-stamps the stats. The timer is armed after the lastawait, so a slowget_settingscan't reorder it.system/initpublishesgateway.readyonce per spawned backend. A resume that flips coordinator mode re-emits init, and a second ready reran startup and created a new session over the repainted transcript.sidstayed null and every prompt only queued.session.deleteis wired to the backend.A resume continues under the live session's id, the same as the web gateway.
Verification
Real TUI build and real agent-server in a PTY. The temp config holds one saved session for the workspace and one from another project.
main/resumepicker0 live · 0 resumable0 live · 1 resumable:ds_e2e_old today 4 msgs prove the multiple-of-four lemma(the other project's session is hidden)turns: 0❯ prove the multiple-of-four lemma/Read 1 file/⏺ The multiple-of-four case holds.,turns: 3turns: 3(5 of 5 runs)d don the rowOn the reporting user's real sessions dir, the workspace listing returns their 788-message session and a 10-message one.
Test plan
tests/server/test_session_resume_picker.py(12):gatewayClient.test.ts(10 new):toolContext, banner and model refreshget_settingsgateway.readyper backend, and a fresh one after a respawntest_headless_keeps_the_damped_wording…, also fails onmain. The blank-title test was added after that run started; its file passes 12/12.cursorDriftRegressionand twomodelPickertests are timing-sensitive; they failed only while another project's test run held the machine at load 27, and passed in isolation on this code (modelPicker 14/14 ×3, cursorDrift 4/4).Follow-ups (not in this PR)
~/.clawcodex/sessions(3,390 of the reporting user's 4,557). The picker is workspace-scoped, so they no longer show there, but each listing still parses them./new,/clearand the picker's "+ new" overwrite the previous conversation's file after the next turn, because the backend'sclearkeeps the session id. The picker's "+ new" also leaves the backend conversation in place while the screen goes blank (unchanged here). The fix is to save under a fresh id on clear, as Claude Code does.delete_sessiononly protects this process's own live session. A second TUI in the same workspace could delete the first one's live file. The web client's delete has no guard either.🤖 Generated with Claude Code