fix(terminal): correct attachExisting reentrancy supersession check (W3)#246
Conversation
attachExisting() guarded against a rapid re-attach to a different session with
`if Task.isCancelled { return }` after awaiting the tail snapshot — but it never
stored/cancelled its own Task, so isCancelled was ALWAYS false. A reconnect to
session B while A's snapshot was in flight flushed A's stale snapshot into B's
freshly-installed reattach buffer (garbled/duplicated output). Gemini deep-review
catch.
The adapter is @mainactor, so `state` after the await is the authoritative
supersession signal. Replaced the dead check with a pure nonisolated static
reattachSuperseded(state:targetSessionId:) predicate (superseded unless state is
still .running for the same id), unit-tested without a WKWebView.
swift test full: 1825 tests, 0 failures.
Deferred (P2, per reviewers): window.pushChunk WKContentWorld isolation — needs
an index.html/TerminalView restructure + on-device render validation; the nav
guard already blocks off-bundle navigation. Helper-not-running UX already shipped
in W1-B (#244).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 33d6d341b0
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if Self.reattachSuperseded(state: state, targetSessionId: sessionId) { | ||
| return sessionId | ||
| } | ||
| paintReattachSnapshot(snapshot) |
There was a problem hiding this comment.
Preserve cancellation for same-session reattach
TerminalAttachSession.doAttach cancels the previous attachTask before every reconnect, but reconnects reuse the same sessionId. If the canceled attach's getTailSnapshot resumes after the new same-session attach has set state back to .running(sessionId:), this state-only guard treats the canceled attach as current and falls through to paintReattachSnapshot, flushing the newer reattachBuffer before its own snapshot arrives. That reintroduces interleaved/garbled terminal output on same-session reconnects; keep Task.isCancelled (or an attach generation token) in addition to the session-id check.
Useful? React with 👍 / 👎.
What & why
Follow-up to #243/#244/#245.
TerminalSessionAdapter.attachExisting(to:sessionId:)subscribes to the live raw stream,awaits the tail snapshot, then paints. To stop a rapid re-attach to a different session from clobbering the in-flight one, it checkedif Task.isCancelled { return }after the await — but it never stores or cancels its ownTask, soTask.isCancelledwas alwaysfalse. A reconnect to session B while A's snapshot was in flight flushed A's stale snapshot into B's freshly-installed reattach buffer → garbled/duplicated terminal output. (Gemini deep-review 🔴.)Fix
The adapter is
@MainActor, so the snapshotawaitis the only suspension point andstateafter resume is the authoritative supersession signal. Replaced the dead check with a purenonisolated staticpredicate:attachExistingbails before painting iffreattachSuperseded(...).nonisolated+ pure so it's unit-tested without a WKWebView (the XCTest host aborts on WKWebView creation outside an AppKit loop — same reason render is a manual gate).Tests
TerminalSessionAdapterTests(W3): same running session → not superseded; different running session → superseded; every non-running state → superseded.swift testfull: 1825 tests, 0 failures.Deferred (P2, per reviewers)
window.pushChunkWKContentWorld isolation — non-trivialindex.html/TerminalViewrestructure; render changes need the W2 on-device smoke to validate. TheTerminalNavigationGuardalready blocks off-bundle navigation (defense-in-depth). Tracked separately.🤖 Generated with Claude Code