Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
1e445af
fix(mcp): surface PoolExhausted/PoolShuttingDown error context (P0)
Alajmah Jul 8, 2026
78bdad0
feat(detector): P1 model-aware stall detector state machine
Alajmah Jul 8, 2026
6eaf796
fix(detector): P1 review fixes — state transition, auth-expiry, markers
Alajmah Jul 8, 2026
abb5d14
feat(pool): P1.5 lease accounting instrumentation
Alajmah Jul 8, 2026
470d5f3
fix(pool): P1.5 review fix — double-release skip + legacy caller safety
Alajmah Jul 8, 2026
38ddc01
feat(navigation): P2 staged readiness probe + fast-fail on displacement
Alajmah Jul 9, 2026
77faec5
feat(recovery): ZCode SessionStart hook for bridge auto-recovery
Alajmah Jul 9, 2026
b89a316
fix(navigation): P2 review fixes — displacement debounce + JS error d…
Alajmah Jul 9, 2026
38ff8d8
feat(send): P2.5 send-readiness selector hardening + rich diagnostics
Alajmah Jul 9, 2026
34c82a2
fix(send): scope broad fallback to composer form (ChatGPT review caveat)
Alajmah Jul 9, 2026
4d2f2df
docs: P3 multi-client CDP degradation note + corrected root cause
Alajmah Jul 9, 2026
ffef9f7
fix(reconcile): treat non_text as retryable, not terminal (e2e placeh…
Alajmah Jul 9, 2026
63e0db2
fix(navigation): _is_url_at_conversation now handles project-scoped URLs
Alajmah Jul 10, 2026
23af499
refactor(url): use adjacent-pair match per ChatGPT review
Alajmah Jul 10, 2026
3c7c819
fix(owned-mode): remove adopt fallback — never steal another process'…
Alajmah Jul 11, 2026
255046b
fix(owned-mode): use OwnedTabRequiredError consistently in connect path
Alajmah Jul 11, 2026
c92df8a
fix(pool): closing-slot race — don't discard active_keys for replacem…
Alajmah Jul 11, 2026
b3f8f3b
style: fix lint warnings in new test files (unused imports, sort order)
Alajmah Jul 11, 2026
22840ea
style: fix unused imports in source files (ruff)
Alajmah Jul 12, 2026
5a2869b
fix(composer): P0 text verification round-trip — recursive extractor,…
Alajmah Jul 12, 2026
6dbd022
fix(composer): ChatGPT review — placeholder <br> handling + executabl…
Alajmah Jul 12, 2026
5526583
fix(send): P0 send acknowledgment + diagnostic preservation
Alajmah Jul 12, 2026
f440635
fix(send): ChatGPT review amendments — pre-send baseline, tri-state, …
Alajmah Jul 12, 2026
96161a0
fix(send): ChatGPT review C — move valid_probe_seen below composerPre…
Alajmah Jul 12, 2026
b78e200
style: remove unused test scaffolding (ChatGPT minor note)
Alajmah Jul 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,6 @@ sentinel_flow.json
.rest-proxy.log
.sse-server.log


# ChatGPT collaboration binding (local only)
.chatgpt-collaboration.json
45 changes: 43 additions & 2 deletions docs/deployment.md
Original file line number Diff line number Diff line change
Expand Up @@ -287,8 +287,49 @@ W2A_PARALLEL_TABS=1 chatgpt-web2api --port 8082 --cdp-port 9222

> **Rollout warning:** do not mix old (port-lock-only) workers and new
> (`parallel_tabs=true`) workers on the **same CDP port** during a rolling
> upgrade — their lock files don't exclude each other. Finish the rollout on a
> port before enabling parallel mode on it.
> upgrade — their lock files don't exclude each other. Finish the rollout on
> a port before enabling parallel mode on it.

### Other CDP clients on the same Chrome (Super-Browser, Playwright, etc.)

The bridge creates **owned tabs** (`tab_mode="owned"`, the default) — each
driver gets its own dedicated tab via `Target.createTarget` and its own
websocket. A `Page.navigate` issued by one CDP client goes over that client's
websocket to that client's tab; it **cannot** navigate another client's tab.
CDP targets are independent.

However, running multiple CDP clients (e.g., 4 Super-Browser MCP servers + the
bridge + the MCP server = 6+ clients) on one Chrome instance can still
**degrade reliability** through resource contention:

- **CPU/memory saturation** — each CDP client drives React hydration, DOM
polling, and JS evaluation. Under load, the Chrome renderer can slow
enough that ChatGPT's SPA fails to hydrate the composer within the
bridge's readiness window (see P2 staged readiness diagnostics).
- **CDP HTTP endpoint queuing** — the `/json/list` and `/json/version`
endpoints are served by Chrome's DevTools HTTP server, which is
single-threaded. Many clients polling concurrently can introduce latency.
- **Profile-level state** — all tabs share one Chrome profile (one account,
one cookie jar, one service worker). A service-worker update or auth
redirect on one tab can briefly affect the others.

**What this means in practice:** the bridge's write path (navigate + send)
is more sensitive to these effects than the read path (list/get
conversations via `/backend-api/*`), because the write path requires
DOM-level readiness (composer hydrated, send button enabled) while the read
path is a pure HTTP fetch. If you observe "navigation timeout" or "no send
button" errors that worsen as more clients connect, consider:

1. Running the bridge on a **dedicated Chrome instance** (separate
`--user-data-dir` and `--cdp-port`).
2. Reducing the number of concurrent CDP clients on the shared Chrome.
3. Enabling `parallel_tabs: true` so the bridge and MCP server each get
their own tab (they do by default with `tab_mode="owned"`, but
`parallel_tabs` adds per-target locking and fail-closed semantics).

The bridge's error messages now include staged diagnostics (P2) that name
the failing readiness stage, making this degradation diagnosable rather
than opaque.

---

Expand Down
60 changes: 60 additions & 0 deletions scripts/ensure_hook.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
#!/usr/bin/env python
"""ZCode SessionStart hook wrapper for `chatgpt-web2api ensure`.

Runs `chatgpt-web2api ensure` to reconcile the bridge (start REST + SSE if
missing, verify health). ALWAYS exits 0 so a bridge failure never blocks the
ZCode session — the bridge being down is an infrastructure problem, not a
reason to prevent the user from working. Failures are logged to stderr where
ZCode captures them in its hook execution log.

Usage (from ZCode hooks.json):
command: python.exe
args: [path/to/ensure_hook.py, --rest-port, 8080, --mcp-sse-port, 8090]

Exit codes:
0 — always (hook passes regardless of bridge state)
The bridge's actual state is logged to stderr for diagnosis.
"""
import subprocess
import sys


def main() -> None:
# Forward our args (minus the script name) to `chatgpt-web2api ensure`.
# The first arg is this script's path; the rest are ensure's flags.
ensure_args = sys.argv[1:]
cmd = [sys.executable, "-m", "chatgpt_web2api", "ensure"] + ensure_args

try:
result = subprocess.run(
cmd,
capture_output=True,
text=True,
timeout=90, # Chrome cold-start can take 10-15s; 90s is generous
)
# Log the output for ZCode's hook execution log.
if result.stdout:
print(result.stdout, file=sys.stderr)
if result.stderr:
print(result.stderr, file=sys.stderr)
if result.returncode != 0:
print(
f"ensure exited {result.returncode} — bridge may need manual "
f"attention (auth=2, failure=1). This does not block the session.",
file=sys.stderr,
)
except subprocess.TimeoutExpired:
print(
"ensure timed out after 90s — bridge may be slow to start. "
"This does not block the session.",
file=sys.stderr,
)
except Exception as e:
print(f"ensure hook error: {e}", file=sys.stderr)

# ALWAYS exit 0 — never block the ZCode session.
sys.exit(0)


if __name__ == "__main__":
main()
16 changes: 14 additions & 2 deletions src/chatgpt_web2api/api_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -530,10 +530,16 @@ async def _full_response(
transparently — the client only sees it (as a 429) if the limit
persists across all retries.
"""
# P1: resolve model-aware detector budgets from config.
from .completion_detector import DetectorBudgets

budgets = DetectorBudgets.from_config(self._config.chatgpt, model)

async def _send_and_collect() -> str:
collected = ""
async for chunk in self._driver.send_and_stream(text, timeout=timeout):
async for chunk in self._driver.send_and_stream(
text, timeout=timeout, budgets=budgets, model=model,
):
collected += chunk.delta
return collected

Expand Down Expand Up @@ -577,6 +583,10 @@ async def _stream_response(
cleared it), but if one occurs it falls back to the inline
``[Error: ...]`` SSE chunk — documented as a known limitation.
"""
# P1: resolve model-aware detector budgets from config.
from .completion_detector import DetectorBudgets

budgets = DetectorBudgets.from_config(self._config.chatgpt, model)

async def _preflight() -> None:
"""Raise RateLimitError if the pop-up is present right now."""
Expand Down Expand Up @@ -638,7 +648,9 @@ async def _preflight() -> None:
)

try:
async for chunk in self._driver.send_and_stream(text, timeout=timeout):
async for chunk in self._driver.send_and_stream(
text, timeout=timeout, budgets=budgets, model=model,
):
if chunk.delta:
await self._send_sse(
resp,
Expand Down
Loading
Loading