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

Open 💬 4 comments Opened Aug 19, 2026 by nextmediabr-lgtm
💡 Likely answer: A maintainer (github-actions[bot], contributor) responded on this thread — see the highlighted reply below.

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.

View original on GitHub ↗

4 Comments

github-actions[bot] contributor · 8 days ago

Potential duplicates detected. Please review them and close your issue if it is a duplicate.

  • #38751

Powered by Codex Action

nextmediabr-lgtm · 8 days ago

@tibo-openai flagging this one directly, since the underlying behaviour is a design decision rather than a one-off crash.

The short version: every in-app browser tab is given its own isolated storage profile, so no tab can ever inherit a signed-in session. Any Google Docs/Sheets link therefore lands on accounts.google.com/signin instead of the document, and Google refuses to complete OAuth inside an embedded webview. That unresolvable sign-in URL is then persisted as the tab's restoreUrl, so the thread is permanently poisoned — every reopen re-mounts the doomed tab and pins the renderer's main thread at 100% CPU. It reproduced 4 times in a row here.

Two things that make it hard to hit from the outside:

  1. The "Open web URLs and links in → Default browser" setting does not prevent it. That setting covers newly clicked links, not tabs already persisted in app state.
  2. The renderer leaves no trace in the app-server logs, so nothing in the logs points at the browser. It only shows up under sample on the renderer PID.

Separately, and probably cheap to fix: codex_rollout::recorder logs 2277 failed to parse rollout line entries in a 10-day window, mostly unknown variant \thread_name_updated\``. It's a forward-compatibility gap in the rollout deserializer, logged at TRACE so it never surfaces.

Happy to run any diagnostic you want on the affected machine — the failing state is still reproducible from the backup of the app state.

nextmediabr-lgtm · 8 days ago

Correction and additional evidence (2026-08-19)

I re-audited the affected machine, the pre-fix state snapshots, the SQLite log database, and the packaged Desktop implementation. The core bug is still reproducible from the preserved state, but two causal claims and two counts in the opening report need correction.

1. browserStorageId is not a per-tab Chromium profile

In Desktop build 26.814.41407, ordinary in-app-browser webviews all use the same Electron session partition: persist:codex-browser-app. The packaged main process configures that one partition through session.fromPartition(...) and reuses it for browser-sidebar webviews.

browserStorageId is a durable page/persistence identity used to associate a conversation, browser tab, navigation snapshot, and serialized page state. It is not an Electron cookie jar.

So the relevant isolation boundary is:

  • Codex in-app browser profile vs. the user's external Chrome/Safari profile: isolated.
  • One Codex in-app browser tab vs. another Codex in-app browser tab: shared Electron session/cookies.

The pre-fix app-state snapshot contained 69 unique persisted page-storage identities, not 67 isolated Chromium profiles. Google sign-in still cannot be relied on inside this embedded browser; Google documents embedded user-agents as unsupported/disallowed for sign-in and OAuth flows: https://developers.google.com/identity/protocols/oauth2/policies

2. The original request-rate diagnosis was wrong

The early diagnosis counted log rows as requests. Re-querying the original minute directly in logs_2.sqlite gives:

| Local minute | thread/turns/list log rows | Unique requests |
| --- | ---: | ---: |
| 19:53 (normal comparison) | 19 | 8 total / 7 carrying RPC context |
| 20:07 (hang reproduction) | 804 | 12 |

The 804 rows during the hang break down as:

  • 792 codex_rollout::recorder rows
  • 768 per-line parse failures
  • 12 resume-start rows
  • 12 resume-complete rows
  • 12 remaining app-server request/response rows

Therefore this was not a 14-requests-per-second loop. The amplification was work and logging per reconstruction.

3. What the surgical A/B test does prove

Before the surgical recovery:

  • 69 unique persisted page-storage identities
  • exactly 2 restoreUrl entries pointing at accounts.google.com sign-in
  • the affected thread's right panel persisted as open
  • renderer at about 99% CPU and 1.2-1.5 GB physical footprint

The recovery removed only those 2 Google sign-in routes and closed that one persisted panel. It left 67 other persisted browser identities in place. After reopening the same thread, the renderer fell to about 0.8% CPU / 602 MB and the hang stopped reproducing.

That is strong A/B evidence that the auto-restored Google auth-wall state was the trigger. The saved macOS sample proves that one renderer spent all 3,024 samples on its V8/JIT main thread, but the frames are unsymbolized. It does not prove whether the inner loop is Google's page JavaScript or Codex's restore/reconciliation code. That exact inner loop remains unresolved.

4. Relationship to the suggested duplicate

#38751 is the same subsystem but a different measured failure mode:

  • #38751: Windows, eager hydration/retention of many inactive browser pages, process growth, and about 3.2 GB additional working set.
  • This issue: macOS, one persisted auth-wall tab plus an auto-open panel, deterministic single-core renderer spin, reproduced four times and removed by a two-route surgical state change.

They likely share restoration/lifecycle code, but the trigger and failure signature here are narrower than generic tab-retention memory pressure.

5. Adjacent defects already tracked elsewhere

The extra findings in the opening report should not be treated as new browser-root-cause claims:

  • logs_2.sqlite incremental auto-vacuum is already tracked by #35823; the macOS SQLite checkpoint/disk-write symptom is also covered by #32431.
  • invalid URI: relative URL without a base during legacy rollout replay is tracked by #36639.
  • unknown variant thread_name_updated is already represented by #21761 (and earlier #24423).

Privacy note

The preserved raw diagnostic log contains local paths, thread identifiers, URLs, and HTTP session-cookie headers. I will not upload it as-is. I can provide a minimized synthetic state fixture or sanitized aggregates if a maintainer asks for a specific diagnostic.

nextmediabr-lgtm · 8 days ago

Not a duplicate of #38751.

#38751 covers eager tab restoration adding ~3.2 GB RAM on Windows — a memory pressure issue. This report covers a distinct failure mode: a persisted restoreUrl pointing at a Google sign-in page causes the renderer's main thread to spin at 100% CPU indefinitely on macOS, making the thread permanently unresponsive. The reproduction trigger (Google OAuth redirect inside an embedded webview, written to app state as restoreUrl) and the symptom (renderer hang, not memory growth) are different.

We linked #38751 in the Additional Information section because both issues share the same root cause — uncapped persistent tab accumulation with no eviction — but they need separate fixes: one is a memory management issue, the other is a renderer liveness issue.