Skip to content

fix(window): preserve scoped session delivery across window transitions - #935

Merged
Juliusolsson05 merged 6 commits into
mainfrom
fix/session-window-routing
Sep 20, 2026
Merged

Juliusolsson05 merged 6 commits into
mainfrom
fix/session-window-routing

Conversation

@Juliusolsson05

@Juliusolsson05 Juliusolsson05 commented Sep 12, 2026

Copy link
Copy Markdown
Owner

A session without a window owner previously broadcast its content to every window. Closing/reloading windows and delayed cleanup could also lose final output or release a newer owner. This change requires an explicit claim for session delivery and makes unrecoverable observation gaps visible in the affected pane.

  • Claims carry window-renderer generation and revision. Recovery claims the display only after SessionManager validates the target; stale releases, conflicting-window close requests and obsolete handoff acknowledgements cannot revoke a successor.
  • Recognized transition traffic retains an ordered cloned queue under per-session and application item/estimated-byte/age limits. Unknown traffic keeps bounded metadata only. Expired metadata retains conservative evidence in a fixed-size Bloom filter; it never grants routing authority.
  • Read-only refresh flushes existing coalescers before seeding available snapshots, then validates owner/run/native-selection/locator evidence around saved-history reads. The real renderer history mapper is reused with stale-update rejection. Refresh preserves drafts and a visible transient-output warning; it never restarts a backend, submits input, or treats a raw PTY tail as a checkpoint.
  • Natural backend exit retains pane ownership until explicit disposal. Recorder control edges remain outside the lossy observation queue. Shared history/gap contracts live outside preload, with the old history type re-export preserved.

Validation:

  • 116 distinct unit cases across seven focused files: a 114-test serial run passed, then the final two close-admission cases passed with all 15 composed registry/forwarder/IPC cases. Coverage includes the real SessionManager admission path and the existing Codex replacement suite.
  • 14 distinct renderer cases across three files: the 13-test run passed, then all eight repair-hook cases passed with the final native-selection case. These exercise the real history mapper and shared header, including late replies, early notices, deduplication and draft preservation.
  • Full typecheck and final incremental typecheck passed; test contract, all seven package-pin checks and diff whitespace checks passed.
  • npm run test:package passed on the final implementation, including desktop/remote builds, the universal hotkey helper and required-entry-point verification.
  • Reviewed the complete diff and all session-routing call sites. Concurrent local tests hit initial dynamic-import timeouts and contaminated subsequent mocks; serial runs passed without increasing timeouts or changing production behavior.

Limits: no live multi-window/PTY smoke was performed. Queue byte limits are admission estimates, not measured heap. Native source evidence uses the current manager's observable fields; unobserved native resets, in-place transcript rewrites and producer epochs across the remaining coalescers are still B12 work under #918. History is a bounded suffix, and missing transient output remains explicit. Runtime archives were not downloaded, so build verification is not a signed-release smoke test. No package pin changed.

Fixes #920
Refs #918

Independent B01 slice of #931, based on 552914f5; does not include or assume merge of tmux PR #933. The focused implementation plan is the branch's first commit. Main subsequently advanced to 115e26fc via the skills/TLDR features; the changed composition lines were inspected and do not overlap this routing wiring. CI must verify the current PR merge revision. Nothing is merged by this PR creation.

CI verification: both quality-gate and minimum-node-fixture-gate passed on head 150328459a6ac392e54070e4a66e4c7d340c2c65, run 34718213663. Review-ready; no merge performed.

Keep unknown-owner observations out of unrelated windows and retain bounded, ordered delivery only for recognized view lifetimes. Revisioned claims protect reload, handoff and delayed cleanup; recovery grants a display claim only after backend target admission. Read-only repair reseeds observable state and scoped history while keeping transient loss visible.

Refs #920

Refs #918
Juliusolsson05 and others added 3 commits September 19, 2026 18:34
Two conflicts, both from the unified stage (#1013):

- PaneHeader: main deleted the related-agent strip and with it this
  header's only store read. The routing-gap notice this branch adds is
  the remaining reader, so it keeps the store import and subscribes to
  exactly one rare object on the displayed session.
- ARCHITECTURE 8.2.1: main rewrote the section around a per-channel IPC
  table and kept the old sentence saying a session without recorded
  ownership is broadcast to every window. That fallback is the thing
  this branch removes, so its paragraphs replace that sentence and the
  table stays.

Also: the routing-recovery test's WorkspaceState fixture still named
`dispatchMode`, `detachedSessions` and `buried`, all gone with the
unified stage, and `stage` is now required.

Verified on the merge: npx tsc -b clean; the branch's six suites pass
(78 tests) and the tile-tree suites pass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ent repair stops offering itself

Codex review of #935, two findings:

1. P1. session:kill-owned released the display claim whatever the
   manager answered. killOwned returns false for two different
   situations: nothing left to close, and "you do not own that
   backend" — a stale pane whose saved cwd or provider no longer
   matches. In the second, releasing revoked a RUNNING session's only
   owner, and since this branch removes the broadcast fallback its
   output was quarantined with no owner left to even show the gap: the
   pane went quiet. The release now needs the close to have happened,
   or the backend to be gone, which is the same evidence killOwned
   checks. The composition test drives the real manager through a
   refused close and then a real one.

2. P2. A successful refresh acknowledges and DELETES the main-process
   gap ticket, so pressing "Refresh view" again could only return
   stale — permanently so once an ordinary recovery (a terminal remount)
   minted a new ownership revision. The pane keeps the warning, because
   that output is gone for good, but stops offering a repair it cannot
   perform; a later incident issues a new ticket and the control returns
   with it.

The existing refresh test asserted a second click re-ran the repair,
which held only because its resync mock ignores the ticket. It now pins
the real contract: no control after a spent repair, the control back on
a new gap, and no duplicated entries when the repair replays.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ent only on a finished repair

Codex delta review of #935:

1. P1. My previous fix asked for a backend snapshot to decide "nothing
   left to close". Mid-handoff a Codex predecessor HAS no snapshot while
   its replacement reservation still owns it, and if the successor's
   startup then fails, compensation restores the predecessor — whose
   display claim had already been released, so its output went nowhere.
   SessionManager now answers the question directly:
   retainsSessionOwnership consults the same four tables killOwned
   does (live entries, recoveries in flight, replacement reservations,
   redirects).
2. P2. The resync handler acknowledged — and therefore DELETED — the
   repair ticket before the renderer had read saved history. A failed
   read then left the pane on "refresh is unavailable" with a button
   whose ticket was gone, and an ordinary recovery rotated ownership so
   every later press returned stale. The ticket is now spent where the
   repair actually completes: on a seed with no history to read, or on
   a successful history read.

Tests: a reserved-but-snapshotless session keeps its claim through a
refused close; the seed keeps the ticket and the failed read keeps it
while the successful read spends it. Both fail against the previous
behaviour.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@Juliusolsson05
Juliusolsson05 merged commit 67f30e1 into main Sep 20, 2026
2 checks passed
@Juliusolsson05
Juliusolsson05 deleted the fix/session-window-routing branch September 20, 2026 02:19
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.

bug(windows): avoid broadcasting session content when ownership is unknown

1 participant