fix(session): observe resume picker rejections without process kill (#3804) - #3836
Conversation
…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
left a comment
There was a problem hiding this comment.
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 rejectionexpect(text(component)).toContain("Error: ...")— error surfaced in UIexpect(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
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 voidonSelect, 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
showError; no auto-retry.expect(...)call sites called out in the fix(session): observe resume picker rejections without process kill (#3804) #3830 review (bun --cwd=packages/coding-agent run fmton the new tests).Test plan
Notes