Skip to content

fix(session): observe resume picker rejections without process kill (#3804) - #3836

Merged
Yeachan-Heo merged 2 commits into
Yeachan-Heo:devfrom
innocarpe:fix/issue-3804-resume-picker-fmt
Aug 5, 2026
Merged

fix(session): observe resume picker rejections without process kill (#3804)#3836
Yeachan-Heo merged 2 commits into
Yeachan-Heo:devfrom
innocarpe:fix/issue-3804-resume-picker-fmt

Conversation

@innocarpe

Copy link
Copy Markdown
Contributor

Summary

Successor to closed PR #3830 addressing the single REQUEST_CHANGES blocker.

Managed-candidate preparation can reject before the inner switchSession() catch. The session picker dispatched resume through a void onSelect, so that rejection escaped as an unhandled promise rejection and could kill the process (observed on Windows upgrade resume).

The previous exact head was behavior-correct (identity fence preserved, no auto-retry, rejections observed, current session remains usable). CI/review only blocked on Biome formatter wrapping in two new test files.

Changes

Test plan

  • Focused selector/resume tests pass
  • Biome format matches review diffs (single-line expects)
  • Live interactive TUI resume race (not run)

Notes

…eachan-Heo#3804)

Managed-candidate preparation can reject before switchSession (identity fence).
The picker dispatched resume through a void onSelect, so that rejection escaped
as an unhandled promise rejection and could kill the process. Catch at the UI
dispatch boundary, surface via showError, and keep the active session usable.

Lore-id: 3804a1b2
Constraint: preserve strict identity fence -- no auto-retry of changed candidate
Constraint: keep current session usable after resume preparation failure
Rejected: weaken identity validation | would admit races into migration
Rejected: swallow all handleResumeSession errors | direct callers need propagation
Confidence: high
Scope-risk: narrow
Reversibility: easy
Tested: picker race showError + no unhandledRejection; component async onSelect recovery; existing resume reentrancy suite
Not-tested: live interactive TUI end-to-end against a real filesystem race
Successor to closed Yeachan-Heo#3830. Exact-head review accepted the identity-fence
and unhandled-rejection recovery; the only blocker was biome formatter
wrapping of three expect() calls that fit the 120-col limit.

Lore-id: 3804fmt01
Constraint: no behavior change -- formatter-only
Confidence: high
Scope-risk: narrow
Reversibility: easy
Tested: focused resume/selector suites; biome format clean
Not-tested: live interactive TUI resume race

Fixes Yeachan-Heo#3804

@Yeachan-Heo Yeachan-Heo left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Red-team read-only review: async rejection observation / no retry / identity fence

Head SHA verified: 8e125acc4644529ee6964a60c90bbfa8ab8a0c2d

Scope

5 files (2 source, 3 test). This PR is the successor to closed #3830: a74d8a326 provides the behavior fix, 8e125acc4 is the biome formatting follow-up.

Validation performed

1. Async rejection observation — ✅
session-selector.ts:250-264 introduces #dispatchSelect() which wraps this.onSelect(sessionPath) in void Promise.resolve(...).catch(...). The catch handler:

  • If picker is "settled": surfaces error via #showError + #requestRender (host already closed the picker)
  • Otherwise: resets to "browsing" state, unfreezes input, shows error, requests render

This prevents async onSelect rejections from becoming process-killing unhandled rejections. Verified by test observes async onSelect rejections without unhandled rejection (#3804):

  • expect(unhandled).not.toHaveBeenCalled() — no unhandled rejection
  • expect(text(component)).toContain("Error: ...") — error surfaced in UI
  • expect(text(component)).toContain("one") — picker stays usable

2. No retry — ✅
selector-controller-session-delete.test.ts:371:

expect(prepareManagedCandidateForStrictAdoption).toHaveBeenCalledTimes(1);

The managed-candidate race (where prepareManagedCandidateForStrictAdoption rejects) is attempted exactly once. No auto-retry of the same candidate. switchSession is NOT called (line 373), and the current session remains active (line 375).

3. Identity fence — ✅
selector-controller-session-delete.test.ts:372:

expect(prepareManagedCandidateForStrictAdoption).toHaveBeenCalledWith(selected.path, "copy-retain", identity);

The identity parameter is passed through to the strict adoption check — the managed candidate is validated against the inspected identity before any migration authority is adopted. When the check rejects (managed session changed), the fence holds: no switch, current session retained.

4. Biome formatting — ✅

bunx biome check <affected test files> → OK

Three expect() calls reformatted to fit within the 120-col limit (the sole purpose of commit 8e125acc4). No behavior change in the formatting commit.

5. Test execution — ✅

bun test session-selector-resume-confirm.test.ts selector-controller-session-delete.test.ts → 20 pass, 0 fail

6. CI status — ✅
17 SUCCESS, 6 SKIPPED, 0 FAILURE. Mergeable.

Verdict

The async rejection observation mechanism is correct: all onSelect return values (sync or async) are observed via Promise.resolve().catch(), preventing unhandled rejections. The identity fence and no-retry contract are enforced by the strict adoption path. Formatting-only follow-up is clean.

Approved.

— gaebal-gajae

@Yeachan-Heo
Yeachan-Heo merged commit c2d800d into Yeachan-Heo:dev Aug 5, 2026
23 checks passed
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