resume existing Claude sessions with --resume, not --session-id
test / unit + widget + golden + a11y (push) Failing after 32s
test / integration_test (xvfb) (push) Has been skipped
test / bundle smoke (xvfb 5s) (push) Has been skipped
test / daemon subprocess + web WASM smoke (push) Has been skipped
test / dart doc (lib API) (push) Failing after 32s

Confirmed root cause of the dead-pane bug: `claude --session-id <id>`
rejects an id that already exists ("Session ID … is already in use") and
exits. The primary pane uses a deterministic id to resume across restarts,
and /resume re-binds to an existing id — both relaunched with --session-id,
so whenever the tmux session wasn't already alive (clean boot, or after
/clear+/resume) Claude exited instantly and the pane had no live backend:
typed input vanished while the transcript still rendered. The pane now
launches an existing session (transcript on disk) with `--resume <id>` and
only a brand-new one with `--session-id <id>`. Fresh secondaries and /clear
(fresh ids) were always fine. Verified empirically against a live session.

T-161.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-24 11:45:03 +02:00
co-authored by Claude Opus 4.7
parent b8168d8ffe
commit 0d45416bb8
6 changed files with 63 additions and 13 deletions
+16 -13
View File
@@ -207,21 +207,26 @@ class _ClaudePaneState extends State<ClaudePane> {
final home = Platform.environment['HOME'] ?? '';
final transcriptFile = '$home/.claude/projects/${repoRoot.replaceAll('/', '-')}/$_sessionId.jsonl';
// A transcript already on disk means this session existed before, so we
// resume it; otherwise it's new. This drives both the self-heal kill and
// the launch flag — `claude --session-id <id>` REFUSES an existing id
// ("already in use"), so an existing session must launch with `--resume`
// (T-161).
final transcriptExists = await File(transcriptFile).exists();
// Self-heal (T-147): if no transcript is bound to our session id, any
// clide tmux session of this name is stale (created before --session-id
// clide tmux session of this name is stale (created before session-id
// binding, or otherwise unconnectable) and `new-session -A` would
// attach to it and leave the pane stuck waiting forever. Kill it so a
// clean session is created with our --session-id.
//
// Safe by construction: this only ever kills clide's OWN session — by
// its exact `clide-claude-<slug>` name, on the private `-L clide`
// socket the user's terminal claude never runs on — and never deletes
// any transcript file. A healthy session's transcript already exists,
// so re-attach (D-41 continuity) is preserved.
if (!await File(transcriptFile).exists()) {
// clean session is created. Safe by construction: only ever kills clide's
// OWN `clide-claude-<slug>` session on the private `-L clide` socket, and
// never deletes any transcript. A healthy session's transcript exists, so
// re-attach (D-41 continuity) is preserved.
if (!transcriptExists) {
await tmux.killSession(_sessionName!);
}
final launch = claudeLaunchArgs(_sessionId!, resume: transcriptExists);
final tmuxConf = await _ensureTmuxConf();
const cols = _cols;
const rows = _rows;
@@ -239,9 +244,7 @@ class _ClaudePaneState extends State<ClaudePane> {
'$cols',
'-y',
'$rows',
'claude',
'--session-id',
_sessionId!,
...launch,
];
// CLAUDE_CODE_NO_FLICKER=1 enables claude's fullscreen TUI mode:
@@ -260,7 +263,7 @@ class _ClaudePaneState extends State<ClaudePane> {
});
if (!resp.ok) {
argv = ['claude', '--session-id', _sessionId!];
argv = launch;
resp = await ipc.request('pane.spawn', args: {
'argv': argv,
'kind': PaneKind.claude.wire,
@@ -68,6 +68,12 @@ String _hash(String s) {
/// workspace re-binds the same `<uuid>.jsonl` across restarts (resume).
String primarySessionId(String repoRoot) => _deterministicUuid(primarySessionName(repoRoot));
/// The `claude` argv to launch [sessionId]: `--resume` an existing session,
/// or `--session-id` to create a new one. `--session-id` REFUSES an id that
/// already exists ("Session ID … is already in use") — so resuming a pane
/// whose transcript already exists must use `--resume` (T-161).
List<String> claudeLaunchArgs(String sessionId, {required bool resume}) => resume ? ['claude', '--resume', sessionId] : ['claude', '--session-id', sessionId];
/// A fresh random session id for a secondary pane — secondaries are
/// always clean sessions, never resumed.
String freshSessionId() {