fix(claude): /clear no longer kills the session on CLI 2.1.177 (T-437)
/clear tore the session down and respawned on the same deterministic --session-id BEFORE the old claude process had actually exited. The orchestrator's close() ran conversation.dispose() unawaited and kill() only sent SIGTERM without awaiting exitCode, so the respawn raced a still-alive holder of the id — claude 2.1.177 rejects it as "Session ID … is already in use" and exits 1. Root cause confirmed from clide's own crash log + isolated probes against 2.1.177: the id frees the instant the holder dies (SIGTERM cleans the new ~/.claude/sessions/<pid>.json registry), so awaiting real death is the fix — and it preserves T-268's deterministic-id continuity (chosen over minting a fresh id, which would change the continuity model). - stream_json_session: kill() awaits exitCode (SIGTERM → 2s → SIGKILL → await); dispose() idempotent (shared cached future); new SessionEnd.reason getter (last non-empty stderr line, capped). - session_orchestrator: close() awaits session.dispose() so teardown returns only once the process is truly dead, before clear + respawn. - claude_pane: surface end.reason in the status line — no more opaque "code 1". - session_naming: correct the stale clearSessionTranscript doc (real sidecar is the shared memory/ dir) + the await-death precondition. - tests: close() blocks until process exit; SessionEnd.reason. CLI 2.1.177 re-probe (folded-in scope): sessions/ registry characterized (PID-keyed, cleaned on exit); init cache auto-refreshes; advertised slash_commands show no routing-table drift. No further code change needed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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).
|
- 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;
|
**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/<munged-root>/<id>.jsonl` and the sidecar `<id>/` dir.
|
||||||
|
- `session_naming.dart:75` — respawn uses `claudeLaunchArgs(id, resume: false)` = `[''--session-id'', <id>]`, where `<id>` 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 (`<id>.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/<pid>.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/<pid>.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 <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-<version>.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/<pid>.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/<munged-root>/<id>.jsonl` and the sidecar `<id>/` dir.
|
||||||
|
- `session_naming.dart:75` — respawn uses `claudeLaunchArgs(id, resume: false)` = `[''--session-id'', <id>]`, where `<id>` 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 (`<id>.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/<pid>.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/<pid>.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 <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-<version>.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/<pid>.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 <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/<pid>.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 `<id>/`; 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/<pid>.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-<version>.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;
|
||||||
|
|||||||
@@ -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).
|
- 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);
|
**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/<munged-root>/<id>.jsonl` and the sidecar `<id>/` dir.
|
||||||
|
- `session_naming.dart:75` — respawn uses `claudeLaunchArgs(id, resume: false)` = `[''--session-id'', <id>]`, where `<id>` 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 (`<id>.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/<pid>.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/<pid>.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 <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-<version>.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/<pid>.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/<munged-root>/<id>.jsonl` and the sidecar `<id>/` dir.
|
||||||
|
- `session_naming.dart:75` — respawn uses `claudeLaunchArgs(id, resume: false)` = `[''--session-id'', <id>]`, where `<id>` 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 (`<id>.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/<pid>.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/<pid>.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 <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-<version>.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/<pid>.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/<munged-root>/<id>.jsonl` and the sidecar `<id>/` dir.
|
||||||
|
- `session_naming.dart:75` — respawn uses `claudeLaunchArgs(id, resume: false)` = `[''--session-id'', <id>]`, where `<id>` 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 (`<id>.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/<pid>.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/<pid>.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 <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-<version>.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/<pid>.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 <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/<pid>.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 `<id>/`; 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/<pid>.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-<version>.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/<munged-root>/<id>.jsonl` and the sidecar `<id>/` dir.
|
||||||
|
- `session_naming.dart:75` — respawn uses `claudeLaunchArgs(id, resume: false)` = `[''--session-id'', <id>]`, where `<id>` 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 (`<id>.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/<pid>.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/<pid>.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 <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-<version>.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/<pid>.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 <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/<pid>.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 `<id>/`; 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/<pid>.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-<version>.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);
|
||||||
|
|||||||
@@ -40,6 +40,16 @@ heading, and (b) bumping `pubspec.yaml` `version:` in the same commit.
|
|||||||
and `clide log level [<level>]` does the same from the CLI — D-6 parity. The
|
and `clide log level [<level>]` does the same from the CLI — D-6 parity. The
|
||||||
choice survives restart. (T-433)
|
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
|
## [2.5.0] — 2026-06-14
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@@ -420,7 +420,10 @@ class _ClaudePaneState extends State<ClaudePane> {
|
|||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
final tail = end.stderrTail.isEmpty ? '' : '; stderr tail:\n${end.stderrTail.join('\n')}';
|
final tail = end.stderrTail.isEmpty ? '' : '; stderr tail:\n${end.stderrTail.join('\n')}';
|
||||||
_kernel?.log.warn('claude', 'session $_orchId exited (code ${end.exitCode})$tail');
|
_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
|
// Send composed text to Claude over the stream-json channel. Commands clide
|
||||||
|
|||||||
@@ -102,10 +102,16 @@ String claudeTranscriptPath(String repoRoot, String sessionId) => '${claudeProje
|
|||||||
|
|
||||||
/// Erase [sessionId]'s transcript under [projectDir] so a subsequent
|
/// Erase [sessionId]'s transcript under [projectDir] so a subsequent
|
||||||
/// `claude --session-id <sessionId>` re-creates it empty — the in-place
|
/// `claude --session-id <sessionId>` re-creates it empty — the in-place
|
||||||
/// `/clear` path for the primary pane (T-268). Removes both the `<id>.jsonl`
|
/// `/clear` path for the primary pane (T-268). Removes the `<id>.jsonl`, plus
|
||||||
/// and the sidecar `<id>/` directory claude keeps beside it. Best-effort:
|
/// a per-session `<id>/` sidecar dir if one exists (best-effort; missing
|
||||||
/// missing entries are not an error. The caller MUST have killed the session's
|
/// entries are not an error). Note the shared per-project `memory/` dir that
|
||||||
/// process first, so claude is not mid-write.
|
/// 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<void> clearSessionTranscript(String projectDir, String sessionId) async {
|
Future<void> clearSessionTranscript(String projectDir, String sessionId) async {
|
||||||
final file = File('$projectDir/$sessionId.jsonl');
|
final file = File('$projectDir/$sessionId.jsonl');
|
||||||
if (await file.exists()) await file.delete();
|
if (await file.exists()) await file.delete();
|
||||||
|
|||||||
@@ -313,12 +313,18 @@ class ClaudeSessionOrchestrator extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Kill and forget a session (the real teardown). The conversation's
|
/// 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<void> close(String id) async {
|
Future<void> close(String id) async {
|
||||||
final m = _sessions.remove(id);
|
final m = _sessions.remove(id);
|
||||||
if (m == null) return;
|
if (m == null) return;
|
||||||
broker.removeMember(id);
|
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();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -107,7 +107,20 @@ class ClaudeStreamJsonProcess extends StreamJsonProcess {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> kill() async {
|
Future<void> 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();
|
_proc.kill();
|
||||||
|
try {
|
||||||
|
await _proc.exitCode.timeout(const Duration(seconds: 2));
|
||||||
|
} on TimeoutException {
|
||||||
|
_proc.kill(ProcessSignal.sigkill);
|
||||||
|
await _proc.exitCode;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -288,6 +301,20 @@ class SessionEnd {
|
|||||||
|
|
||||||
final int exitCode;
|
final int exitCode;
|
||||||
final List<String> stderrTail;
|
final List<String> 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 {
|
class StreamJsonSession {
|
||||||
@@ -965,10 +992,17 @@ class StreamJsonSession {
|
|||||||
_endCtl.add(_end!);
|
_endCtl.add(_end!);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> 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<void> dispose() => _disposeFuture ??= _dispose();
|
||||||
|
Future<void>? _disposeFuture;
|
||||||
|
|
||||||
|
Future<void> _dispose() async {
|
||||||
_disposed = true; // deliberate teardown — suppress the exit-watch path
|
_disposed = true; // deliberate teardown — suppress the exit-watch path
|
||||||
await _sub?.cancel();
|
await _sub?.cancel();
|
||||||
await _proc.kill();
|
await _proc.kill(); // awaits the process's real exit (T-437)
|
||||||
await _items.close();
|
await _items.close();
|
||||||
await _statusCtl.close();
|
await _statusCtl.close();
|
||||||
await _workflowsCtl.close();
|
await _workflowsCtl.close();
|
||||||
|
|||||||
@@ -33,6 +33,26 @@ class _FakeProc extends StreamJsonProcess {
|
|||||||
Future<void> kill() async => killed = true;
|
Future<void> 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<void> _gate;
|
||||||
|
final _ctl = StreamController<String>.broadcast();
|
||||||
|
int killCount = 0;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Stream<String> get lines => _ctl.stream;
|
||||||
|
@override
|
||||||
|
void writeLine(String line) {}
|
||||||
|
@override
|
||||||
|
Future<void> kill() async {
|
||||||
|
killCount++;
|
||||||
|
await _gate.future;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Helpers
|
// 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<void>();
|
||||||
|
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 --------------------------
|
// ---- claude.kill-all-sessions via orchestrator --------------------------
|
||||||
|
|
||||||
group('claude.kill-all-sessions via orchestrator (T-167)', () {
|
group('claude.kill-all-sessions via orchestrator (T-167)', () {
|
||||||
|
|||||||
@@ -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', () {
|
group('BoundedLineBuffer', () {
|
||||||
test('keeps only the last cap lines', () {
|
test('keeps only the last cap lines', () {
|
||||||
final b = BoundedLineBuffer(cap: 3);
|
final b = BoundedLineBuffer(cap: 3);
|
||||||
|
|||||||
Reference in New Issue
Block a user