Skip to content

fix: prevent phantom (previous) sessions from lazy-resume fallback#584

Merged
PureWeen merged 4 commits intomainfrom
fix/lazy-resume-phantom-previous
Apr 14, 2026
Merged

fix: prevent phantom (previous) sessions from lazy-resume fallback#584
PureWeen merged 4 commits intomainfrom
fix/lazy-resume-phantom-previous

Conversation

@PureWeen
Copy link
Copy Markdown
Owner

What

When lazy-resume fails (CLI returns "Session not found" / corrupt / shutdown) and creates a fresh session, the old persisted entry was not being marked as superseded. On the next SaveActiveSessionsToDisk, MergeSessionEntries found both old and new entries with the same display name and renamed the old one to "(previous)".

Root cause

The lazy-resume fallback in EnsureSessionConnectedAsync creates a fresh session but didn't:

  1. Set RecoveredFromSessionId — so CanSafelyDropSupersededGroupMoveEntry returned false
  2. Add the old session ID to _closedSessionIds — so the merge didn't filter it out

Fix

Two lines after the fresh session is created:

  • state.Info.RecoveredFromSessionId ??= sessionId — marks the new session as the successor
  • _closedSessionIds.TryAdd(sessionId, 0) — ensures the merge drops the old entry

Real-world trigger

Observed on session "34678": the CLI shut down the original session (session.shutdown at 02:57 UTC). After app restart, lazy-resume got "Session not found", created a fresh session, but left the old entry on disk → "34678 (previous)" appeared.

Testing

3335/3335 tests pass.

PureWeen and others added 2 commits April 14, 2026 11:22
When lazy-resume fails (session not found / corrupt / shutdown) and creates a fresh session, set RecoveredFromSessionId and add the old ID to _closedSessionIds. Without this, MergeSessionEntries sees two entries with the same name and renames the old one to '(previous)'.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The reconnect handler's 'Session not found' and 'corrupted' fallback paths also create fresh sessions without setting RecoveredFromSessionId or adding the old ID to _closedSessionIds, causing the same phantom '(previous)' entries.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@PureWeen
Copy link
Copy Markdown
Owner Author

PureWeen commented Apr 14, 2026

Code Review — PR #584: fix: prevent phantom (previous) sessions from lazy-resume fallback

Reviewed by: 3 independent reviewers | CI: ⚠️ No checks reported on branch


Final Re-Review (4 commits)

Previous Finding Status

# Finding Status
1 🟡 ??= should be = on RecoveredFromSessionId FIXED — All 3 sites now use = (commit acab0f62f)
2 🟡 Missing behavioral test for lazy-resume fallback FIXED — 3 behavioral MergeSessionEntries tests + 1 structural guard added (commit c459819a0)
3 🟢 TryAdd vs indexer inconsistency FIXED — All 3 sites now use _closedSessionIds[id] = 0
4 🟢 Defensive null guard on validated input ACCEPTED — Harmless defensive guard, kept as-is
5 🟢 Code duplication across 3 sites ACCEPTED — Helper extraction deferred; pattern is small and clear

Test Quality Assessment

4 well-constructed tests:

  • Merge_LazyResumeFallback_RecoveredFromSessionId_DropsOldEntry — behavioral: verifies merge drops old entry when both RecoveredFromSessionId and _closedSessionIds are set
  • Merge_DoubleRecovery_UsesImmediatePredecessor — behavioral: proves immediate predecessor (B) is correctly linked and dropped after restart (empty _closedSessionIds)
  • Merge_DoubleRecovery_StaleAncestor_CreatesPhantom — behavioral: documents why ??= was wrong — stale ancestor creates phantom
  • LazyResumeFallback_SetsRecoveredFromSessionIdAndClosedIds — structural guard ensuring the production code uses = (not ??=) and the indexer

The 3 behavioral tests cover the merge logic regardless of which call site triggers it. The structural test serves as a supplementary invariant guard against regression — consistent with the project's convention of using structural tests as supplements to behavioral coverage.

No New Issues Found

All 3 reviewers confirmed no new bugs, regressions, security issues, or race conditions.


Recommended Action: ✅ Approve

All prior findings addressed. Code is correct, consistent, and well-tested. Ship it.

PureWeen and others added 2 commits April 14, 2026 12:04
… consistency

Review findings addressed:
1. ??= → = on RecoveredFromSessionId: in double-recovery (A→B→C), ??= preserves stale ancestor A instead of immediate predecessor B, breaking CanSafelyDropSupersededGroupMoveEntry after restart.
2. TryAdd → indexer: matches every other _closedSessionIds write site in the codebase.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds 4 tests:
1. Merge with RecoveredFromSessionId + _closedSessionIds drops old entry
2. Double-recovery (A→B→C) with immediate predecessor drops correctly
3. Double-recovery with stale ancestor proves why ??= was wrong
4. Structural guard that EnsureSessionConnectedAsync uses = (not ??=)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@PureWeen PureWeen merged commit 6d3306b into main Apr 14, 2026
@PureWeen PureWeen deleted the fix/lazy-resume-phantom-previous branch April 14, 2026 18:34
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.

1 participant