Skip to content

fix(sessions): verify session id in liveness check to prune ghost sessions#10

Merged
Clast merged 1 commit into
Affirm:mainfrom
tarkatronic:fix/session-id-liveness-verification
Jun 4, 2026
Merged

fix(sessions): verify session id in liveness check to prune ghost sessions#10
Clast merged 1 commit into
Affirm:mainfrom
tarkatronic:fix/session-id-liveness-verification

Conversation

@tarkatronic

Copy link
Copy Markdown
Member

Problem

Navi was showing more sessions than were actually running (e.g. 8 shown for 5 live Claudes). The liveness check was PID-only:

public var isAlive: Bool { pid > 0 && kill(pid, 0) == 0 }

kill(pid, 0) == 0 only asks whether some process holds that PID — not whether it's still the Claude session that created it. Two things defeated it:

  1. Navi never updated a session's stored PID. When a session id is reused under a new process (a --resume, or /clear/compaction minting a new id on the same process), the stored PID drifts from reality.
  2. A drifted PID stays "alive" via PID reuse. Once the original PID exits, the OS hands that number to another process, so kill(oldPid, 0) returns 0 and the dead session lingers as a ghost.

Confirmed concretely: a session id with hook activity but no live session file was still being counted because its recorded PID resolved to an unrelated live process.

Fix

  • discoverSessions() now returns the set of session IDs whose own ~/.claude/sessions/<pid>.json points at a live PID — the authoritative identity-verified live set.
  • Pruning keeps a session only when its own id is in that set (SessionInfo.isAlive(among:)), so a reused/drifted PID can no longer keep a dead session alive.
  • Tracked sessions refresh their stored PID when a resume moves the id to a new process, keeping the PID-based display guard and terminal focus correct.
  • Pruning is skipped entirely on a transient sessions-dir read error, so live sessions are never wiped.

Performance

Zero new filesystem work — this reuses the per-poll directory scan discoverSessions() already performs (1 readdir + a handful of small file reads). The only added cost is building a small Set<String> and one dictionary filter by set membership — microseconds. No process spawns; poll cadence unchanged.

Testing

  • swift build clean
  • swift test — all 95 tests pass, including 3 new isAlive(among:) cases (id in live set, ghost id with a live-but-wrong PID, empty set)

Version bumped 1.2.1 → 1.2.2 in .claude-plugin/plugin.json and Sources/NaviCore/Version.swift.

🤖 Generated with Claude Code

…ump to 1.2.2

Navi's liveness check was PID-only (kill(pid, 0) == 0), which only asks
whether some process holds the PID — not whether it's still the Claude
session that created it. After PID reuse, or a resumed session whose
original PID exited, a dead session could pass the check and linger as a
ghost, inflating the session count.

discoverSessions() now returns the set of session IDs whose own
~/.claude/sessions/<pid>.json points at a live PID, and pruning keeps a
session only when its own id is in that set. Tracked sessions also
refresh their stored PID when a resume moves the id to a new process, so
the PID-based display guard and terminal focus stay correct. Pruning is
skipped on a transient sessions-dir read error so live sessions are never
wiped. No new filesystem work — this reuses the per-poll directory scan
discoverSessions() already performs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@Clast Clast self-requested a review June 4, 2026 20:44
@Clast Clast merged commit 79ff612 into Affirm:main Jun 4, 2026
2 checks passed
@tarkatronic tarkatronic deleted the fix/session-id-liveness-verification branch June 4, 2026 20:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants