refactor: Move-Based Server-Scoped Boards (Pin Sessions)#233
Conversation
Replace the per-WebSocket relay ephemeral (rk-relay-*) + @rk_board server-option encoding with a move-based, server-scoped pin-session board model. Net ~830 lines removed. Every window lives in exactly one place: a home session (SESSIONS view, direct relay attach) or moved into its own single-window _rk-pin-<windowID> session (BOARDS view). Removing window *sharing* removes the need for the per-connection ephemeral isolation layer. Board membership is derived from _rk-pin-* sessions plus session vars @rk_board / @rk_home / @rk_board_order (fractional ComputeOrderKey). No DB (Constitution II). Boards are server-scoped, empty boards vanish, and pins persist across rk restarts (no restore-sweep). Deleted: relay ephemeral path, NewGroupedSession, @rk_owner_pid stamping, cmd/rk/serve_sweep.go, and the @rk_board encoding + cross-server union + lazy/eager cleanup. Intended behavioral changes: pinned windows leave their home session's tab list until unpinned; multi-client active-window collisions on a shared real session are accepted. The _rk-ctl anchor + exit-empty off backstop are untouched.
There was a problem hiding this comment.
Pull request overview
Refactors run-kit’s “boards” and relay architecture to remove per-WebSocket rk-relay-* ephemerals and the @rk_board server-option encoding, replacing them with a move-based, server-scoped “pin session” model (_rk-pin-*) where each pinned window is moved into its own single-window tmux session and relayed via direct attach.
Changes:
- Introduces
_rk-pin-<id>pin-session naming + helpers, and filters pin-sessions from user-facing session lists. - Rewrites board membership to be derived from pin-sessions + session vars (
@rk_board,@rk_home,@rk_board_order) and updates SSE/relay to match the new model. - Updates frontend docs/contracts and e2e specs to reflect server-scoped board derivation and direct relay attach.
Reviewed changes
Copilot reviewed 34 out of 34 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| fab/changes/260602-qn62-move-based-board-pin-sessions/plan.md | Design/acceptance plan for pin-session refactor and removals |
| fab/changes/260602-qn62-move-based-board-pin-sessions/intake.md | Intake rationale and decisions for move-based pin sessions |
| fab/changes/260602-qn62-move-based-board-pin-sessions/.status.yaml | Change pipeline status metadata |
| fab/changes/260602-qn62-move-based-board-pin-sessions/.history.jsonl | Pipeline history/event log |
| docs/memory/run-kit/ui-patterns.md | Documents SESSIONS-vs-BOARDS exclusivity and unchanged board rendering path |
| docs/memory/run-kit/index.md | Updates memory index entry for tmux sessions doc scope |
| docs/memory/run-kit/architecture.md | Updates API/relay/boards/SSE architecture documentation for pin-sessions |
| app/frontend/src/api/boards.ts | Updates board API doc comments for pin-session model |
| app/frontend/src/hooks/use-boards.ts | Updates subscription comments for server-scoped derivation + multi-server summary |
| app/frontend/tests/e2e/boards-same-session-multi-pane.spec.ts | Updates e2e assertions/docs for move-based isolation |
| app/frontend/tests/e2e/boards-same-session-multi-pane.spec.md | Companion doc updated for pin-session model |
| app/frontend/tests/e2e/boards-multi-server.spec.ts | Updates cleanup semantics for persistent pin-sessions |
| app/frontend/tests/e2e/boards-multi-server.spec.md | Companion doc updated for server-scoped pins + unioned board-name view |
| app/frontend/tests/e2e/boards-mobile.spec.ts | Updates cleanup semantics for persistent pin-sessions |
| app/frontend/tests/e2e/boards-mobile.spec.md | Companion doc updated for pin-sessions cleanup semantics |
| app/frontend/tests/e2e/boards-desktop-suspend.spec.md | Companion doc updated for pin-sessions cleanup semantics |
| app/backend/internal/tmux/tmux.go | Adds pin-session helpers, filters pin sessions in parseSessions, removes relay sweep/ephemeral utilities, adds ListPinSessionNames |
| app/backend/internal/tmux/tmux_test.go | Updates parseSessions and ResolveWindowSession tests for new model |
| app/backend/internal/tmux/active_window_test.go | Updates baseGroupName tests after relay-ephemeral removal |
| app/backend/internal/tmux/board.go | Rewrites boards to be pin-session derived; implements Pin/Unpin/Reorder with move-based semantics |
| app/backend/internal/tmux/board_test.go | Rewrites board tests for new pin-session behavior and invariants |
| app/backend/internal/tmux/socketsweep_test.go | Removes owner PID option tests (relay sweep deleted) |
| app/backend/internal/tmux/reaper.go | Removes stale sweep reference from comment |
| app/backend/api/router.go | Removes grouped-session/owner PID ops from TmuxOps interface and prod wiring |
| app/backend/api/sse.go | Removes board bootstrap/cleanup logic; keeps only explicit board-changed events |
| app/backend/api/sse_test.go | Deletes tests covering removed board bootstrap/cleanup behaviors; updates real-session filtering test |
| app/backend/api/sse_subscriber_test.go | Deletes pure-function kill-diff test (function removed) |
| app/backend/api/sessions_test.go | Removes grouped-session/owner PID fields/methods from mocks |
| app/backend/api/relay.go | Removes ephemeral relay creation and attaches directly to resolved session |
| app/backend/api/relay_test.go | Updates relay tests for direct-attach behavior and absence of ephemerals |
| app/backend/cmd/rk/serve.go | Removes relay startup sweep and updates supervisor startup comment |
| app/backend/cmd/rk/serve_sweep.go | Deleted: orphaned relay sweep implementation |
| app/backend/cmd/rk/serve_sweep_test.go | Deleted: tests for relay sweep predicates and scoping |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if _, err := tmuxExecRawServer(ctx, server, "has-session", "-t", pinSession); err == nil { | ||
| current, readErr := showSessionOption(ctx, server, pinSession, BoardOption) | ||
| if readErr != nil { | ||
| return fmt.Errorf("read %s on existing pin %q: %w", BoardOption, pinSession, readErr) | ||
| } |
There was a problem hiding this comment.
Fixed — Pin now stamps @rk_home/@rk_board/@rk_board_order BEFORE moving the window into the pin-session, so a window can never enter a pin-session that lacks @rk_home (a stamp failure kills the still-empty pin-session; nothing is stranded). The idempotent re-pin path now also re-stamps @rk_board and repairs a missing @rk_board_order. (4eed733)
| // Double-fault guard: only kill the pin-session if the move-back SUCCEEDED. If the | ||
| // move-back itself fails the window is still physically inside the pin-session; | ||
| // killing it would destroy a live window. Leaving the (still-named) pin-session | ||
| // keeps the window recoverable — the pin is unpinnable by window id, and the next | ||
| // same-window Pin sees the existing session and re-stamps via the idempotent path. |
There was a problem hiding this comment.
Fixed — chose the ordering fix you suggested: stamp-before-move makes a window-moved-but-unstamped state impossible, so the double-fault rollback is gone entirely and @rk_home durability is guaranteed at creation rather than via recovery. As a backstop, Unpin now recovers an @rk_home-less pin-session by renaming it to recovered instead of hard-failing, so a window is never stranded. (4eed733)
| } | ||
| if !ValidBoardName(board) { | ||
| return fmt.Errorf("invalid board name") |
There was a problem hiding this comment.
Fixed — Unpin now reads the pin-session's @rk_board and treats a mismatch with the requested board as a no-op success, so a stale/mismatched /api/boards/{name}/unpin neither unpins the window nor causes the handler to emit a board-changed SSE event for a board the window was never on. (4eed733)
| // Select the window on its real session so the attach renders the right | ||
| // window. The accepted tradeoff (#1 in the intake): the real session has a | ||
| // single active-window pointer shared across attachments, so multi-client | ||
| // navigation mutates the real session's active window. For a pin-session this | ||
| // is a no-op — its sole window is permanently active. | ||
| if err := s.tmux.SelectWindow(windowID, server); err != nil { | ||
| slog.Error("select-window failed", "err", err, "session", session, "windowID", windowID) |
There was a problem hiding this comment.
Fixed — handleRelay now uses the session-scoped SelectWindowInSession(session, windowID, server) (the resolved owning session) instead of a bare SelectWindow, so the selected active window matches the session attach-session -t attaches to, group-safe. (4eed733)
| // Filter run-kit's single-window board pin-sessions from every | ||
| // user-facing session list. A pinned window is physically MOVED into | ||
| // its `_rk-pin-*` session, so it leaves its home session's tab list; | ||
| // the pin-session itself is never a user-facing SESSIONS entry (it is | ||
| // rendered only as a BOARDS pane). This is the single chokepoint — every |
There was a problem hiding this comment.
Fixed — good catch, this was the CI failure (boards rendered empty). handleBoardGet (and windowExistsOnServer) now resolve each pinned window via its own pin-session directly — tmux.PinSessionName(windowID) then ListWindows(pinSession), a by-name target query that is NOT subject to the parseSessions _rk-pin-* filter — instead of scanning the filtered ListSessions. Added a session-aware mock + regression test. (3bd1f04)
handleBoardGet built its live-window join by scanning the user-facing
ListSessions, which the move-based board change taught to hide _rk-pin-*
sessions. Since pinned windows now LIVE in those hidden sessions, the join
matched nothing and GET /api/boards/{name} returned [] — the board rendered
zero panes (.xterm count 0). Fixed by joining each board entry against its
own pin-session directly via tmux.PinSessionName(windowID) -> ListWindows,
a by-name target query not subject to the session-list filter.
Also fix windowExistsOnServer to check the pin-session, so re-pinning an
already-pinned window to a different board reaches tmux.Pin's re-stamp path
instead of a spurious 404.
Adds a session-aware mock (listWindowsBySession) and a regression test
asserting the board renders its entry when the window lives only in its
pin-session — the integration gap that let CI catch what unit mocks could not.
Verified: just test-backend green; the 6 previously-failing board/relay e2e
specs (boards-pin-flow, boards-same-session-multi-pane, boards-multi-server,
boards-mobile, boards-desktop-suspend, shell-rotation) now pass locally.
Address Copilot PR-review findings, several of which interacted with the earlier rework: - Pin stamps @rk_home/@rk_board/@rk_board_order BEFORE moving the window into the pin-session. The window can no longer enter a pin-session that lacks @rk_home, which eliminates the prior double-fault 'un-unpinnable' state and removes the rollback-move complexity: a stamp failure simply kills the still-empty pin-session and the window is untouched in its home. - Idempotent re-pin re-stamps @rk_board and repairs a missing @rk_board_order. It does NOT attempt to re-derive @rk_home (impossible once the window lives in its pin-session); durability is guaranteed by stamp-before-move instead. - Unpin no-ops on a board-name mismatch, so the handler never emits a false board-changed SSE event for a board the window was never on. - Unpin recovers an @rk_home-less pin-session by renaming it to recovered<id> rather than hard-failing — a window is never stranded. - handleRelay uses session-scoped SelectWindowInSession instead of a bare SelectWindow, so the selected window matches the session it attaches to (group-safe). Adds TestUnpin_BoardMismatchIsNoOp and TestUnpin_HomelessPinRecoversWindow. just test-backend green; the 6 board/relay e2e specs pass locally.
Meta
Pipeline: intake ✓ → apply ✓ → review ✓ → hydrate ✓ → ship → review-pr
Summary
Replace the per-WebSocket relay ephemeral (
rk-relay-*) +@rk_boardserver-option encoding with a move-based, server-scoped pin-session board model. Net ~830 lines removed. The ephemeral isolation layer and the@rk_boardencoding were two independent subsystems carrying the bulk of the relay/board complexity; removing window sharing removes the root cause of the isolation requirement, letting the common case (a single terminal) be the dumb thing — a direct attach to a real session.Changes
_rk-pin-<windowID>session (BOARDS view)._rk-pin-*sessions plus session vars@rk_board/@rk_home/@rk_board_order(fractionalComputeOrderKey). No DB (§II). Boards are server-scoped, empty boards vanish, and pins persist across rk restarts (no restore-sweep).NewGroupedSession,@rk_owner_pidstamping,cmd/rk/serve_sweep.go, and the@rk_boardencoding + cross-server union + lazy/eager cleanup._rk-ctlanchor +exit-empty offbackstop are left as-is.Intended behavioral changes
Verification
just test-backend— passjust test-frontend— passtsc --noEmit— passgo build ./...— compiles cleanNote:
just buildandjust test-e2ecould NOT run in the authoring environment — theVERSIONfile is absent (also absent onmain) andairis not onPATH. These are environment-provisioning gaps, not code issues. CI should run both.