openai / openai/codex

[macOS] Restoring a persisted Google sign-in tab in the in-app browser pins the renderer at 100% CPU

Open
#39,552 4 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

app browser bug performance
Dominant language
Rust
Stars
125k
Forks
19.4k
PR merge metrics
PR metrics pending

Description

What version of the Codex App are you using?

26.814.41407 (build 6720)

What subscription do you have?

ChatGPT Pro

What platform is your computer?

Darwin 25.6.0 arm64 arm (macOS 26.6.2, build 25G83, Apple Silicon, 48 GB RAM)


What issue are you seeing?

TL;DR — The in-app browser shares one Chromium partition (persist:codex-browser-app), but each tab gets its own logical browserStorageId. Because the in-app browser and the user's external browser are fully isolated, Google sign-in from inside a tab cannot complete. This machine had accumulated 69 persisted page-storage identities across 67 browser tabs, none of them ever evicted. One of those tabs was parked on a Google sign-in page it could never complete — and restoring it pinned the renderer's main thread at 100% CPU indefinitely.

Opening one specific thread pegs the Codex (Renderer) process at 99% CPU on a single core with ~1.5 GB RSS, and the app becomes unresponsive. It reproduced 4 times over one hour, every time on the same thread. Killing the renderer PID recovers the app; reopening the thread hangs it again.

The root cause is not the thread's size. It is a persisted in-app browser tab whose restoreUrl points at a Google sign-in page.

When a Google Sheets link is opened in the in-app browser, the tab is persisted in ~/.codex/.codex-global-state.json under electron-persisted-atom-statethread-tab-routes-v1:<threadId> with a restoreUrl like:

https://accounts.google.com/v3/signin/identifier?continue=https://docs.google.com/spreadsheets/d/<REDACTED>/edit&followup=...&ltmpl=sheets&service=wise&flowName=GlifWebSignIn&flowEntry=ServiceLogin

Note it is the sign-in URL, not the document URL. This happens because every in-app browser tab gets its own logical browserStorageId within the shared persist:codex-browser-app partition. On this machine there were 67 persisted browser tabs across 19 threads, each with a distinct browserStorageId — and because the in-app browser's partition is isolated from the user's external browser, no tab carries Google session cookies, and every Google document link degrades into a sign-in redirect. Google blocks OAuth sign-in from embedded webviews, so the flow never completes.

On thread restore, these tabs are re-mounted automatically, the sign-in flow loops, and the renderer's main thread spins forever.

Measured evidence — the renderer. sample <renderer-pid> 4 shows 3024 of 3024 samples on com.apple.main-thread, entirely in JIT-compiled JavaScript frames, with no disk I/O and no SQLite frames in the stack:

Call graph:
    3024 Thread_81960   DispatchQueue_1: com.apple.main-thread  (serial)
    + 3024 start  (in dyld) + 6992
    +   3024 main  (in Codex (Renderer)) + 228
    +     3024 ChromeMain  (in Codex Framework) + 540
    +       3024 v8::CpuProfileNode::GetDeoptInfos() const  (in Codex Framework)
    +         ... (all remaining frames in JIT-compiled JS, <unknown binary>)

The renderer leaves no trace at all in the app-server logs — a hang in this component is currently undiagnosable from logs alone.

Measured evidence — the backend. During the hang, thread/turns/list handling produces a large volume of log output while serving only a handful of requests:

minute log lines unique rpc.request_id lines per request
normal (19:53) 19 8 2.4
during hang (20:07) 804 12 67

To be precise about what this does and does not show: the request count is not runaway (12 vs 8). What explodes is the work per request — a 28× increase in log output per request. 780 of those 804 lines come from a single target, codex_rollout::recorder, and they are parse failures (see below).

The topology.right.open = true flag for that thread is what makes it reproducible: the side panel is restored before any user interaction, so the "open links in default browser" setting does not prevent it. That setting governs newly clicked links; it does not apply to tabs already persisted in app state.

How the bad state gets created in the first place

The hang on thread restore is the second half of the story. The first half is what happens when a Google link is opened at all.

Opening a Google Sheets link from a chat message mounts it in the side panel's in-app browser. Because that tab's browserStorageId has no prior Google sign-in session (the partition is isolated from the external browser), it carries no Google cookies, so Google immediately redirects it to accounts.google.com/v3/signin/.... Google refuses to complete OAuth inside an embedded webview, so the tab can never resolve — it just sits there.

From the user's side, the visible symptom is a side panel stuck on "Preparing preview…" that never finishes, while the app becomes progressively unresponsive. There is no error, no timeout, and no way to tell from the UI that the panel is parked on a sign-in wall rather than loading the document.

That unresolved sign-in URL is then written into app state as the tab's restoreUrl. From that point on the thread is poisoned: every subsequent open re-mounts the same doomed tab and re-enters the loop. This is why the hang reproduced 4 times in a row on the same thread — the failing state is persistent, not transient.

So the full cycle is:

  1. Open a Google Docs/Sheets link in the side panel → new isolated profile → no cookies → redirected to sign-in.
  2. Sign-in cannot complete (embedded webview) → panel stuck on "Preparing preview…".
  3. The sign-in URL is persisted as restoreUrl, and topology.right.open stays true.
  4. Every later open of that thread restores the tab automatically → renderer main thread spins at 100% CPU → app unresponsive.
  5. Killing the renderer process recovers the app, but step 4 repeats on the next open.

Nothing in this chain requires the thread to be large, and nothing in it can be escaped from the UI.

What steps can reproduce the bug?
  1. In a thread, open a Google Sheets (or Docs) link in the in-app browser side panel, while not signed into Google in the in-app browser (the in-app browser partition is isolated from your external browser session).
  2. The tab lands on accounts.google.com/v3/signin/... instead of the document.
  3. Leave the side panel open and close the app.
  4. Reopen the app and open that thread.
  5. The renderer pegs one core at ~99% and the app stops responding. The side panel shows a preview that never finishes loading.

Affected thread in this report: 019ed3a4-… (6352 rollout items). Also reproduced against a second thread, 019ff7d2-… (35759 items), so item count is not the trigger — the persisted sign-in tab is.

What is the expected behavior?
  • In-app browser tabs should be able to complete Google sign-in (or inherit the app's authenticated session), so a Google document link opens the document rather than a sign-in wall. Currently Google blocks OAuth inside embedded webviews and the shared persist:codex-browser-app partition carries no external browser cookies.
  • A restoreUrl pointing at a known-blocked embedded sign-in flow should not be restored automatically on thread open.
  • Restoring a side-panel tab should be cancellable and time-boxed: a failing restore must not spin the renderer's main thread indefinitely.
  • Rollout parse failures should not cost 67 log lines per request, and unknown event variants should be skipped once, not re-reported per line.
  • The "Open web URLs and links in → Default browser" setting should also apply to restored tabs, not only newly clicked links.
Rollout parse failures flooding the logs

Independently of the hang, codex_rollout::recorder is failing to parse rollout lines on essentially every resume. 2277 such failures are present in a 10-day log window, in two flavours:

failed to parse rollout line: unknown variant `thread_name_updated`,
    expected one of `error`, `warning`, `guardian_warning`, ...

failed to parse rollout line: invalid URI: relative URL without a base

The first one looks like a forward-compatibility gap: thread_name_updated events are being written into rollouts, but the deserializer's enum does not know that variant, so every occurrence fails to parse. Older rollouts are therefore partially unreadable by the current build, and the failure is silent — these are logged at TRACE, so nothing surfaces in normal operation.

This is what produces the 67-lines-per-request figure above: resuming a thread walks the rollout and logs a failure per unparseable line. On a large thread that is hundreds of log writes for a single UI request.

Related: a WARN also fires on resume —

apply_rollout_reconstruction{thread_id=<redacted> rollout_item_count=6352}:
    ignored world-state patch without a full snapshot
Additional information

Workaround (with the app fully closed), editing ~/.codex/.codex-global-state.json:

  1. Under electron-persisted-atom-statethread-tab-routes-v1:<threadId>, delete any entry in routes whose params.restoreUrl contains accounts.google.com.
  2. Remove those tabIds from topology.right.tabIds and reassign activeTabId if it pointed at one of them.
  3. Set topology.right.open = false.

A caveat for anyone scripting this: tabs live in two sibling keysthread-tab-routes-v1:<id> and thread-browser-tabs-v1:<id>. A generic "orphaned tabIds" sweep that inspects only one key will wrongly delete valid ids belonging to the other and break unrelated threads. Collect live ids from both keys before pruning.

After removing all 67 persisted browser tabs, the hang no longer reproduces.

Storage growth. The 67 browser tabs carried 69 unique persisted page-storage identities (browserStorageId), all retained indefinitely in app state, with up to 11 tabs persisted on a single thread. These accumulate silently — there appears to be no eviction. All tabs share the single persist:codex-browser-app partition; browserStorageId is a logical page identity, not a separate Chromium cookie jar. Related: #38751 reports the same eager-restore behavior on Windows adding ~3.2 GB RAM on chat open.

Possibly related, but distinct: #28502 (renderer at 100% CPU on app launch via auto-started browser/node_repl runtime) and #30178 (in-app browser crashing the main app during webview navigation). This report differs in that the hang is triggered by restoring a persisted sign-in tab on thread open, with the isolated-storage-per-tab behavior as the underlying cause.

Separate issue, same machine, same day: ~/.codex/logs_2.sqlite had grown to 462 MB while holding only 53 MB of live data — 88.5% of pages were on the freelist. The database has auto_vacuum = 2 (incremental), but the pragma is never invoked, so the 10-day log rotation frees pages that are never reclaimed. WAL checkpoints then rewrite an almost-empty 462 MB file. macOS raised a resource exception against the codex backend process for this (/Library/Logs/DiagnosticReports/codex_*.diag): 2147.49 MB written in 3923 s — 547 KB/s sustained against a 24.86 KB/s limit, with the heaviest stack in sqlite3_exec → sqlite3_step → pwrite. A manual VACUUM brought it to 45 MB with all 38035 rows intact. Happy to file that separately if useful.

cc @tibo-openai


Daniel Bernardes (@nextmediabr-lgtm) — hit the bug, reproduced it, and called the shape of it before the evidence did: "the in-app browser is a multiple-personality Chromium."

Claude Opus 5, running in Claude Code — process sampling, log forensics, app-state inspection, and this write-up.

Every figure above was measured on the affected machine. Where a first pass got a number wrong — an early draft read log lines as request counts — it was re-measured and corrected rather than left standing. If anything here does not reproduce on your side, say so and we will go back to the machine and check.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start by tracing persisted side-panel tab restoration through the electron-persisted-atom-state keys thread-tab-routes-v1 and thread-browser-tabs-v1, using the reported accounts.google.com restoreUrl case. Then inspect codex_rollout::recorder handling of thread_name_updated and relative URLs. Done means blocked sign-in tabs cannot hang the renderer during restore, restoration can stop safely, and repeated rollout parse failures no longer flood resume handling.

Written by the indexing model from the issue text.

Assessment

Tech stack
electron, macos, rust
Domain
backend, desktop, observability
Issue type
Bug
Difficulty
5/5
Estimated time
Over a week
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
38/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.