[BUG] Remote Control leaves bridge-session-only .jsonl stubs that show up in /resume as empty "(session)" entries
What's Wrong?
With Remote Control enabled, a 267-byte <uuid>.jsonl gets left in ~/.claude/projects/<project>/ containing only a single line:
{"type":"bridge-session","sessionId":"<uuid>","bridgeSessionId":"cse_…","lastSequenceNum":0,"ownerAccountUuid":"…","ownerOrganizationUuid":"…"}
It has no user or assistant messages. The /resume picker lists these as an untitled (session) entry showing "267 bytes" and no branch, usually at the top since it's the newest file. With remoteControlAtStartup: true, 68 of them built up across 14 projects.
Root cause (from reading the v2.1.300 bundle; minified names)
saveBridgeSession (Zit) is meant to never create a transcript for a session that doesn't have one yet. It only writes if the guard passes:
function Qwt(e,n,r){return Boolean(e.sessionFile)||r?.targetExists===!0&&n!==void 0}
The REPL bridge hook captures the session at bridge init: Ta=K(), Ql=xu() (the computed <Ta>.jsonl path), and ya=()=>Ta===K()?Ld()??Ql:Ql.
- Launch a fresh session with Remote Control on.
Ta= the fresh, not-yet-materialized session; nothing is written (guard fails, link kept in memory only). Correct. /resumeanother session. The session-change handler only sets the detached flag (case"resume": ho.current=ci!==Ta);Ta/Qlkeep pointing at the fresh session.- On exit (hook teardown), the non-clear branch calls
Zit(Ta, wi.bridgeSessionId, …, ya(), …, void 0, it). SinceTa!==K(),ya()returnsQl, the never-materialized fresh session's path. The guard checksDi().sessionFile, which is now the resumed session's file (truthy), so it passes andXTcreates<Ta>.jsonlwith only thebridge-sessionline.
So the guard checks the current session's file but the write targets a different session's path.
The /clear path in the same hook already handles this correctly, using the old session's real file and skipping when it doesn't exist:
let fi=wi===Ta?Ld():Ri; if(fi!==null){ ... F7(Ta,fi,{targetExists:!0},it) ... }
This also explains why each stub's mtime matches the resumed session's exit-time cost-state/last-prompt writes to the millisecond.
What Should Happen?
The teardown path should resolve the old session's real file the same way the /clear path does (or append with onlyIfExists), so no transcript is created for a session that never had one. Optionally, the /resume picker could also skip transcripts that contain no conversation messages, which would hide the stubs already on disk.
Steps to Reproduce
- Set
"remoteControlAtStartup": true(or run/rcimmediately in a fresh session). - Without sending a message,
/resumean existing session. - Exit Claude Code.
- Run
/resumeagain: an empty(session)entry appears for the original fresh session ID; its.jsonlcontains only thebridge-sessionline.
(Steps derived from the code path above and the on-disk evidence.)
Environment
- Claude Code v2.1.300
- Windows 11