From 0d45416bb82198bfad6664ddcaed37e947a03d99 Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Sun, 24 May 2026 11:45:03 +0200 Subject: [PATCH] resume existing Claude sessions with --resume, not --session-id MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Confirmed root cause of the dead-pane bug: `claude --session-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 ` and only a brand-new one with `--session-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) --- .pql/changelog/ticket_history/2026-05.sql | 1 + .pql/changelog/tickets/2026-05.sql | 23 ++++++++++++++++ CHANGELOG.md | 5 ++++ lib/builtin/claude/src/claude_pane.dart | 29 +++++++++++--------- lib/builtin/claude/src/session_naming.dart | 6 ++++ test/builtin/claude/session_naming_test.dart | 12 ++++++++ 6 files changed, 63 insertions(+), 13 deletions(-) diff --git a/.pql/changelog/ticket_history/2026-05.sql b/.pql/changelog/ticket_history/2026-05.sql index 1a8ad9a0..bf026bfa 100644 --- a/.pql/changelog/ticket_history/2026-05.sql +++ b/.pql/changelog/ticket_history/2026-05.sql @@ -2219,3 +2219,4 @@ INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-141', 'status', 'in_progress', 'done', NULL, '2026-05-23 20:49:44', '2026-05-23 20:49:44', '2026-05-23 20:49:44', NULL, 'fb2405478b0d422d39a924ccedec5022', 1) ON CONFLICT(hash) DO NOTHING; INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-157', 'status', 'backlog', 'in_progress', NULL, '2026-05-23 20:52:47', '2026-05-23 20:52:47', '2026-05-23 20:52:47', NULL, 'b2b31ab5fe70fb5027773775b0945bd5', 1) ON CONFLICT(hash) DO NOTHING; INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-157', 'status', 'in_progress', 'done', NULL, '2026-05-23 20:57:07', '2026-05-23 20:57:07', '2026-05-23 20:57:07', NULL, 'ce114090979d31512a9aa09f1c9311a1', 1) ON CONFLICT(hash) DO NOTHING; +INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-159', 'status', 'backlog', 'done', NULL, '2026-05-24 08:54:30', '2026-05-24 08:54:30', '2026-05-24 08:54:30', NULL, '478956f24d051f603aab2c7ceec328b2', 1) ON CONFLICT(hash) DO NOTHING; diff --git a/.pql/changelog/tickets/2026-05.sql b/.pql/changelog/tickets/2026-05.sql index 50d7a4ad..7394e718 100644 --- a/.pql/changelog/tickets/2026-05.sql +++ b/.pql/changelog/tickets/2026-05.sql @@ -2659,3 +2659,26 @@ Acceptance: roster matches list-panes/config; each row mirrors the member''s mod INSERT INTO tickets (id, type, parent_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-157', 'task', 'T-132', 'Team sidebar: live per-member status (mode + context)', 'Extend the Claude meta sidebar (T-141) roster rows with each teammate''s LIVE status — permission-mode badge (default/accept-edits/plan/bypass) and context-token usage — mirroring the per-session strip (T-145/T-150). The data exists: TeamObserver creates a TranscriptPublisher per teammate whose statusStream emits SessionStatus, but it is NEVER published to the MessageBus. Wire it: in team_observer _joined, subscribe to the publisher''s statusStream and publish {agentId, model, permissionMode, contextTokens} to a new ClaudeConversation.memberStatusChannel; cancel on _left. The sidebar subscribes to that channel and updates the matching roster row. Do NOT re-tail transcripts (D-75) — reuse the existing stream. Acceptance: a running team shows each member''s mode badge + context tokens, updating live; module is the single status-forwarding point; tests; coverage >= floor.', 'in_progress', 'low', NULL, NULL, NULL, '2026-05-23 20:48:31', '2026-05-23 20:52:47', NULL, '386818f67d3cbcc1a1b7a528c2baaca1', 1) ON CONFLICT(id) DO UPDATE SET type=excluded.type, parent_id=excluded.parent_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash); INSERT INTO tickets (id, type, parent_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-157', 'task', 'T-132', 'Team sidebar: live per-member status (mode + context)', 'Extend the Claude meta sidebar (T-141) roster rows with each teammate''s LIVE status — permission-mode badge (default/accept-edits/plan/bypass) and context-token usage — mirroring the per-session strip (T-145/T-150). The data exists: TeamObserver creates a TranscriptPublisher per teammate whose statusStream emits SessionStatus, but it is NEVER published to the MessageBus. Wire it: in team_observer _joined, subscribe to the publisher''s statusStream and publish {agentId, model, permissionMode, contextTokens} to a new ClaudeConversation.memberStatusChannel; cancel on _left. The sidebar subscribes to that channel and updates the matching roster row. Do NOT re-tail transcripts (D-75) — reuse the existing stream. Acceptance: a running team shows each member''s mode badge + context tokens, updating live; module is the single status-forwarding point; tests; coverage >= floor.', 'done', 'low', NULL, NULL, NULL, '2026-05-23 20:48:31', '2026-05-23 20:57:07', NULL, '6fd17798d24f81a3f81f284c7bd595e2', 1) ON CONFLICT(id) DO UPDATE SET type=excluded.type, parent_id=excluded.parent_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash); INSERT INTO tickets (id, type, parent_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-159', 'bug', 'T-132', 'ClidePane convey notifies focus listeners during build (markNeedsBuild crash)', 'Found via make run (2026-05-24). ClidePane.didChangeDependencies/didUpdateWidget run during the build phase and call FocusTracker.setStatusWidget -> notifyListeners() synchronously, which rebuilds the focus-listening status-bar item (PaneContextStatusItem''s ListenableBuilder) mid-build -> ''setState()/markNeedsBuild() called during build'' (thrown repeatedly each frame). Introduced by T-150; the T-150 widget tests missed it because no focus listener was in their tree. Fix: ClidePane defers the convey to a post-frame callback when schedulerPhase == persistentCallbacks (mid-build), re-checking focus then; immediate otherwise. Regression test added with a PaneContextStatusItem in the tree while a ClidePane mounts focused.', 'backlog', 'high', NULL, NULL, NULL, '2026-05-24 08:53:13', '2026-05-24 08:53:13', NULL, '52caaaca78b7c2bf6cae907fcf0fcdae', 1) ON CONFLICT(id) DO UPDATE SET type=excluded.type, parent_id=excluded.parent_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash); +INSERT INTO tickets (id, type, parent_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-159', 'bug', 'T-132', 'ClidePane convey notifies focus listeners during build (markNeedsBuild crash)', 'Found via make run (2026-05-24). ClidePane.didChangeDependencies/didUpdateWidget run during the build phase and call FocusTracker.setStatusWidget -> notifyListeners() synchronously, which rebuilds the focus-listening status-bar item (PaneContextStatusItem''s ListenableBuilder) mid-build -> ''setState()/markNeedsBuild() called during build'' (thrown repeatedly each frame). Introduced by T-150; the T-150 widget tests missed it because no focus listener was in their tree. Fix: ClidePane defers the convey to a post-frame callback when schedulerPhase == persistentCallbacks (mid-build), re-checking focus then; immediate otherwise. Regression test added with a PaneContextStatusItem in the tree while a ClidePane mounts focused.', 'done', 'high', NULL, NULL, NULL, '2026-05-24 08:53:13', '2026-05-24 08:54:30', NULL, 'a2200e366f389c81f2e52e986715f9d1', 1) ON CONFLICT(id) DO UPDATE SET type=excluded.type, parent_id=excluded.parent_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash); +INSERT INTO tickets (id, type, parent_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-160', 'bug', 'T-132', 'Status bar overflows instead of marquee-scrolling the in-pane slot', 'Observed via make run (2026-05-24): the bottom status bar shows a RenderFlex ''overflowed by 77 PIXELS'' stripe after the in-pane context line (opus 4.7 · default · 21k ctx · 10 skills); the ClideMarquee never engages. + +Cause: StatusbarHost (lib/app.dart ~1143) lays items in a Row [left items..., Spacer(), right items...], each at INTRINSIC width with no Flexible. PaneContextStatusItem wraps its text in ConstrainedBox(maxWidth:360) + ClideMarquee, but because the text (~280px) is under 360 the slot takes content width, so the marquee''s viewport == its content and it never scrolls. T-154 appended ''· N skills'', lengthening the line enough that git-branch + context slot + right items exceed the bar width -> the Row overflows (~77px) instead of the slot shrinking + marquee scrolling. + +Fix direction: give the in-pane context slot a flexible/bounded width in the status bar (e.g. wrap it in Flexible(fit: loose), or have the bar allot it a shrinkable share) so it yields width when the bar is tight and ClideMarquee scrolls within the allotted width. Verify the interaction with the Spacer and the right-aligned items (priority>=100), and reconsider the fixed maxWidth:360. Acceptance: at narrow widths the in-pane slot marquee-scrolls and the status bar never shows an overflow stripe; widget test at a constrained width. Introduced by T-150 (marquee) + T-154 (longer line).', 'backlog', 'medium', NULL, NULL, NULL, '2026-05-24 09:13:42', '2026-05-24 09:13:42', NULL, 'd7c01b7742f730c2c7e2fb5473cdbefd', 1) ON CONFLICT(id) DO UPDATE SET type=excluded.type, parent_id=excluded.parent_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash); +INSERT INTO tickets (id, type, parent_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-161', 'bug', 'T-132', 'Typed input reaches no live Claude after respawn — pane renders transcript only', 'Observed via make run (2026-05-24). After using /clear and /resume (which appear to work), regular typed messages produce no response and never appear in the conversation stream. + +Diagnosis (live process inspection): the clide app (PID 77023) had ZERO child processes; ''tmux -L clide list-sessions'' reported ''no server running''. clide spawns pane processes (tmux client / claude) via posix_spawn as its own children, so no children + no -L clide server = NO live Claude backing the pane. The two stray ''claude'' procs were the user''s other sessions (an agent + a game-coding session), not clide''s. The conversation still renders because TranscriptReader tails the transcript FILE — so /clear and /resume only re-point the reader at a different file; there is no live session behind them. + +Leading hypothesis: T-156''s _respawnWithSession (/clear, /resume) kills the tmux session (killing the last session also kills the -L clide server) and the subsequent _spawn does not leave a durable session — but clide may still have _usingTmux=true and routes input via tmux.sendMessage to a now-dead session, where set-buffer/paste-buffer/send-keys silently no-op (tmuxRunner ignores non-zero exit). Net: keystrokes vanish. + +To localise: does a BRAND-NEW Claude tab (fresh spawn, no respawn) accept input and respond? If yes, the respawn path is the culprit; if no, the spawn path itself regressed. Also check the make-run console for pane.spawn errors / ''session exited''. + +Acceptance: after /clear and /resume, a live tmux session (or no-tmux PTY) exists and typed input reaches Claude and streams a response; clide''s _usingTmux/_paneId/_sessionName stay consistent with the actual spawned process; regression test or a documented manual verification. Relates to T-156, T-147 (self-heal), D-41.', 'backlog', 'high', NULL, NULL, NULL, '2026-05-24 09:24:02', '2026-05-24 09:24:02', NULL, '7ef055c3f51dffb9efe3e03c785a10be', 1) ON CONFLICT(id) DO UPDATE SET type=excluded.type, parent_id=excluded.parent_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash); +INSERT INTO tickets (id, type, parent_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-162', 'bug', 'T-132', 'Slash typeahead omits clide-owned commands (/resume)', '/resume works when typed but doesn''t appear in the composer slash typeahead. Cause: the typeahead sources its command list from activeClaudeConfig.slashCommands (the CLI stream-json probe), which does not include ''resume'' (it''s not an in-session slash command the CLI advertises; clide owns/intercepts it). /clear appears because ''clear'' IS in the probe list. Fix: union kClideOwnedCommands (slash_commands.dart) into the typeahead''s command source — e.g. in ClaudeComposer''s default slashCommandsResolver return {...?activeClaudeConfig?.slashCommands, ...kClideOwnedCommands}. Acceptance: typing /res suggests /resume; /clear still suggested; widget test asserting clide-owned commands appear even when absent from the probe list. Follow-up to T-152/T-156.', 'backlog', 'low', NULL, NULL, NULL, '2026-05-24 09:29:07', '2026-05-24 09:29:07', NULL, '403801e2c738160eac2e49e474eb30d0', 1) ON CONFLICT(id) DO UPDATE SET type=excluded.type, parent_id=excluded.parent_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash); +INSERT INTO tickets (id, type, parent_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-163', 'task', 'T-132', 'Composer prompt history via Up/Down (Claude-CLI-style)', 'Arrow Up/Down in the Claude composer should scroll through previously-sent prompts, matching the Claude Code CLI UX: +- Up: if the caret isn''t on the first visual line, move up one line (normal editing); once on the first line, Up steps to the previous prompt in history. Down mirrors it (move down within multiline; at the last line, step to the next/newer entry). +- When entering history, stash the current draft as the newest entry so Down past the most recent restores exactly what the user was typing. +- History is the sequence of prompts submitted from this composer (this session at least; persisting across restarts is a nice-to-have). +- Plays well with the existing Enter-submit / Shift+Enter-newline and the slash typeahead (Up/Down already drive the typeahead when it''s open — history nav only applies when the typeahead is closed). + +Implementation: a small history controller + caret-line detection in ClaudeComposer (EditableText); the Up/Down handling lives alongside the existing _onKey/Shortcuts. Acceptance: send a few prompts, Up walks back through them, Down returns through them to the restored draft; multiline prompts navigate line-by-line before crossing into history; widget tests for the bound conditions (first/last line) and draft stash/restore.', 'backlog', 'medium', NULL, NULL, NULL, '2026-05-24 09:42:44', '2026-05-24 09:42:44', NULL, 'c2d0a0a7c96dba20b920a2cc9d20d2e0', 1) ON CONFLICT(id) DO UPDATE SET type=excluded.type, parent_id=excluded.parent_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash); diff --git a/CHANGELOG.md b/CHANGELOG.md index f6ae72ed..6d13b9bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -145,6 +145,11 @@ heading, and (b) bumping `pubspec.yaml` `version:` in the same commit. ### Fixed +- Claude pane is no longer dead-on-arrival when resuming a session (T-161). + The primary pane (and `/resume`) relaunched Claude with `--session-id + `, which Claude rejects as "already in use" — so the pane had + no live backend and typed input vanished. clide now uses `--resume` for an + existing session and `--session-id` only for a brand-new one. - Claude pane no longer floods the console with "markNeedsBuild called during build" (T-159) — a focused pane surfacing its status-bar widget now defers the notification out of the build phase instead of rebuilding diff --git a/lib/builtin/claude/src/claude_pane.dart b/lib/builtin/claude/src/claude_pane.dart index c014d688..f24a5059 100644 --- a/lib/builtin/claude/src/claude_pane.dart +++ b/lib/builtin/claude/src/claude_pane.dart @@ -207,21 +207,26 @@ class _ClaudePaneState extends State { 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 ` 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-` 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-` 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 { '$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 { }); if (!resp.ok) { - argv = ['claude', '--session-id', _sessionId!]; + argv = launch; resp = await ipc.request('pane.spawn', args: { 'argv': argv, 'kind': PaneKind.claude.wire, diff --git a/lib/builtin/claude/src/session_naming.dart b/lib/builtin/claude/src/session_naming.dart index 5d9e0028..8c280ca1 100644 --- a/lib/builtin/claude/src/session_naming.dart +++ b/lib/builtin/claude/src/session_naming.dart @@ -68,6 +68,12 @@ String _hash(String s) { /// workspace re-binds the same `.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 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() { diff --git a/test/builtin/claude/session_naming_test.dart b/test/builtin/claude/session_naming_test.dart index 1df081fe..45e989d8 100644 --- a/test/builtin/claude/session_naming_test.dart +++ b/test/builtin/claude/session_naming_test.dart @@ -76,4 +76,16 @@ void main() { expect(a, isNot(b)); }); }); + + group('claudeLaunchArgs (T-161)', () { + test('resumes an existing session with --resume, not --session-id', () { + // --session-id refuses an existing id ("already in use"), so resuming + // (transcript on disk) must use --resume. + expect(claudeLaunchArgs('abc', resume: true), ['claude', '--resume', 'abc']); + }); + + test('creates a new session with --session-id', () { + expect(claudeLaunchArgs('abc', resume: false), ['claude', '--session-id', 'abc']); + }); + }); }