web: a second browser tab silently doubles terminals per target, halving the effective cap
- Dominant language
- Python
- Stars
- 2
- Forks
- 0
- Avg merge
- 15h 59m
- Merged PRs (30d)
- 9
Description
## Symptom
Opening the `remo web` console in a second browser tab silently doubles terminal
consumption. Eight panes in two tabs is sixteen live PTYs, sixteen `ssh`
processes, and sixteen cap slots — which is exactly the default
`terminal_cap_per_client`, so the second tab is enough on its own to lock the
operator out with `cap_reached` (see #176 for the diagnostics half of this).
Nothing warns about it, and the duplication is invisible from the UI: both tabs
look correct, because both *are* correct in isolation.
## Why it happens
Three independent behaviours compose into it:
1. **The layout is shared, so the second tab restores the same panes.**
`frontend/src/state/workspace.ts` persists `panes` + `focusedId` to
`localStorage` (FR-034), which is per-origin, not per-tab. A new tab therefore
comes up with the *identical* pane set rather than an empty one.
2. **Each pane mounts its own connection.** Every `TerminalCard` drives a
`TerminalConnection`, whose `attach()` calls `createTerminal()` on mount.
3. **The service never dedups.** `TerminalRegistry.register()`
(`web/terminal_registry.py:108-146`) mints a fresh `terminal_id` per call
with no lookup by `session_target_id`, so two requests for the same target
produce two independent attachments — and two `ssh … remo-host sessions
attach` process groups against one remote Zellij session.
Worth noting the remote side survives this: `remo-host sessions attach` joins the
existing Zellij session, so the two tabs show the same content and neither
corrupts the other. The cost is entirely local — doubled PTYs, doubled ssh, and
a cap that empties twice as fast as the operator expects.
## The actual decision
This is not obviously a bug, which is why it deserves a deliberate answer rather
than a patch. Three defensible designs:
- **A — Share the attachment.** `register()` returns the existing live
attachment when one already exists for that `session_target_id` + `client_id`.
Cheapest on resources and matches the operator's mental model ("one terminal
per target"). But the WS token is single-use and one PTY now has two readers:
output fan-out, resize arbitration (two tabs, two viewport sizes, one
`TIOCSWINSZ` — see #174), and close semantics ("whose DELETE wins?") all have
to be designed. This is a real feature, not a tweak.
- **B — Keep them independent, make the cost visible.** Leave the model alone and
fix the surprise: count duplicates honestly in the `cap_reached` message
(#176), and optionally badge a pane that is already open elsewhere. Smallest
change, keeps per-tab resize correct, and is arguably the honest answer given
the caps exist precisely to bound this.
- **C — Don't restore panes into a second tab.** Scope the *live* pane set per
tab (`sessionStorage`) while keeping the saved layout in `localStorage`, so a
new tab opens empty and the operator chooses what to attach. Removes the
silent doubling at its source, at the cost of changing restore behaviour that
FR-034 deliberately specified.
My weak preference is **B now, C considered separately** — A is the only one that
needs new protocol design, and the multi-reader resize problem is exactly the
class of bug #174 just fixed.
## Acceptance
Whichever direction is chosen:
- The behaviour is documented in `docs/web-session-interface.md` — today the
duplication is undocumented in either direction.
- An operator with two tabs open can tell, from the product, that they have two
attachments per target.
- If A is chosen: resize arbitration, output fan-out, and close semantics are
specified before implementation, and the single-use token model
(`web/tokens.py`) is revisited.
Found while diagnosing a `cap_reached` report; the reporter had two consoles
open and no way to see that it mattered.
Contributor guide
Research direction
Read frontend/src/state/workspace.ts and web/terminal_registry.py:108-146 to trace layout restoration and attachment creation, then review web/tokens.py, docs/web-session-interface.md, and the constraints referenced in #174 and #176. First establish which of A, B, or C is accepted; done means the selected behavior is documented and an operator with two tabs can tell that attachments are duplicated.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python, typescript
- Domain
- backend, frontend
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Needs clarification
- Newbie friendliness
- 28/100