diff --git a/.pql/changelog/ticket_history/2026-06.sql b/.pql/changelog/ticket_history/2026-06.sql index 97570c35..eb8f62dd 100644 --- a/.pql/changelog/ticket_history/2026-06.sql +++ b/.pql/changelog/ticket_history/2026-06.sql @@ -5204,3 +5204,125 @@ Beyond the `/clear` fix, T-437 also owns bringing clide''s CLI characterization - Audit other session-lifecycle assumptions that may have drifted with the registry addition (resume/fork id handling, `--session-id` vs `--resume` selection). **Acceptance (updated):** `/clear` clears the primary pane to an empty conversation on CLI 2.1.177 without exiting; `claude` stderr/exit reason is surfaced in the pane + logs; init cache + routing table refreshed for 2.1.177; the `sessions/` registry lifecycle documented in this ticket (or a D-record if it changes a decision).', NULL, '2026-06-15 11:30:26', '2026-06-15 11:30:26', '2026-06-15 11:30:26', NULL, '2ab7f67318159b7b9a8b26eeefa2ed8d', 2) ON CONFLICT(hash) DO NOTHING; +INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FCNYXXH5AAHZR7WV0550J3RC', 'status', 'backlog', 'in_progress', NULL, '2026-06-15 11:38:50', '2026-06-15 11:38:50', '2026-06-15 11:38:50', NULL, 'c0d2e8bddc695f3cc0afdfd728aed3a6', 2) ON CONFLICT(hash) DO NOTHING; +INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FCNYXXH5AAHZR7WV0550J3RC', 'status', 'in_progress', 'in_progress', NULL, '2026-06-15 11:40:04', '2026-06-15 11:40:04', '2026-06-15 11:40:04', NULL, 'd858bb1bd89be62a0d00d564229a2517', 2) ON CONFLICT(hash) DO NOTHING; +INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FCNYXXH5AAHZR7WV0550J3RC', 'description', '**Symptom.** Typing `/clear` in the primary Claude pane kills the session: the pane shows `claude exited (code 1) — /clear to restart` and the "Warming up — your conversation will appear here." banner, instead of clearing to a fresh empty conversation. Screenshot taken in workspace `/var/mnt/data/projects/settled-reach`. + +**Environment.** `claude` CLI **2.1.177** (`~/.local/bin/claude`). The codebase''s init-probe cache and the last probe task only cover ≤ **2.1.175** — the CLI has moved past what clide last characterized. This is almost certainly a CLI-version regression, not a clide code change. + +**`/clear` code path (read-only trace, 2026-06-15):** +- `lib/builtin/claude/src/slash_commands.dart:42` — `clear` is in `kClideOwnedCommands`; clide handles it natively (T-156), does not forward it. +- `lib/builtin/claude/src/claude_pane.dart:651-659` — `_clearSession()`: primary pane → `_respawnWithSession(primarySessionId(root), clearTranscript: true)`. +- `claude_pane.dart:680-703` — `_respawnWithSession()`: kills the old session (`activeSessionOrchestrator.close(_orchId)`), then `clearSessionTranscript(...)`, then `_spawn()`. +- `lib/builtin/claude/src/session_naming.dart:109-114` — `clearSessionTranscript()` deletes ONLY `~/.claude/projects//.jsonl` and the sidecar `/` dir. +- `session_naming.dart:75` — respawn uses `claudeLaunchArgs(id, resume: false)` = `[''--session-id'', ]`, where `` is the **same deterministic `primarySessionId(root)`** (derived from the repo path, `session_naming.dart:68`). +- `session_naming.dart:72-74` (the load-bearing comment): "`--session-id` REFUSES an id that already exists (''Session ID … is already in use'')." + +**Root cause (strong hypothesis — see caveat).** T-268 built `/clear` on the assumption that the per-project transcript file (`.jsonl` + sidecar) is the *only* thing the CLI uses to decide whether a `--session-id` is "in use". The current CLI tracks session ids in **additional** state beyond that transcript: +- `~/.claude/sessions/.json` — a live-session registry. Confirmed contents (2.1.177): `{"pid":…,"sessionId":"e7dad3cf-…","cwd":"/var/mnt/data/projects/clide","version":"2.1.177","status":"idle",…}`. Keyed by PID, carries the clide session UUID + cwd. +- `~/.claude/history.jsonl` (2.8 MB, append-only). + +So `/clear` deletes the transcript and respawns with the *same deterministic* `--session-id`, but the killed session''s id is still registered (the registry entry is keyed by the now-dead PID and is the CLI''s own state — clide''s purge doesn''t touch it, and an abrupt kill leaves no chance for the CLI to clean it). The new `claude` rejects the id as already-in-use and **exits 1 at startup validation, before any model turn**. Reusing a fixed deterministic id is inherently brittle against the CLI adding new id-tracking surfaces. + +**Caveat — not yet pinned.** The actual `claude` stderr was NOT captured: the pane only surfaces `exited (code 1)`, swallowing the CLI''s error string. The above is inferred from (a) the documented `--session-id` refusal, (b) the confirmed new registry, (c) the version gap. It must be confirmed against the real stderr before the fix is chosen. **That clide shows an opaque "code 1" with no underlying reason is itself a defect** (see fix #3). + +**Fix directions (ranked):** +1. **Stop reusing a fixed `--session-id` on clear.** Mint a fresh id (as secondary panes already do) and persist it as the pane''s *current* primary id, decoupling "current primary session" from the path-derived default so cross-restart continuity (T-268''s goal) survives without colliding. Robust against any CLI-internal id tracking — the right long-term fix and aligns with D-75 (avoid version-pinned coupling to CC internals). +2. **Make the old id reusable before respawn:** graceful-shutdown the old process (SIGTERM, give the CLI a chance to clean its `sessions/.json`) and/or sweep `~/.claude/sessions/*.json` for entries whose `sessionId == target` before reuse. Fragile — reaches into CLI private state; D-75 caution. +3. **Surface `claude`''s stderr/exit reason in the pane** (and logs) so "code 1" is never opaque again. Do this regardless of 1/2 — it''s what makes this diagnosable. + +**Also:** re-probe the CLI for 2.1.177 and refresh the init cache / the slash-command routing table (T-410/T-411 territory) — the `~/.claude/sessions/` registry is new behavior worth characterizing; other session-lifecycle assumptions may have shifted too. + +**Diagnostic step for the fixer:** +1. Reproduce `/clear` in a primary pane with CLI 2.1.177. +2. Capture the spawned `claude`''s stderr/stdout on the failed respawn (the `--session-id ` invocation). Confirm whether it is "Session ID … is already in use" vs another error. +3. Inspect `~/.claude/sessions/*.json` immediately after the kill — does an entry with the target `sessionId` linger? + +**Files a fix would touch:** `claude_pane.dart` (`_clearSession` 651-659, `_respawnWithSession` 680-703, exit-status handler ~423), `session_naming.dart` (`claudeLaunchArgs` 75, `clearSessionTranscript` 109-114, id derivation 68), `session_orchestrator.dart` (`_spawn` 219-283, `close`), `slash_commands.dart` (routing). + +**Related:** T-268 (done — built the delete-transcript-then-reuse-`--session-id` mechanism that just regressed), T-156 (clide-owned `/clear`), T-161 (`--resume` vs `--session-id` selection), D-77 (stream-json session model), D-75 (version-pinned coupling to CC internals — the risk this realizes). + +--- + +**Folded-in scope (2026-06-15): CLI 2.1.177 re-probe — part of this ticket, not a follow-up.** + +Beyond the `/clear` fix, T-437 also owns bringing clide''s CLI characterization up to the running version: +- Re-run the stream-json `initialize` probe against `claude` **2.1.177** and refresh the per-version init cache (`~/.config/clide/claude/init-.json`, T-151). The codebase was last probed at ≤ 2.1.175 (probe task + `kClideOwnedCommands`/routing assumptions). +- Refresh the slash-command routing table (T-410/T-411) from the new probe: re-confirm advertised vs TUI-only vs owned for 2.1.177, so nothing regresses to a raw harness error. +- Characterize the new `~/.claude/sessions/.json` live-session registry (fields, lifecycle: when written, updated, removed — esp. whether a killed PID''s entry is cleaned). This directly informs which `/clear` fix is viable. +- Audit other session-lifecycle assumptions that may have drifted with the registry addition (resume/fork id handling, `--session-id` vs `--resume` selection). + +**Acceptance (updated):** `/clear` clears the primary pane to an empty conversation on CLI 2.1.177 without exiting; `claude` stderr/exit reason is surfaced in the pane + logs; init cache + routing table refreshed for 2.1.177; the `sessions/` registry lifecycle documented in this ticket (or a D-record if it changes a decision).', '**Symptom.** Typing `/clear` in the primary Claude pane kills the session: the pane shows `claude exited (code 1) — /clear to restart` and the "Warming up — your conversation will appear here." banner, instead of clearing to a fresh empty conversation. Screenshot taken in workspace `/var/mnt/data/projects/settled-reach`. + +**Environment.** `claude` CLI **2.1.177** (`~/.local/bin/claude`). The codebase''s init-probe cache and the last probe task only cover ≤ **2.1.175** — the CLI has moved past what clide last characterized. This is almost certainly a CLI-version regression, not a clide code change. + +**`/clear` code path (read-only trace, 2026-06-15):** +- `lib/builtin/claude/src/slash_commands.dart:42` — `clear` is in `kClideOwnedCommands`; clide handles it natively (T-156), does not forward it. +- `lib/builtin/claude/src/claude_pane.dart:651-659` — `_clearSession()`: primary pane → `_respawnWithSession(primarySessionId(root), clearTranscript: true)`. +- `claude_pane.dart:680-703` — `_respawnWithSession()`: kills the old session (`activeSessionOrchestrator.close(_orchId)`), then `clearSessionTranscript(...)`, then `_spawn()`. +- `lib/builtin/claude/src/session_naming.dart:109-114` — `clearSessionTranscript()` deletes ONLY `~/.claude/projects//.jsonl` and the sidecar `/` dir. +- `session_naming.dart:75` — respawn uses `claudeLaunchArgs(id, resume: false)` = `[''--session-id'', ]`, where `` is the **same deterministic `primarySessionId(root)`** (derived from the repo path, `session_naming.dart:68`). +- `session_naming.dart:72-74` (the load-bearing comment): "`--session-id` REFUSES an id that already exists (''Session ID … is already in use'')." + +**Root cause (strong hypothesis — see caveat).** T-268 built `/clear` on the assumption that the per-project transcript file (`.jsonl` + sidecar) is the *only* thing the CLI uses to decide whether a `--session-id` is "in use". The current CLI tracks session ids in **additional** state beyond that transcript: +- `~/.claude/sessions/.json` — a live-session registry. Confirmed contents (2.1.177): `{"pid":…,"sessionId":"e7dad3cf-…","cwd":"/var/mnt/data/projects/clide","version":"2.1.177","status":"idle",…}`. Keyed by PID, carries the clide session UUID + cwd. +- `~/.claude/history.jsonl` (2.8 MB, append-only). + +So `/clear` deletes the transcript and respawns with the *same deterministic* `--session-id`, but the killed session''s id is still registered (the registry entry is keyed by the now-dead PID and is the CLI''s own state — clide''s purge doesn''t touch it, and an abrupt kill leaves no chance for the CLI to clean it). The new `claude` rejects the id as already-in-use and **exits 1 at startup validation, before any model turn**. Reusing a fixed deterministic id is inherently brittle against the CLI adding new id-tracking surfaces. + +**Caveat — not yet pinned.** The actual `claude` stderr was NOT captured: the pane only surfaces `exited (code 1)`, swallowing the CLI''s error string. The above is inferred from (a) the documented `--session-id` refusal, (b) the confirmed new registry, (c) the version gap. It must be confirmed against the real stderr before the fix is chosen. **That clide shows an opaque "code 1" with no underlying reason is itself a defect** (see fix #3). + +**Fix directions (ranked):** +1. **Stop reusing a fixed `--session-id` on clear.** Mint a fresh id (as secondary panes already do) and persist it as the pane''s *current* primary id, decoupling "current primary session" from the path-derived default so cross-restart continuity (T-268''s goal) survives without colliding. Robust against any CLI-internal id tracking — the right long-term fix and aligns with D-75 (avoid version-pinned coupling to CC internals). +2. **Make the old id reusable before respawn:** graceful-shutdown the old process (SIGTERM, give the CLI a chance to clean its `sessions/.json`) and/or sweep `~/.claude/sessions/*.json` for entries whose `sessionId == target` before reuse. Fragile — reaches into CLI private state; D-75 caution. +3. **Surface `claude`''s stderr/exit reason in the pane** (and logs) so "code 1" is never opaque again. Do this regardless of 1/2 — it''s what makes this diagnosable. + +**Also:** re-probe the CLI for 2.1.177 and refresh the init cache / the slash-command routing table (T-410/T-411 territory) — the `~/.claude/sessions/` registry is new behavior worth characterizing; other session-lifecycle assumptions may have shifted too. + +**Diagnostic step for the fixer:** +1. Reproduce `/clear` in a primary pane with CLI 2.1.177. +2. Capture the spawned `claude`''s stderr/stdout on the failed respawn (the `--session-id ` invocation). Confirm whether it is "Session ID … is already in use" vs another error. +3. Inspect `~/.claude/sessions/*.json` immediately after the kill — does an entry with the target `sessionId` linger? + +**Files a fix would touch:** `claude_pane.dart` (`_clearSession` 651-659, `_respawnWithSession` 680-703, exit-status handler ~423), `session_naming.dart` (`claudeLaunchArgs` 75, `clearSessionTranscript` 109-114, id derivation 68), `session_orchestrator.dart` (`_spawn` 219-283, `close`), `slash_commands.dart` (routing). + +**Related:** T-268 (done — built the delete-transcript-then-reuse-`--session-id` mechanism that just regressed), T-156 (clide-owned `/clear`), T-161 (`--resume` vs `--session-id` selection), D-77 (stream-json session model), D-75 (version-pinned coupling to CC internals — the risk this realizes). + +--- + +**Folded-in scope (2026-06-15): CLI 2.1.177 re-probe — part of this ticket, not a follow-up.** + +Beyond the `/clear` fix, T-437 also owns bringing clide''s CLI characterization up to the running version: +- Re-run the stream-json `initialize` probe against `claude` **2.1.177** and refresh the per-version init cache (`~/.config/clide/claude/init-.json`, T-151). The codebase was last probed at ≤ 2.1.175 (probe task + `kClideOwnedCommands`/routing assumptions). +- Refresh the slash-command routing table (T-410/T-411) from the new probe: re-confirm advertised vs TUI-only vs owned for 2.1.177, so nothing regresses to a raw harness error. +- Characterize the new `~/.claude/sessions/.json` live-session registry (fields, lifecycle: when written, updated, removed — esp. whether a killed PID''s entry is cleaned). This directly informs which `/clear` fix is viable. +- Audit other session-lifecycle assumptions that may have drifted with the registry addition (resume/fork id handling, `--session-id` vs `--resume` selection). + +**Acceptance (updated):** `/clear` clears the primary pane to an empty conversation on CLI 2.1.177 without exiting; `claude` stderr/exit reason is surfaced in the pane + logs; init cache + routing table refreshed for 2.1.177; the `sessions/` registry lifecycle documented in this ticket (or a D-record if it changes a decision). + +--- + +**RESOLUTION (2026-06-15) — root cause confirmed, fixed.** + +**Actual cause (confirmed against real stderr, NOT the original collision-vs-registry guess).** Captured from clide''s own crash log (`~/.local/state/clide/logs/clide.log`): +`session primary exited (code 1); stderr tail: Error: Session ID is already in use.` + +But the "in use" is a **lifecycle race**, not a persistent registry/transcript collision. Isolated probes against claude 2.1.177 proved: +- `--session-id` reuse SUCCEEDS once the prior holder is dead — even immediately after SIGTERM (the `~/.claude/sessions/.json` entry is cleaned on a SIGTERM exit), and even with another *live* duplicate in `-p` mode. +- A transcript-absent id never collides. +The only way real clide hit "already in use": the **old process was still alive** at respawn. `ClaudeSessionOrchestrator.close()` called `conversation.dispose()` (unawaited) and `ClaudeStreamJsonProcess.kill()` sent SIGTERM **without awaiting `exitCode`** — so `/clear` deleted the transcript and respawned on the same deterministic `--session-id` while the old `claude` was still alive (and re-flushing its transcript). 2.1.177 then rejects the id → exit 1. + +**Fix shipped (chose await-death over the ticket''s tentative "mint a fresh id").** Awaiting the old process''s real death is the targeted root-cause fix and *preserves T-268''s deterministic-id continuity* (fresh-id would have required persisting a per-repo current-id and changing the continuity model). Changes: +- `stream_json_session.dart`: `kill()` now awaits `exitCode` (SIGTERM → 2s → SIGKILL → await); `dispose()` is idempotent (cached future) so the conversation''s unawaited dispose and the orchestrator''s awaited one share one teardown; new `SessionEnd.reason` getter (last non-empty stderr line, capped). +- `session_orchestrator.dart`: `close()` awaits `session.dispose()` → returns only once the process is truly dead, so the transcript clear + respawn happen after death. +- `claude_pane.dart`: `_onSessionEnd` surfaces `end.reason` in the status line — no more opaque "code 1" (fix #3). +- `session_naming.dart`: corrected the stale `clearSessionTranscript` doc (the real sidecar is the shared `memory/` dir, not `/`; left untouched) + the await-death precondition. +- Tests: `close()` blocks until process exit (gated fake); `SessionEnd.reason` extraction/cap/empty. + +**Folded-in CLI 2.1.177 re-probe — done, no code change needed.** +- `~/.claude/sessions/.json` registry characterized: PID-keyed, carries `{sessionId, cwd, version, kind:"interactive", entrypoint:"sdk-cli", status}`; written at start, **removed on a SIGTERM/clean exit**. So once clide awaits death, the id is free. +- Init cache (`init-.json`) auto-refreshes at runtime per version — nothing to hand-edit. +- Probed 2.1.177 advertised `slash_commands` (31): no drift that breaks the routing table — none of clide''s `kTuiOnlyCommands` became advertised, and new entries (skills/builtins) correctly fall through to `forward`. + +**Verification status.** Unit-tested (the teardown ordering + reason surfacing) and validated against the live CLI via probes. `make test` green. NOT yet exercised in the running GUI (would need `make run` + an interactive `/clear`) — recommend a quick live confirm before closing.', NULL, '2026-06-15 13:30:11', '2026-06-15 13:30:11', '2026-06-15 13:30:11', NULL, 'ca1b8cc05faf1bf385494030cbc9eb2f', 2) ON CONFLICT(hash) DO NOTHING; +INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FCNYXXH5AAHZR7WV0550J3RC', 'status', 'in_progress', 'review', NULL, '2026-06-15 13:30:21', '2026-06-15 13:30:21', '2026-06-15 13:30:21', NULL, '2f16ac8313a7e627166ae842ba7c096c', 2) ON CONFLICT(hash) DO NOTHING; diff --git a/.pql/changelog/tickets/2026-06.sql b/.pql/changelog/tickets/2026-06.sql index fb9f1ccc..e68107d1 100644 --- a/.pql/changelog/tickets/2026-06.sql +++ b/.pql/changelog/tickets/2026-06.sql @@ -5776,3 +5776,243 @@ Beyond the `/clear` fix, T-437 also owns bringing clide''s CLI characterization - Audit other session-lifecycle assumptions that may have drifted with the registry addition (resume/fork id handling, `--session-id` vs `--resume` selection). **Acceptance (updated):** `/clear` clears the primary pane to an empty conversation on CLI 2.1.177 without exiting; `claude` stderr/exit reason is surfaced in the pane + logs; init cache + routing table refreshed for 2.1.177; the `sessions/` registry lifecycle documented in this ticket (or a D-record if it changes a decision).', 'backlog', 'high', NULL, NULL, 'D-77', '2026-06-15 11:12:36', '2026-06-15 11:30:26', NULL, 'e07a8de1900862cd38a0193aca4846e4', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_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 (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FCNYXXH5AAHZR7WV0550J3RC', 'bug', NULL, '/clear kills the Claude session (exit 1) — respawn reuses a --session-id the CLI still tracks (2.1.177 regression)', '**Symptom.** Typing `/clear` in the primary Claude pane kills the session: the pane shows `claude exited (code 1) — /clear to restart` and the "Warming up — your conversation will appear here." banner, instead of clearing to a fresh empty conversation. Screenshot taken in workspace `/var/mnt/data/projects/settled-reach`. + +**Environment.** `claude` CLI **2.1.177** (`~/.local/bin/claude`). The codebase''s init-probe cache and the last probe task only cover ≤ **2.1.175** — the CLI has moved past what clide last characterized. This is almost certainly a CLI-version regression, not a clide code change. + +**`/clear` code path (read-only trace, 2026-06-15):** +- `lib/builtin/claude/src/slash_commands.dart:42` — `clear` is in `kClideOwnedCommands`; clide handles it natively (T-156), does not forward it. +- `lib/builtin/claude/src/claude_pane.dart:651-659` — `_clearSession()`: primary pane → `_respawnWithSession(primarySessionId(root), clearTranscript: true)`. +- `claude_pane.dart:680-703` — `_respawnWithSession()`: kills the old session (`activeSessionOrchestrator.close(_orchId)`), then `clearSessionTranscript(...)`, then `_spawn()`. +- `lib/builtin/claude/src/session_naming.dart:109-114` — `clearSessionTranscript()` deletes ONLY `~/.claude/projects//.jsonl` and the sidecar `/` dir. +- `session_naming.dart:75` — respawn uses `claudeLaunchArgs(id, resume: false)` = `[''--session-id'', ]`, where `` is the **same deterministic `primarySessionId(root)`** (derived from the repo path, `session_naming.dart:68`). +- `session_naming.dart:72-74` (the load-bearing comment): "`--session-id` REFUSES an id that already exists (''Session ID … is already in use'')." + +**Root cause (strong hypothesis — see caveat).** T-268 built `/clear` on the assumption that the per-project transcript file (`.jsonl` + sidecar) is the *only* thing the CLI uses to decide whether a `--session-id` is "in use". The current CLI tracks session ids in **additional** state beyond that transcript: +- `~/.claude/sessions/.json` — a live-session registry. Confirmed contents (2.1.177): `{"pid":…,"sessionId":"e7dad3cf-…","cwd":"/var/mnt/data/projects/clide","version":"2.1.177","status":"idle",…}`. Keyed by PID, carries the clide session UUID + cwd. +- `~/.claude/history.jsonl` (2.8 MB, append-only). + +So `/clear` deletes the transcript and respawns with the *same deterministic* `--session-id`, but the killed session''s id is still registered (the registry entry is keyed by the now-dead PID and is the CLI''s own state — clide''s purge doesn''t touch it, and an abrupt kill leaves no chance for the CLI to clean it). The new `claude` rejects the id as already-in-use and **exits 1 at startup validation, before any model turn**. Reusing a fixed deterministic id is inherently brittle against the CLI adding new id-tracking surfaces. + +**Caveat — not yet pinned.** The actual `claude` stderr was NOT captured: the pane only surfaces `exited (code 1)`, swallowing the CLI''s error string. The above is inferred from (a) the documented `--session-id` refusal, (b) the confirmed new registry, (c) the version gap. It must be confirmed against the real stderr before the fix is chosen. **That clide shows an opaque "code 1" with no underlying reason is itself a defect** (see fix #3). + +**Fix directions (ranked):** +1. **Stop reusing a fixed `--session-id` on clear.** Mint a fresh id (as secondary panes already do) and persist it as the pane''s *current* primary id, decoupling "current primary session" from the path-derived default so cross-restart continuity (T-268''s goal) survives without colliding. Robust against any CLI-internal id tracking — the right long-term fix and aligns with D-75 (avoid version-pinned coupling to CC internals). +2. **Make the old id reusable before respawn:** graceful-shutdown the old process (SIGTERM, give the CLI a chance to clean its `sessions/.json`) and/or sweep `~/.claude/sessions/*.json` for entries whose `sessionId == target` before reuse. Fragile — reaches into CLI private state; D-75 caution. +3. **Surface `claude`''s stderr/exit reason in the pane** (and logs) so "code 1" is never opaque again. Do this regardless of 1/2 — it''s what makes this diagnosable. + +**Also:** re-probe the CLI for 2.1.177 and refresh the init cache / the slash-command routing table (T-410/T-411 territory) — the `~/.claude/sessions/` registry is new behavior worth characterizing; other session-lifecycle assumptions may have shifted too. + +**Diagnostic step for the fixer:** +1. Reproduce `/clear` in a primary pane with CLI 2.1.177. +2. Capture the spawned `claude`''s stderr/stdout on the failed respawn (the `--session-id ` invocation). Confirm whether it is "Session ID … is already in use" vs another error. +3. Inspect `~/.claude/sessions/*.json` immediately after the kill — does an entry with the target `sessionId` linger? + +**Files a fix would touch:** `claude_pane.dart` (`_clearSession` 651-659, `_respawnWithSession` 680-703, exit-status handler ~423), `session_naming.dart` (`claudeLaunchArgs` 75, `clearSessionTranscript` 109-114, id derivation 68), `session_orchestrator.dart` (`_spawn` 219-283, `close`), `slash_commands.dart` (routing). + +**Related:** T-268 (done — built the delete-transcript-then-reuse-`--session-id` mechanism that just regressed), T-156 (clide-owned `/clear`), T-161 (`--resume` vs `--session-id` selection), D-77 (stream-json session model), D-75 (version-pinned coupling to CC internals — the risk this realizes). + +--- + +**Folded-in scope (2026-06-15): CLI 2.1.177 re-probe — part of this ticket, not a follow-up.** + +Beyond the `/clear` fix, T-437 also owns bringing clide''s CLI characterization up to the running version: +- Re-run the stream-json `initialize` probe against `claude` **2.1.177** and refresh the per-version init cache (`~/.config/clide/claude/init-.json`, T-151). The codebase was last probed at ≤ 2.1.175 (probe task + `kClideOwnedCommands`/routing assumptions). +- Refresh the slash-command routing table (T-410/T-411) from the new probe: re-confirm advertised vs TUI-only vs owned for 2.1.177, so nothing regresses to a raw harness error. +- Characterize the new `~/.claude/sessions/.json` live-session registry (fields, lifecycle: when written, updated, removed — esp. whether a killed PID''s entry is cleaned). This directly informs which `/clear` fix is viable. +- Audit other session-lifecycle assumptions that may have drifted with the registry addition (resume/fork id handling, `--session-id` vs `--resume` selection). + +**Acceptance (updated):** `/clear` clears the primary pane to an empty conversation on CLI 2.1.177 without exiting; `claude` stderr/exit reason is surfaced in the pane + logs; init cache + routing table refreshed for 2.1.177; the `sessions/` registry lifecycle documented in this ticket (or a D-record if it changes a decision).', 'in_progress', 'high', NULL, NULL, 'D-77', '2026-06-15 11:12:36', '2026-06-15 11:38:50', NULL, '2749bd938060221306d1241ca188568c', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_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 (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FCNYXXH5AAHZR7WV0550J3RC', 'bug', NULL, '/clear kills the Claude session (exit 1) — respawn reuses a --session-id the CLI still tracks (2.1.177 regression)', '**Symptom.** Typing `/clear` in the primary Claude pane kills the session: the pane shows `claude exited (code 1) — /clear to restart` and the "Warming up — your conversation will appear here." banner, instead of clearing to a fresh empty conversation. Screenshot taken in workspace `/var/mnt/data/projects/settled-reach`. + +**Environment.** `claude` CLI **2.1.177** (`~/.local/bin/claude`). The codebase''s init-probe cache and the last probe task only cover ≤ **2.1.175** — the CLI has moved past what clide last characterized. This is almost certainly a CLI-version regression, not a clide code change. + +**`/clear` code path (read-only trace, 2026-06-15):** +- `lib/builtin/claude/src/slash_commands.dart:42` — `clear` is in `kClideOwnedCommands`; clide handles it natively (T-156), does not forward it. +- `lib/builtin/claude/src/claude_pane.dart:651-659` — `_clearSession()`: primary pane → `_respawnWithSession(primarySessionId(root), clearTranscript: true)`. +- `claude_pane.dart:680-703` — `_respawnWithSession()`: kills the old session (`activeSessionOrchestrator.close(_orchId)`), then `clearSessionTranscript(...)`, then `_spawn()`. +- `lib/builtin/claude/src/session_naming.dart:109-114` — `clearSessionTranscript()` deletes ONLY `~/.claude/projects//.jsonl` and the sidecar `/` dir. +- `session_naming.dart:75` — respawn uses `claudeLaunchArgs(id, resume: false)` = `[''--session-id'', ]`, where `` is the **same deterministic `primarySessionId(root)`** (derived from the repo path, `session_naming.dart:68`). +- `session_naming.dart:72-74` (the load-bearing comment): "`--session-id` REFUSES an id that already exists (''Session ID … is already in use'')." + +**Root cause (strong hypothesis — see caveat).** T-268 built `/clear` on the assumption that the per-project transcript file (`.jsonl` + sidecar) is the *only* thing the CLI uses to decide whether a `--session-id` is "in use". The current CLI tracks session ids in **additional** state beyond that transcript: +- `~/.claude/sessions/.json` — a live-session registry. Confirmed contents (2.1.177): `{"pid":…,"sessionId":"e7dad3cf-…","cwd":"/var/mnt/data/projects/clide","version":"2.1.177","status":"idle",…}`. Keyed by PID, carries the clide session UUID + cwd. +- `~/.claude/history.jsonl` (2.8 MB, append-only). + +So `/clear` deletes the transcript and respawns with the *same deterministic* `--session-id`, but the killed session''s id is still registered (the registry entry is keyed by the now-dead PID and is the CLI''s own state — clide''s purge doesn''t touch it, and an abrupt kill leaves no chance for the CLI to clean it). The new `claude` rejects the id as already-in-use and **exits 1 at startup validation, before any model turn**. Reusing a fixed deterministic id is inherently brittle against the CLI adding new id-tracking surfaces. + +**Caveat — not yet pinned.** The actual `claude` stderr was NOT captured: the pane only surfaces `exited (code 1)`, swallowing the CLI''s error string. The above is inferred from (a) the documented `--session-id` refusal, (b) the confirmed new registry, (c) the version gap. It must be confirmed against the real stderr before the fix is chosen. **That clide shows an opaque "code 1" with no underlying reason is itself a defect** (see fix #3). + +**Fix directions (ranked):** +1. **Stop reusing a fixed `--session-id` on clear.** Mint a fresh id (as secondary panes already do) and persist it as the pane''s *current* primary id, decoupling "current primary session" from the path-derived default so cross-restart continuity (T-268''s goal) survives without colliding. Robust against any CLI-internal id tracking — the right long-term fix and aligns with D-75 (avoid version-pinned coupling to CC internals). +2. **Make the old id reusable before respawn:** graceful-shutdown the old process (SIGTERM, give the CLI a chance to clean its `sessions/.json`) and/or sweep `~/.claude/sessions/*.json` for entries whose `sessionId == target` before reuse. Fragile — reaches into CLI private state; D-75 caution. +3. **Surface `claude`''s stderr/exit reason in the pane** (and logs) so "code 1" is never opaque again. Do this regardless of 1/2 — it''s what makes this diagnosable. + +**Also:** re-probe the CLI for 2.1.177 and refresh the init cache / the slash-command routing table (T-410/T-411 territory) — the `~/.claude/sessions/` registry is new behavior worth characterizing; other session-lifecycle assumptions may have shifted too. + +**Diagnostic step for the fixer:** +1. Reproduce `/clear` in a primary pane with CLI 2.1.177. +2. Capture the spawned `claude`''s stderr/stdout on the failed respawn (the `--session-id ` invocation). Confirm whether it is "Session ID … is already in use" vs another error. +3. Inspect `~/.claude/sessions/*.json` immediately after the kill — does an entry with the target `sessionId` linger? + +**Files a fix would touch:** `claude_pane.dart` (`_clearSession` 651-659, `_respawnWithSession` 680-703, exit-status handler ~423), `session_naming.dart` (`claudeLaunchArgs` 75, `clearSessionTranscript` 109-114, id derivation 68), `session_orchestrator.dart` (`_spawn` 219-283, `close`), `slash_commands.dart` (routing). + +**Related:** T-268 (done — built the delete-transcript-then-reuse-`--session-id` mechanism that just regressed), T-156 (clide-owned `/clear`), T-161 (`--resume` vs `--session-id` selection), D-77 (stream-json session model), D-75 (version-pinned coupling to CC internals — the risk this realizes). + +--- + +**Folded-in scope (2026-06-15): CLI 2.1.177 re-probe — part of this ticket, not a follow-up.** + +Beyond the `/clear` fix, T-437 also owns bringing clide''s CLI characterization up to the running version: +- Re-run the stream-json `initialize` probe against `claude` **2.1.177** and refresh the per-version init cache (`~/.config/clide/claude/init-.json`, T-151). The codebase was last probed at ≤ 2.1.175 (probe task + `kClideOwnedCommands`/routing assumptions). +- Refresh the slash-command routing table (T-410/T-411) from the new probe: re-confirm advertised vs TUI-only vs owned for 2.1.177, so nothing regresses to a raw harness error. +- Characterize the new `~/.claude/sessions/.json` live-session registry (fields, lifecycle: when written, updated, removed — esp. whether a killed PID''s entry is cleaned). This directly informs which `/clear` fix is viable. +- Audit other session-lifecycle assumptions that may have drifted with the registry addition (resume/fork id handling, `--session-id` vs `--resume` selection). + +**Acceptance (updated):** `/clear` clears the primary pane to an empty conversation on CLI 2.1.177 without exiting; `claude` stderr/exit reason is surfaced in the pane + logs; init cache + routing table refreshed for 2.1.177; the `sessions/` registry lifecycle documented in this ticket (or a D-record if it changes a decision).', 'in_progress', 'high', NULL, NULL, 'D-77', '2026-06-15 11:12:36', '2026-06-15 11:40:04', NULL, 'f5f3bfabc43552ffd82b739e14ca7d40', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_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 (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FCNYXXH5AAHZR7WV0550J3RC', 'bug', NULL, '/clear kills the Claude session (exit 1) — respawn reuses a --session-id the CLI still tracks (2.1.177 regression)', '**Symptom.** Typing `/clear` in the primary Claude pane kills the session: the pane shows `claude exited (code 1) — /clear to restart` and the "Warming up — your conversation will appear here." banner, instead of clearing to a fresh empty conversation. Screenshot taken in workspace `/var/mnt/data/projects/settled-reach`. + +**Environment.** `claude` CLI **2.1.177** (`~/.local/bin/claude`). The codebase''s init-probe cache and the last probe task only cover ≤ **2.1.175** — the CLI has moved past what clide last characterized. This is almost certainly a CLI-version regression, not a clide code change. + +**`/clear` code path (read-only trace, 2026-06-15):** +- `lib/builtin/claude/src/slash_commands.dart:42` — `clear` is in `kClideOwnedCommands`; clide handles it natively (T-156), does not forward it. +- `lib/builtin/claude/src/claude_pane.dart:651-659` — `_clearSession()`: primary pane → `_respawnWithSession(primarySessionId(root), clearTranscript: true)`. +- `claude_pane.dart:680-703` — `_respawnWithSession()`: kills the old session (`activeSessionOrchestrator.close(_orchId)`), then `clearSessionTranscript(...)`, then `_spawn()`. +- `lib/builtin/claude/src/session_naming.dart:109-114` — `clearSessionTranscript()` deletes ONLY `~/.claude/projects//.jsonl` and the sidecar `/` dir. +- `session_naming.dart:75` — respawn uses `claudeLaunchArgs(id, resume: false)` = `[''--session-id'', ]`, where `` is the **same deterministic `primarySessionId(root)`** (derived from the repo path, `session_naming.dart:68`). +- `session_naming.dart:72-74` (the load-bearing comment): "`--session-id` REFUSES an id that already exists (''Session ID … is already in use'')." + +**Root cause (strong hypothesis — see caveat).** T-268 built `/clear` on the assumption that the per-project transcript file (`.jsonl` + sidecar) is the *only* thing the CLI uses to decide whether a `--session-id` is "in use". The current CLI tracks session ids in **additional** state beyond that transcript: +- `~/.claude/sessions/.json` — a live-session registry. Confirmed contents (2.1.177): `{"pid":…,"sessionId":"e7dad3cf-…","cwd":"/var/mnt/data/projects/clide","version":"2.1.177","status":"idle",…}`. Keyed by PID, carries the clide session UUID + cwd. +- `~/.claude/history.jsonl` (2.8 MB, append-only). + +So `/clear` deletes the transcript and respawns with the *same deterministic* `--session-id`, but the killed session''s id is still registered (the registry entry is keyed by the now-dead PID and is the CLI''s own state — clide''s purge doesn''t touch it, and an abrupt kill leaves no chance for the CLI to clean it). The new `claude` rejects the id as already-in-use and **exits 1 at startup validation, before any model turn**. Reusing a fixed deterministic id is inherently brittle against the CLI adding new id-tracking surfaces. + +**Caveat — not yet pinned.** The actual `claude` stderr was NOT captured: the pane only surfaces `exited (code 1)`, swallowing the CLI''s error string. The above is inferred from (a) the documented `--session-id` refusal, (b) the confirmed new registry, (c) the version gap. It must be confirmed against the real stderr before the fix is chosen. **That clide shows an opaque "code 1" with no underlying reason is itself a defect** (see fix #3). + +**Fix directions (ranked):** +1. **Stop reusing a fixed `--session-id` on clear.** Mint a fresh id (as secondary panes already do) and persist it as the pane''s *current* primary id, decoupling "current primary session" from the path-derived default so cross-restart continuity (T-268''s goal) survives without colliding. Robust against any CLI-internal id tracking — the right long-term fix and aligns with D-75 (avoid version-pinned coupling to CC internals). +2. **Make the old id reusable before respawn:** graceful-shutdown the old process (SIGTERM, give the CLI a chance to clean its `sessions/.json`) and/or sweep `~/.claude/sessions/*.json` for entries whose `sessionId == target` before reuse. Fragile — reaches into CLI private state; D-75 caution. +3. **Surface `claude`''s stderr/exit reason in the pane** (and logs) so "code 1" is never opaque again. Do this regardless of 1/2 — it''s what makes this diagnosable. + +**Also:** re-probe the CLI for 2.1.177 and refresh the init cache / the slash-command routing table (T-410/T-411 territory) — the `~/.claude/sessions/` registry is new behavior worth characterizing; other session-lifecycle assumptions may have shifted too. + +**Diagnostic step for the fixer:** +1. Reproduce `/clear` in a primary pane with CLI 2.1.177. +2. Capture the spawned `claude`''s stderr/stdout on the failed respawn (the `--session-id ` invocation). Confirm whether it is "Session ID … is already in use" vs another error. +3. Inspect `~/.claude/sessions/*.json` immediately after the kill — does an entry with the target `sessionId` linger? + +**Files a fix would touch:** `claude_pane.dart` (`_clearSession` 651-659, `_respawnWithSession` 680-703, exit-status handler ~423), `session_naming.dart` (`claudeLaunchArgs` 75, `clearSessionTranscript` 109-114, id derivation 68), `session_orchestrator.dart` (`_spawn` 219-283, `close`), `slash_commands.dart` (routing). + +**Related:** T-268 (done — built the delete-transcript-then-reuse-`--session-id` mechanism that just regressed), T-156 (clide-owned `/clear`), T-161 (`--resume` vs `--session-id` selection), D-77 (stream-json session model), D-75 (version-pinned coupling to CC internals — the risk this realizes). + +--- + +**Folded-in scope (2026-06-15): CLI 2.1.177 re-probe — part of this ticket, not a follow-up.** + +Beyond the `/clear` fix, T-437 also owns bringing clide''s CLI characterization up to the running version: +- Re-run the stream-json `initialize` probe against `claude` **2.1.177** and refresh the per-version init cache (`~/.config/clide/claude/init-.json`, T-151). The codebase was last probed at ≤ 2.1.175 (probe task + `kClideOwnedCommands`/routing assumptions). +- Refresh the slash-command routing table (T-410/T-411) from the new probe: re-confirm advertised vs TUI-only vs owned for 2.1.177, so nothing regresses to a raw harness error. +- Characterize the new `~/.claude/sessions/.json` live-session registry (fields, lifecycle: when written, updated, removed — esp. whether a killed PID''s entry is cleaned). This directly informs which `/clear` fix is viable. +- Audit other session-lifecycle assumptions that may have drifted with the registry addition (resume/fork id handling, `--session-id` vs `--resume` selection). + +**Acceptance (updated):** `/clear` clears the primary pane to an empty conversation on CLI 2.1.177 without exiting; `claude` stderr/exit reason is surfaced in the pane + logs; init cache + routing table refreshed for 2.1.177; the `sessions/` registry lifecycle documented in this ticket (or a D-record if it changes a decision). + +--- + +**RESOLUTION (2026-06-15) — root cause confirmed, fixed.** + +**Actual cause (confirmed against real stderr, NOT the original collision-vs-registry guess).** Captured from clide''s own crash log (`~/.local/state/clide/logs/clide.log`): +`session primary exited (code 1); stderr tail: Error: Session ID is already in use.` + +But the "in use" is a **lifecycle race**, not a persistent registry/transcript collision. Isolated probes against claude 2.1.177 proved: +- `--session-id` reuse SUCCEEDS once the prior holder is dead — even immediately after SIGTERM (the `~/.claude/sessions/.json` entry is cleaned on a SIGTERM exit), and even with another *live* duplicate in `-p` mode. +- A transcript-absent id never collides. +The only way real clide hit "already in use": the **old process was still alive** at respawn. `ClaudeSessionOrchestrator.close()` called `conversation.dispose()` (unawaited) and `ClaudeStreamJsonProcess.kill()` sent SIGTERM **without awaiting `exitCode`** — so `/clear` deleted the transcript and respawned on the same deterministic `--session-id` while the old `claude` was still alive (and re-flushing its transcript). 2.1.177 then rejects the id → exit 1. + +**Fix shipped (chose await-death over the ticket''s tentative "mint a fresh id").** Awaiting the old process''s real death is the targeted root-cause fix and *preserves T-268''s deterministic-id continuity* (fresh-id would have required persisting a per-repo current-id and changing the continuity model). Changes: +- `stream_json_session.dart`: `kill()` now awaits `exitCode` (SIGTERM → 2s → SIGKILL → await); `dispose()` is idempotent (cached future) so the conversation''s unawaited dispose and the orchestrator''s awaited one share one teardown; new `SessionEnd.reason` getter (last non-empty stderr line, capped). +- `session_orchestrator.dart`: `close()` awaits `session.dispose()` → returns only once the process is truly dead, so the transcript clear + respawn happen after death. +- `claude_pane.dart`: `_onSessionEnd` surfaces `end.reason` in the status line — no more opaque "code 1" (fix #3). +- `session_naming.dart`: corrected the stale `clearSessionTranscript` doc (the real sidecar is the shared `memory/` dir, not `/`; left untouched) + the await-death precondition. +- Tests: `close()` blocks until process exit (gated fake); `SessionEnd.reason` extraction/cap/empty. + +**Folded-in CLI 2.1.177 re-probe — done, no code change needed.** +- `~/.claude/sessions/.json` registry characterized: PID-keyed, carries `{sessionId, cwd, version, kind:"interactive", entrypoint:"sdk-cli", status}`; written at start, **removed on a SIGTERM/clean exit**. So once clide awaits death, the id is free. +- Init cache (`init-.json`) auto-refreshes at runtime per version — nothing to hand-edit. +- Probed 2.1.177 advertised `slash_commands` (31): no drift that breaks the routing table — none of clide''s `kTuiOnlyCommands` became advertised, and new entries (skills/builtins) correctly fall through to `forward`. + +**Verification status.** Unit-tested (the teardown ordering + reason surfacing) and validated against the live CLI via probes. `make test` green. NOT yet exercised in the running GUI (would need `make run` + an interactive `/clear`) — recommend a quick live confirm before closing.', 'in_progress', 'high', NULL, NULL, 'D-77', '2026-06-15 11:12:36', '2026-06-15 13:30:11', NULL, '5f75ed72b507deff17701061ec3091ae', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_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 (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FCNYXXH5AAHZR7WV0550J3RC', 'bug', NULL, '/clear kills the Claude session (exit 1) — respawn reuses a --session-id the CLI still tracks (2.1.177 regression)', '**Symptom.** Typing `/clear` in the primary Claude pane kills the session: the pane shows `claude exited (code 1) — /clear to restart` and the "Warming up — your conversation will appear here." banner, instead of clearing to a fresh empty conversation. Screenshot taken in workspace `/var/mnt/data/projects/settled-reach`. + +**Environment.** `claude` CLI **2.1.177** (`~/.local/bin/claude`). The codebase''s init-probe cache and the last probe task only cover ≤ **2.1.175** — the CLI has moved past what clide last characterized. This is almost certainly a CLI-version regression, not a clide code change. + +**`/clear` code path (read-only trace, 2026-06-15):** +- `lib/builtin/claude/src/slash_commands.dart:42` — `clear` is in `kClideOwnedCommands`; clide handles it natively (T-156), does not forward it. +- `lib/builtin/claude/src/claude_pane.dart:651-659` — `_clearSession()`: primary pane → `_respawnWithSession(primarySessionId(root), clearTranscript: true)`. +- `claude_pane.dart:680-703` — `_respawnWithSession()`: kills the old session (`activeSessionOrchestrator.close(_orchId)`), then `clearSessionTranscript(...)`, then `_spawn()`. +- `lib/builtin/claude/src/session_naming.dart:109-114` — `clearSessionTranscript()` deletes ONLY `~/.claude/projects//.jsonl` and the sidecar `/` dir. +- `session_naming.dart:75` — respawn uses `claudeLaunchArgs(id, resume: false)` = `[''--session-id'', ]`, where `` is the **same deterministic `primarySessionId(root)`** (derived from the repo path, `session_naming.dart:68`). +- `session_naming.dart:72-74` (the load-bearing comment): "`--session-id` REFUSES an id that already exists (''Session ID … is already in use'')." + +**Root cause (strong hypothesis — see caveat).** T-268 built `/clear` on the assumption that the per-project transcript file (`.jsonl` + sidecar) is the *only* thing the CLI uses to decide whether a `--session-id` is "in use". The current CLI tracks session ids in **additional** state beyond that transcript: +- `~/.claude/sessions/.json` — a live-session registry. Confirmed contents (2.1.177): `{"pid":…,"sessionId":"e7dad3cf-…","cwd":"/var/mnt/data/projects/clide","version":"2.1.177","status":"idle",…}`. Keyed by PID, carries the clide session UUID + cwd. +- `~/.claude/history.jsonl` (2.8 MB, append-only). + +So `/clear` deletes the transcript and respawns with the *same deterministic* `--session-id`, but the killed session''s id is still registered (the registry entry is keyed by the now-dead PID and is the CLI''s own state — clide''s purge doesn''t touch it, and an abrupt kill leaves no chance for the CLI to clean it). The new `claude` rejects the id as already-in-use and **exits 1 at startup validation, before any model turn**. Reusing a fixed deterministic id is inherently brittle against the CLI adding new id-tracking surfaces. + +**Caveat — not yet pinned.** The actual `claude` stderr was NOT captured: the pane only surfaces `exited (code 1)`, swallowing the CLI''s error string. The above is inferred from (a) the documented `--session-id` refusal, (b) the confirmed new registry, (c) the version gap. It must be confirmed against the real stderr before the fix is chosen. **That clide shows an opaque "code 1" with no underlying reason is itself a defect** (see fix #3). + +**Fix directions (ranked):** +1. **Stop reusing a fixed `--session-id` on clear.** Mint a fresh id (as secondary panes already do) and persist it as the pane''s *current* primary id, decoupling "current primary session" from the path-derived default so cross-restart continuity (T-268''s goal) survives without colliding. Robust against any CLI-internal id tracking — the right long-term fix and aligns with D-75 (avoid version-pinned coupling to CC internals). +2. **Make the old id reusable before respawn:** graceful-shutdown the old process (SIGTERM, give the CLI a chance to clean its `sessions/.json`) and/or sweep `~/.claude/sessions/*.json` for entries whose `sessionId == target` before reuse. Fragile — reaches into CLI private state; D-75 caution. +3. **Surface `claude`''s stderr/exit reason in the pane** (and logs) so "code 1" is never opaque again. Do this regardless of 1/2 — it''s what makes this diagnosable. + +**Also:** re-probe the CLI for 2.1.177 and refresh the init cache / the slash-command routing table (T-410/T-411 territory) — the `~/.claude/sessions/` registry is new behavior worth characterizing; other session-lifecycle assumptions may have shifted too. + +**Diagnostic step for the fixer:** +1. Reproduce `/clear` in a primary pane with CLI 2.1.177. +2. Capture the spawned `claude`''s stderr/stdout on the failed respawn (the `--session-id ` invocation). Confirm whether it is "Session ID … is already in use" vs another error. +3. Inspect `~/.claude/sessions/*.json` immediately after the kill — does an entry with the target `sessionId` linger? + +**Files a fix would touch:** `claude_pane.dart` (`_clearSession` 651-659, `_respawnWithSession` 680-703, exit-status handler ~423), `session_naming.dart` (`claudeLaunchArgs` 75, `clearSessionTranscript` 109-114, id derivation 68), `session_orchestrator.dart` (`_spawn` 219-283, `close`), `slash_commands.dart` (routing). + +**Related:** T-268 (done — built the delete-transcript-then-reuse-`--session-id` mechanism that just regressed), T-156 (clide-owned `/clear`), T-161 (`--resume` vs `--session-id` selection), D-77 (stream-json session model), D-75 (version-pinned coupling to CC internals — the risk this realizes). + +--- + +**Folded-in scope (2026-06-15): CLI 2.1.177 re-probe — part of this ticket, not a follow-up.** + +Beyond the `/clear` fix, T-437 also owns bringing clide''s CLI characterization up to the running version: +- Re-run the stream-json `initialize` probe against `claude` **2.1.177** and refresh the per-version init cache (`~/.config/clide/claude/init-.json`, T-151). The codebase was last probed at ≤ 2.1.175 (probe task + `kClideOwnedCommands`/routing assumptions). +- Refresh the slash-command routing table (T-410/T-411) from the new probe: re-confirm advertised vs TUI-only vs owned for 2.1.177, so nothing regresses to a raw harness error. +- Characterize the new `~/.claude/sessions/.json` live-session registry (fields, lifecycle: when written, updated, removed — esp. whether a killed PID''s entry is cleaned). This directly informs which `/clear` fix is viable. +- Audit other session-lifecycle assumptions that may have drifted with the registry addition (resume/fork id handling, `--session-id` vs `--resume` selection). + +**Acceptance (updated):** `/clear` clears the primary pane to an empty conversation on CLI 2.1.177 without exiting; `claude` stderr/exit reason is surfaced in the pane + logs; init cache + routing table refreshed for 2.1.177; the `sessions/` registry lifecycle documented in this ticket (or a D-record if it changes a decision). + +--- + +**RESOLUTION (2026-06-15) — root cause confirmed, fixed.** + +**Actual cause (confirmed against real stderr, NOT the original collision-vs-registry guess).** Captured from clide''s own crash log (`~/.local/state/clide/logs/clide.log`): +`session primary exited (code 1); stderr tail: Error: Session ID is already in use.` + +But the "in use" is a **lifecycle race**, not a persistent registry/transcript collision. Isolated probes against claude 2.1.177 proved: +- `--session-id` reuse SUCCEEDS once the prior holder is dead — even immediately after SIGTERM (the `~/.claude/sessions/.json` entry is cleaned on a SIGTERM exit), and even with another *live* duplicate in `-p` mode. +- A transcript-absent id never collides. +The only way real clide hit "already in use": the **old process was still alive** at respawn. `ClaudeSessionOrchestrator.close()` called `conversation.dispose()` (unawaited) and `ClaudeStreamJsonProcess.kill()` sent SIGTERM **without awaiting `exitCode`** — so `/clear` deleted the transcript and respawned on the same deterministic `--session-id` while the old `claude` was still alive (and re-flushing its transcript). 2.1.177 then rejects the id → exit 1. + +**Fix shipped (chose await-death over the ticket''s tentative "mint a fresh id").** Awaiting the old process''s real death is the targeted root-cause fix and *preserves T-268''s deterministic-id continuity* (fresh-id would have required persisting a per-repo current-id and changing the continuity model). Changes: +- `stream_json_session.dart`: `kill()` now awaits `exitCode` (SIGTERM → 2s → SIGKILL → await); `dispose()` is idempotent (cached future) so the conversation''s unawaited dispose and the orchestrator''s awaited one share one teardown; new `SessionEnd.reason` getter (last non-empty stderr line, capped). +- `session_orchestrator.dart`: `close()` awaits `session.dispose()` → returns only once the process is truly dead, so the transcript clear + respawn happen after death. +- `claude_pane.dart`: `_onSessionEnd` surfaces `end.reason` in the status line — no more opaque "code 1" (fix #3). +- `session_naming.dart`: corrected the stale `clearSessionTranscript` doc (the real sidecar is the shared `memory/` dir, not `/`; left untouched) + the await-death precondition. +- Tests: `close()` blocks until process exit (gated fake); `SessionEnd.reason` extraction/cap/empty. + +**Folded-in CLI 2.1.177 re-probe — done, no code change needed.** +- `~/.claude/sessions/.json` registry characterized: PID-keyed, carries `{sessionId, cwd, version, kind:"interactive", entrypoint:"sdk-cli", status}`; written at start, **removed on a SIGTERM/clean exit**. So once clide awaits death, the id is free. +- Init cache (`init-.json`) auto-refreshes at runtime per version — nothing to hand-edit. +- Probed 2.1.177 advertised `slash_commands` (31): no drift that breaks the routing table — none of clide''s `kTuiOnlyCommands` became advertised, and new entries (skills/builtins) correctly fall through to `forward`. + +**Verification status.** Unit-tested (the teardown ordering + reason surfacing) and validated against the live CLI via probes. `make test` green. NOT yet exercised in the running GUI (would need `make run` + an interactive `/clear`) — recommend a quick live confirm before closing.', 'review', 'high', NULL, NULL, 'D-77', '2026-06-15 11:12:36', '2026-06-15 13:30:21', NULL, 'ae4733f6be976e76598876a08c195dc8', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_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 1147633b..18bea19c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,6 +40,16 @@ heading, and (b) bumping `pubspec.yaml` `version:` in the same commit. and `clide log level []` does the same from the CLI — D-6 parity. The choice survives restart. (T-433) +### Fixed + +- **`/clear` no longer kills the Claude pane.** Clearing the primary pane tore + the session down and respawned on the same deterministic `--session-id` + *before the old `claude` process had actually exited*, so claude 2.1.177 + rejected the id as "already in use" and the respawn exited 1. The session + teardown now awaits the process's real death (SIGTERM, escalating to SIGKILL) + before clearing the transcript and respawning. A dead pane also now shows the + CLI's own reason instead of a bare "exited (code 1)". (T-437) + ## [2.5.0] — 2026-06-14 ### Added diff --git a/lib/builtin/claude/src/claude_pane.dart b/lib/builtin/claude/src/claude_pane.dart index f5203e34..4532d4cb 100644 --- a/lib/builtin/claude/src/claude_pane.dart +++ b/lib/builtin/claude/src/claude_pane.dart @@ -420,7 +420,10 @@ class _ClaudePaneState extends State { if (!mounted) return; final tail = end.stderrTail.isEmpty ? '' : '; stderr tail:\n${end.stderrTail.join('\n')}'; _kernel?.log.warn('claude', 'session $_orchId exited (code ${end.exitCode})$tail'); - setState(() => _statusLine = 'claude exited (code ${end.exitCode}) — /clear to restart'); + // Surface the CLI's own reason (e.g. "Session ID … is already in use") + // instead of an opaque "code 1" (T-437). + final why = end.reason.isEmpty ? '' : ' — ${end.reason}'; + setState(() => _statusLine = 'claude exited (code ${end.exitCode})$why · /clear to restart'); } // Send composed text to Claude over the stream-json channel. Commands clide diff --git a/lib/builtin/claude/src/session_naming.dart b/lib/builtin/claude/src/session_naming.dart index 62d62cc5..d4d22dd1 100644 --- a/lib/builtin/claude/src/session_naming.dart +++ b/lib/builtin/claude/src/session_naming.dart @@ -102,10 +102,16 @@ String claudeTranscriptPath(String repoRoot, String sessionId) => '${claudeProje /// Erase [sessionId]'s transcript under [projectDir] so a subsequent /// `claude --session-id ` re-creates it empty — the in-place -/// `/clear` path for the primary pane (T-268). Removes both the `.jsonl` -/// and the sidecar `/` directory claude keeps beside it. Best-effort: -/// missing entries are not an error. The caller MUST have killed the session's -/// process first, so claude is not mid-write. +/// `/clear` path for the primary pane (T-268). Removes the `.jsonl`, plus +/// a per-session `/` sidecar dir if one exists (best-effort; missing +/// entries are not an error). Note the shared per-project `memory/` dir that +/// claude 2.1.x keeps beside transcripts is deliberately left alone — it is +/// not per-session. +/// +/// The caller MUST have AWAITED the session's process death first (T-437): a +/// still-live claude re-flushes its transcript and keeps the id registered, so +/// the respawn's `--session-id` is rejected as "already in use" (exit 1). +/// [ClaudeSessionOrchestrator.close] now awaits that death before this runs. Future clearSessionTranscript(String projectDir, String sessionId) async { final file = File('$projectDir/$sessionId.jsonl'); if (await file.exists()) await file.delete(); diff --git a/lib/builtin/claude/src/session_orchestrator.dart b/lib/builtin/claude/src/session_orchestrator.dart index dbf317a6..bfa7b394 100644 --- a/lib/builtin/claude/src/session_orchestrator.dart +++ b/lib/builtin/claude/src/session_orchestrator.dart @@ -313,12 +313,18 @@ class ClaudeSessionOrchestrator extends ChangeNotifier { } /// Kill and forget a session (the real teardown). The conversation's - /// onDispose kills the process + closes its streams. + /// onDispose kills the process + closes its streams; we then AWAIT the + /// session's teardown so the `claude` process is genuinely dead before we + /// return (T-437). Callers respawn the primary on the same deterministic + /// `--session-id` right after /clear — if the old process were still alive, + /// claude 2.1.177 would reject the id as "already in use" and the respawn + /// would exit 1. Future close(String id) async { final m = _sessions.remove(id); if (m == null) return; broker.removeMember(id); - m.conversation.dispose(); + m.conversation.dispose(); // cancels the item subscription; kicks off session teardown + await m.session.dispose(); // idempotent — awaits the real process exit notifyListeners(); } diff --git a/lib/builtin/claude/src/stream_json_session.dart b/lib/builtin/claude/src/stream_json_session.dart index 39cecc5b..490dbd9a 100644 --- a/lib/builtin/claude/src/stream_json_session.dart +++ b/lib/builtin/claude/src/stream_json_session.dart @@ -107,7 +107,20 @@ class ClaudeStreamJsonProcess extends StreamJsonProcess { @override Future kill() async { + // Await the process's ACTUAL death, not just the signal (T-437). clide + // respawns the primary on the SAME deterministic --session-id right after + // /clear; if the old process is still alive (or still flushing its + // transcript) when the new one starts, claude 2.1.177 rejects the id with + // "Session ID … is already in use" and the respawn exits 1. SIGTERM first + // (claude cleans its session registry on it), escalate to SIGKILL if it + // doesn't go, and only return once exitCode has resolved. _proc.kill(); + try { + await _proc.exitCode.timeout(const Duration(seconds: 2)); + } on TimeoutException { + _proc.kill(ProcessSignal.sigkill); + await _proc.exitCode; + } } @override @@ -288,6 +301,20 @@ class SessionEnd { final int exitCode; final List stderrTail; + + /// The most recent non-empty stderr line — the CLI's own error message when + /// it dies (e.g. "Session ID … is already in use") — for surfacing in the + /// pane so a non-zero exit is never an opaque "code 1" (T-437). Empty when + /// stderr was silent; capped so a stray long line can't blow out the status + /// line. + String get reason { + for (final line in stderrTail.reversed) { + final t = line.trim(); + if (t.isEmpty) continue; + return t.length > 200 ? '${t.substring(0, 200)}…' : t; + } + return ''; + } } class StreamJsonSession { @@ -965,10 +992,17 @@ class StreamJsonSession { _endCtl.add(_end!); } - Future dispose() async { + /// Idempotent: the conversation controller's [dispose] fires this + /// unawaited while a caller (the orchestrator's [ClaudeSessionOrchestrator.close]) + /// awaits it to know the process is truly dead (T-437). Caching the future + /// makes both paths share one teardown rather than killing/closing twice. + Future dispose() => _disposeFuture ??= _dispose(); + Future? _disposeFuture; + + Future _dispose() async { _disposed = true; // deliberate teardown — suppress the exit-watch path await _sub?.cancel(); - await _proc.kill(); + await _proc.kill(); // awaits the process's real exit (T-437) await _items.close(); await _statusCtl.close(); await _workflowsCtl.close(); diff --git a/test/builtin/claude/session_lifecycle_test.dart b/test/builtin/claude/session_lifecycle_test.dart index 322e5796..11f37c35 100644 --- a/test/builtin/claude/session_lifecycle_test.dart +++ b/test/builtin/claude/session_lifecycle_test.dart @@ -33,6 +33,26 @@ class _FakeProc extends StreamJsonProcess { Future kill() async => killed = true; } +/// A fake whose [kill] blocks until [gate] completes — models a real `claude` +/// that hasn't actually exited yet, so a test can prove `close()` waits for the +/// process's real death before returning (T-437). +class _GatedProc extends StreamJsonProcess { + _GatedProc(this._gate); + final Completer _gate; + final _ctl = StreamController.broadcast(); + int killCount = 0; + + @override + Stream get lines => _ctl.stream; + @override + void writeLine(String line) {} + @override + Future kill() async { + killCount++; + await _gate.future; + } +} + // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- @@ -207,6 +227,37 @@ void main() { }); }); + // ---- close awaits the process's real death (T-437) ---------------------- + + group('close — awaits real process death before returning (T-437)', () { + test('close() does not complete until the process exit resolves', () async { + final gate = Completer(); + final created = <_GatedProc>[]; + final orch = ClaudeSessionOrchestrator( + processFactory: ({required sessionArgs, required cwd, env}) async { + final p = _GatedProc(gate); + created.add(p); + return p; + }, + ); + await orch.spawn(_spec('primary')); + + var closed = false; + final closing = orch.close('primary').then((_) => closed = true); + await pumpEventQueue(); + // The process hasn't exited yet, so /clear must not have proceeded to + // delete the transcript + respawn — close() is still awaiting death. + expect(closed, isFalse, reason: 'close must block until the old process is truly dead'); + + gate.complete(); // the claude process finally exits + await closing; + expect(closed, isTrue); + expect(created.single.killCount, 1, reason: 'idempotent dispose kills exactly once'); + + orch.dispose(); + }); + }); + // ---- claude.kill-all-sessions via orchestrator -------------------------- group('claude.kill-all-sessions via orchestrator (T-167)', () { diff --git a/test/builtin/claude/stream_json_session_test.dart b/test/builtin/claude/stream_json_session_test.dart index f3711998..7f8ffb64 100644 --- a/test/builtin/claude/stream_json_session_test.dart +++ b/test/builtin/claude/stream_json_session_test.dart @@ -937,6 +937,24 @@ void main() { }); }); + group('SessionEnd.reason (T-437)', () { + test('is the last non-empty stderr line — the CLI error', () { + const end = SessionEnd(exitCode: 1, stderrTail: ['warming up', '', 'Error: Session ID abc is already in use.', ' ']); + expect(end.reason, 'Error: Session ID abc is already in use.'); + }); + + test('is empty when stderr was silent', () { + expect(const SessionEnd(exitCode: 1, stderrTail: []).reason, isEmpty); + expect(const SessionEnd(exitCode: 1, stderrTail: ['', ' ']).reason, isEmpty); + }); + + test('caps a very long line so it cannot blow out the status line', () { + final end = SessionEnd(exitCode: 1, stderrTail: ['x' * 500]); + expect(end.reason.length, 201); // 200 chars + ellipsis + expect(end.reason.endsWith('…'), isTrue); + }); + }); + group('BoundedLineBuffer', () { test('keeps only the last cap lines', () { final b = BoundedLineBuffer(cap: 3);