Per-session tab isolation for multiple local agent sessions (foundation already shipped)
- Dominant language
- TypeScript
- Stars
- 133k
- Forks
- 19.9k
- Avg merge
- 18h 46m
- Merged PRs (30d)
- 26
Description
## Feature: per-session tab isolation for multiple *local* agent sessions
### Motivation
When several local agent sessions run in parallel on one machine (e.g. several `claude` sessions across git worktrees, all using `/browse`), they all drive **one** shared headed browser and **clobber each other's active tab** — navigations, snapshots, and state stomp on whoever else is mid-task. The single shared `~/.gstack/chromium-profile` already forces one Chromium instance, which is fine; the problem is that every local session is the **same client**, so there's no per-session tab to own.
(Context: surfaced while investigating #1953 — the shared-daemon multi-session pain.)
### The isolation primitives already exist (but aren't reachable locally)
1.57.9 has everything needed for *remote paired agents*:
- **Per-tab ownership** — `tabOwnership: Map` (`browse/src/browser-manager.ts:177`).
- **Scoped sub-tokens** — root mints a token for a named client with a `tabPolicy` (`server.ts:2179` "Only the root token can mint sub-tokens"; `/pair` takes `--client `, `cli.ts:839`).
- **`own-only` enforcement** — `checkTabAccess()` lets a non-root client touch only its own tabs; **root bypasses** (`browser-manager.ts:927` `if (clientId === 'root') return true`; `server.ts:1044`).
### The gap
Local `$B` commands always authenticate with the **root** token: `Authorization: Bearer ${state.token}` (`cli.ts:542/852/908`), and root bypasses ownership. So local parallel sessions are indistinguishable — they share the one active tab. The `clientId` / `own-only` path only engages for clients that connect with a *scoped* token, which today means the **remote-agent pairing** flow — not bare local invocations.
### Proposed design (small, reuses what's there)
Three pieces:
1. **Auto-mint a per-session `own-only` sub-token**, `clientId = `, keyed on the session id gstack *already* tracks (`~/.gstack/sessions/$PPID`, created in the skill preamble). Root mints it via the existing `/pair` machinery; cache it (e.g. `~/.gstack/sessions/.token`).
2. **Let the local CLI carry a per-session token** via an env var (e.g. `BROWSE_TOKEN` / `BROWSE_CLIENT_ID`) instead of always using the root `state.token`. **Default stays root** when unset → fully backward compatible.
3. **Auto-open / claim that session's tab** on first use; `own-only` then isolates it. A session's commands route to its own tab; `snapshot`/`goto`/etc. never touch another session's.
### Considerations
- **Shared profile is fine** — tabs in one Chromium share cookies/login, which is usually desired (same app, same auth). No second profile needed (and Chromium's `SingletonLock` precludes one anyway).
- **One daemon** — per-git-root state already resolves to the single running daemon; sessions just present different tokens to it.
- **Cleanup** — reuse the existing session-marker TTL (`~/.gstack/sessions` is already swept at 120m) to expire stale per-session tokens/tabs.
- **Backward compatible** — no env set → root behaviour, unchanged.
### Why it's worth it
Parallel multi-session agent work is increasingly common (worktree fan-out, orchestrators). The foundation is already shipped; this is mostly **wiring** (auto-mint + env-carried identity + auto-tab), not new architecture.
Happy to PR this if the approach sounds right — would want a quick maintainer steer on the env name (`BROWSE_TOKEN` vs a dedicated `BROWSE_SESSION`) and whether auto-mint should be opt-in.
Contributor guide
Assessment
This issue has not been assessed yet.