Skip to content

fix(rate-limits,codex): read the OpenCode Go key and Codex's index off the main thread - #24649

Closed
Jinwoo-H wants to merge 1 commit into
sta-9122-binder-off-mainfrom
sta-9122-gokey-codex-off-main
Closed

Jinwoo-H wants to merge 1 commit into
sta-9122-binder-off-mainfrom
sta-9122-gokey-codex-off-main

Conversation

@Jinwoo-H

@Jinwoo-H Jinwoo-H commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor
Files Added Deleted Net
Test 19 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​893 $\color{#cf222e}{\Huge{\mathbf{−}}}$​86 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​807
Prod 20 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​741 $\color{#cf222e}{\Huge{\mathbf{−}}}$​302 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​439

ELI5

On every usage refresh, Orca reads OpenCode's database to find your Go key. It also reads Codex's index on every Codex usage refresh and every Codex terminal launch, to check whether Codex is rebuilding it. Both reads ran on the thread that draws Orca's window, so a large database could freeze the app. This PR moves both reads to background threads.

What Changed

  • OpenCode Go key:
    • The per-database SQLite read is now an openCodeGoKey reader that answers found, missing or unreadable.
    • Discovery stays outside the reader because it's already async. The WSL \\wsl.localhost skip happens before anything is sent to the worker.
    • The public behaviour is unchanged: anything other than "found" still returns null.
  • Codex:
    • One codexIndexStatus reader thread now does all of codex-state-db.ts's disk work: backfill status with a bounded session-file count, indexed thread ids, session-file count, the newest-database lookup, and the sessions directory walk.
    • The public functions are now async, and every caller is updated: fetcher, trust grant, account bridge, index heal, backfill recovery, spawn preflight, and spawn env.
  • Recovery correctness:
    • startCodexStateDbBackfillRecoveryInBackground now reserves its activeRecoveries slot before awaiting, so two concurrent starts share one recovery and ensure…Started waits for the lock to be arbitrated.
    • "Worker didn't answer" is kept separate from "database unreadable". Recovery polling keeps polling on a timeout and only stops on a real unreadable.
  • WSL: Codex homes on \\wsl.localhost are read inside the reader thread, so a hung share stalls only that thread, up to its 60 s timeout. Recovery supports WSL homes on purpose, so failing early would have silently switched it off for them.
  • CLI: the orca CLI also reaches the trust grant. Reader threads now keep the process alive while a read is in flight (the CLI used to exit mid-read), and the new files are added to config/tsconfig.cli.json.
  • Supporting changes:
    • The supervisor lock moved to codex-state-db-backfill-supervisor-lock.ts, to stay under the 300-line limit.
    • Tests that read real database files use a new in-process test worker.

Why

Same approach as #24603 and #24638: one thread per reader on the shared worker, and never a main-thread fallback. Measured terminal-launch cost is about 26 ms when the Codex reader thread is cold (median of 15 runs, max 30 ms) and about 0.4 ms when it's warm, compared with about 0.1 ms inline. Launches only pay the cold cost when no Codex index read happened in the last 30 s.

Linked Issue

Related to #24360. Stacked on #24638, which must land first: orcad runs this Codex code and only gets the worker in #24638.

Visual Proof

N/A. No UI change.

Testing

  • Automated tests added/updated:
    • the readers;
    • each caller's async behaviour;
    • two concurrent recovery starts produce one recovery;
    • polling continues on a worker timeout;
    • WSL homes, with process.platform pinned;
    • the CLI process is kept alive during a read (this test fails without the fix).
  • Gates:
    • pnpm tc passes.
    • 616 test files / 6,187 tests pass across foreign-sqlite-readers, codex, rate-limits, opencode, agent-hooks, automations, ipc/pty and orcad.
    • The config script tests pass.
    • oxlint and check:code-quality:changed pass.
    • The electron-vite build passes.
    • pnpm build:orcad passes, smoke check included.

Review

Agent skill upstream boundary

  • Not applicable

Notes

Checklist

  • I explained what changed and why
  • N/A with reason for visual proof
  • Self-reviewed for correctness, security, and performance
  • Cross-platform (WSL), SSH/remote, and CLI impact considered
  • typecheck, tests, lint, and build pass locally

…f the main thread (STA-9122)

Before: the OpenCode Go key lookup opened opencode.db, and every Codex usage
refresh, hook trust grant, account bridge, and Codex terminal launch opened
Codex's state_<n>.sqlite and walked its sessions tree, all with synchronous
calls on the Electron main thread. A large -wal or a stalled \\wsl.localhost
share froze the window for as long as the open took.

After: both run on the foreign SQLite reader worker, each on its own lane.

- openCodeGoKey reader: takes the claim-ordered database list (discovery stays
  async and outside; WSL paths are still skipped before dispatch) and returns
  found / missing / unreadable. An unanswered worker is `unreadable`; the
  public helper still maps everything but `found` to null, so behaviour is
  unchanged and the separate "stop before OPENCODE_API_KEY" fix only has to
  return the tri-state.
- codexIndexStatus reader: one lane answers three queries (backfill status
  plus a bounded rollout count, indexed thread ids, session-file count). The
  newest-DB lookup and the sessions walk moved into it too, so a WSL Codex
  home is never opened or listed from the main thread. codex-state-db's
  public functions are now async; an unanswered worker returns each one's
  existing failure value (unreadable / not pending / null / 0).
- The lane now returns null for "worker did not answer" and each client
  method maps it to its failure value, so callers can tell it apart from an
  unreadable database. Backfill recovery polling keeps polling on null and
  still stops on a real `unreadable`; its first read spawns nothing on null.
- Recovery dedupe: startCodexStateDbBackfillRecoveryInBackground reserves its
  activeRecoveries slot before awaiting the pending read, so concurrent starts
  share one task and ensureCodexStateDbBackfillRecoveryStarted waits on that
  slot's lock arbitration. A not-pending answer releases the slot.
- A lane holds a ref'd handle while a read is in flight: the worker is
  unref'd, and the orca CLI (which reaches the trust grant) otherwise exited
  mid-read with nothing else alive.
- The supervisor lock moved to its own module to keep recovery under
  max-lines; tsconfig.cli.json lists the new CLI closure.

Tests: both readers on real fixtures; dispatch routing; client lanes
(answer, unanswered, mismatched answer); async callers via an in-process
test worker that runs the real dispatch; two concurrent recovery starts
produce one recovery and ensure waits for the pending read; poll-on-unanswered;
WSL home on pinned win32 posts to the worker with no main-thread fs or SQLite
touch; the bundled worker answers both readers; and a plain-Node child process
stays alive until its read answers (fails without the keep-alive).

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ No new issues found.

Reviewed changes

  • OpenCode Go key off the main thread — new openCodeGoKey reader; readOpenCodeCredentialDatabaseGoKey discovers paths, filters WSL UNC paths before dispatch, then reads on the worker and maps anything other than found to null, so the public result is unchanged.
  • Codex index off the main thread — one codexIndexStatus reader answers backfill status plus a bounded rollout count, indexed thread ids, session-file count, and newest-DB discovery. The codex-state-db public functions are now async, and every caller (codex-fetcher, codex-hook-trust-grant, account bridge, index heal, backfill recovery, spawn preflight/env) awaits them.
  • Recovery coordinator — reserves the activeRecoveries slot synchronously before awaiting the async isPending, uses a 'not-pending' sentinel for "no recovery needed", and keeps "worker did not answer" (keep polling / retry) distinct from a genuinely unreadable index.
  • Worker client — ForeignSqliteReaderLane.read returns T | null and callers map the failure themselves; a ref'd keep-alive handle holds a short-lived CLI process open until a read settles.
  • Supporting — supervisor lock moved to codex-state-db-backfill-supervisor-lock.ts to stay under the line cap; in-process test worker added; the new import graph added to config/tsconfig.cli.json.

I traced the async rewrites against their pre-PR synchronous behaviour across the failure cases (no state DB, corrupt DB, missing backfill_state/threads/credential tables, empty key, WSL paths, and worker no-answer) and verified the coordinator's slot lifecycle under concurrent starts, ensure, gave-up/failure-cap settlement, and stop. The only newly reachable states are the intended "reader did not answer" ones, bounded by the reader's 60 s timeout, and the tests for them assert exact values (the CLI keep-alive test in particular fails without the fix).

ℹ️ Author-flagged packaged-CLI check

The PR notes say the built out/main/foreign-sqlite-readers/ entry "isn't in asarUnpack". In the current tree it is — config/electron-builder.config.cjs:313 unpacks out/main/foreign-sqlite-reader-entry.js, alongside out/main/chunks/**, out/shared/**, out/cli/**, and node_modules/zod/**, and foreign-sqlite-reader-spawn.ts resolves it through findForeignSqliteReaderEntry's parent lookup. Worth re-confirming the note is stale before merge; no code change needed here.

Pullfrog  | View workflow run | Using deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏

@Jinwoo-H

Jinwoo-H commented Oct 2, 2026

Copy link
Copy Markdown
Contributor Author

Closing: after #24572 fixed the reported Cursor freeze, we're keeping this effort to the OpenCode binder. Moving the Codex and Go key reads off the main thread is about 1,600 lines and makes Codex's checks async across terminal launch and recovery, with no reported freeze to justify the risk. The separate correctness fixes stay in #24604 and #24605.

@Jinwoo-H Jinwoo-H closed this Oct 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant