# ask-gpt parallel browser seats

Status: DONE
Priority: 7
Source request: 2026-08-10 owner: "can we make ask-gpt run in parallel a few sessions together? in separate browsers using the same auth?"
Task IDs: (assign on approval)

## Outcome

`ask-gpt` serves N concurrent prompts, each in its own LibreWolf instance, all
authenticated from the owner's one LibreWolf profile snapshot. Today every run
serialises on one `profile.lock` + one profile copy + fixed Marionette port 2828;
a second `ask-gpt` blocks up to 600 s.

## Why it works with the same auth

Auth is cookies inside the profile snapshot. N independent snapshots of the same
source profile = N logged-in browsers, which ChatGPT treats like multiple devices.
Ports are already parameterised (`Session(port=...)`, `free_port()` exists); Xvfb
already self-allocates displays. The single-seat constraint is entirely
self-imposed serialisation, not a platform limit.

## Design (seat pool in solwebd)

- `modules/gptbridge/browser.py`: profile slots `~/.overdeck/gptbridge/browser-profile-<n>`
  each with its own `profile-<n>.lock`, own Marionette port via `free_port()`,
  own download dir (downloads are collected by scanning the seat's dir — shared
  dir would cross-wire files between seats). `reap_orphans` keyed per slot path.
- `solwebd.py`: `SeatPool(max_seats=N)` replacing the single `BrowserSeat` +
  global `Engine.lock`. `/v1/ask` grabs any free seat (per-seat lock);
  agentic `/v1/chat` conversations stay pinned to the seat that opened them
  (composer state is per-browser). Queue when all seats busy.
- Direct (daemon-down) `ask_gpt.py` path: allocate first free slot instead of
  the fixed singleton; falls back to waiting on slot 1 only when all N locked.
- Cap handling: `Capped` is account-wide — one seat detecting a cap marks the
  pool capped (with expiry), so N seats don't each burn a turn discovering it.
- Config: `--seats N` on solwebd, default 1 (today's behavior), owner opts into
  2–3. Each seat ≈ one LibreWolf + Xvfb (~0.5–1 GB RSS) — cap enforced, and the
  laptop-load rules apply (seats > 2 should route via buildbox policy review).

## Risks / must-verify

1. Session-cookie rotation: ChatGPT rotates `__Secure-next-auth…` tokens. Two
   snapshots rotating independently from the same seed must not invalidate each
   other. Verify with 2 seats live simultaneously for >1 h before defaulting on.
2. Cloudflare clearance is per-snapshot; a fresh slot faces the bot check once.
   Slots keep their copies across runs (same reseed semantics as today).
3. Shared quota: parallel turns drain the same subscription pool faster;
   `detect_cap` per turn already guards.

## Acceptance

- Two `ask-gpt` invocations complete concurrently (wall time ≈ max, not sum).
- `health.sh` + `modules/gptbridge/tests/` green, incl. new pool tests
  (seat reuse, pin-to-seat for agentic sessions, account-wide cap fan-out,
  orphan reap scoped to one slot).
- Default (1 seat) behaves byte-identically to today.

## Current receipt

Implemented (16a5847a2) by sonnet subagent; main-session review found and fixed a
same-conversation cross-seat race + queue-depth regression (b5c663e58). Tests
142 passed / 1 skipped; health.sh exit 0 (`-W error` leak is pre-existing on the
parent commit, out of scope). INSTALLED into the live deploy clone 2026-08-10
under /od-emergency (owner blocked by a 2h pro run holding the single seat);
rollback at ~/.cache/gptbridge-rollback-20260810-2354. Installed-entrypoint
proof: with slot 1 flocked by the live pro run, a Session self-picked slot 2,
own Marionette port, reached chatgpt.com, clean exit. ship.sh land re-running
(first attempt failed on a stale/flaky repo-wide gate; suite green on re-run
with latest main merged).

## Next executable action

None — DONE 2026-08-11. Landed 07235bd6b (seats + MAX_SLOTS=15 + resume/live
spec), deploy-local.sh reports deployed at that sha, installed tests 142 green,
ceiling 15 confirmed via installed module. Live proof: 6 seats concurrently
locked by real runs; fresh session acquired slot 8 (refutes the reported
"7-profile ceiling" — profiles materialize on first use). Risk #1 (long
same-auth soak) is being exercised by real multi-seat usage; watch for logouts.

Dispatch rate limiter (spec §3b, owner-demoted to lowest priority 2026-08-11,
"still works anyway, so its ok. its a goot2have not urgent") implemented
2026-08-11 on branch wt/rate-limiter, commit 620494afc: `chat._rate_limit_dispatch`
gates only `submit()` (the sole prompt-dispatch point for both CLI and daemon
paths) behind a global cross-process `~/.overdeck/gptbridge/dispatch.stamp` +
`dispatch.lock` flock — `random.uniform(60, 75)` jittered gap, stamp committed
just before the send click, lock never held across the sleep. Implemented by
codex gpt-5.6-terra/medium via /ask-codex, then self-reviewed (not codex):
found and fixed one real gap — the ratelimit event bypassed `_emit()` and so
omitted the `phase` key every other stream event carries (`ask_gpt.py:261`,
`solwebd.py:564` both key off it); fixed by threading `turn` through
`submit()`/`_rate_limit_dispatch()`. Tests: 236 passed, 1 skipped (231 baseline
+ 5 new in test_dispatch_rate_limit.py), read-only paths (listing, download,
navigation) confirmed to never call `_rate_limit_dispatch`. Not yet landed to
main or deployed.
