Skip to content

[Bug][Windows]: cold Storage inspection blocks the proxy thread on large Codex SQLite logs #2605

Closed

Description

@stephen-drew

Client or integration

Codex App

Area

Dashboard / Storage / Log Guard

Summary

The first Storage / Log Guard inspection runs four unbounded SQLite aggregates synchronously on the OpenCodex proxy thread.

On a large Codex logs_2.sqlite, opening or refreshing Storage can therefore block routing and health responses until the aggregates finish. The current memoization removes repeated scans only while the database and WAL identity stay unchanged. It does not bound the cold scan, and an active Codex process changes the WAL and invalidates the cache.

Expected: Storage inspection must not block the proxy thread. Expensive metrics should run off-thread, be explicitly bounded, or degrade to file/schema-only metrics above a safe threshold.

Actual: readMetrics performs count(*), two GROUP BY queries and sum(estimated_bytes) synchronously. Both /api/storage and /api/storage/codex-logs call the inspection inline.

Current dev documents the same remaining gap in src/codex/log-guard/inspect.ts: memoization "bounds the repeat cost, not the first one" and the cold scan still blocks.

Reproduction

  1. Use a Codex logs_2.sqlite with hundreds of thousands of rows.
  2. Start an unmodified OpenCodex build.
  3. Open the Storage page or request GET /api/storage after the inspection cache is cold or after the WAL changes.
  4. In parallel, request /healthz or send a routed model request.
  5. Observe that the synchronous SQLite aggregates occupy the proxy thread until inspection completes.

Historic live reproduction on OpenCodex 2.26.0:

  • database: approximately 1 GB
  • rows: approximately 495,000
  • GROUP BY level: approximately 17,289 ms
  • GUI resource deadline: 30 seconds
  • filesystem scan itself: under one second across approximately 11,943 files

A local mitigation skipped expensive aggregates when the database exceeded 64 MiB while retaining file size, schema and capability inspection. After that change:

  • GET /api/storage: 628 ms, then 466 ms
  • GET /api/storage/codex-logs: 9 ms
  • GET /healthz: 200

I also ran the unmodified official 2.32.0 inspector in a separate read-only process against the current database:

  • database: 1,468,923,904 bytes
  • rows: 681,808
  • first isolated inspection: 816 ms
  • same-process memoized repeat: 0.448 ms
  • schema: compatible

The current host file cache was already warm, so 816 ms is not presented as a worst case. The earlier 17.3-second live result and the 49-second large-database measurement documented in the current source show the cold/fragmented case.

Version

2.26.0 for the historic live reproduction. The synchronous cold-scan path remains in the official 2.32.0 package and current dev source (file SHA d2c17192365cad0dae2b678456588a03ae407a61 at the time of filing).

Operating system

Windows 11 Home 25H2, OS build 26200.9168, x64.

Provider and model

Not provider-specific. The defect is in local Storage / SQLite inspection.

Logs or error output

Historic unmodified path:
logs_2.sqlite: ~1 GB
rows: ~495,000
GROUP BY level: ~17,289 ms
/api/storage GUI budget: 30,000 ms

Local bounded mitigation:
large database threshold: 67,108,864 bytes
/api/storage: 628 ms, then 466 ms
/api/storage/codex-logs: 9 ms
/healthz: 200

Official 2.32.0 inspector, separate read-only process:
databaseBytes=1468923904
totalRows=681808
firstElapsedMs=816
memoizedRepeatMs=0.448
schema=compatible

Screenshots and supporting files

No screenshot is needed. The current source comment and query implementation are visible in inspect.ts.

Redacted configuration

{
  "storageInspection": "default",
  "database": "Codex logs_2.sqlite",
  "providers": "<not relevant>"
}

Checks

  • I searched existing issues and documentation.
  • I removed secrets, tokens, account details, request credentials, private paths and personal data.

Activity

  1. added
    bugSomething isn't working
    platformOS/service/tray/ACL (Windows-heavy, not Windows-only)
    on Aug 25, 2026
  2. lidge-jun commented on Aug 25, 2026

    @lidge-jun
    Owner

    리뷰 · 우선순위 70 / 80

    설명: 이 이슈는 Storage / Log Guard 첫 검사가 Codex logs_2.sqlite 에 대해 네 개의 한도 없는 SQLite 집계를 프록시 스레드에서 동기적으로 돌린다고 한다. 큰 로그면 Storage 를 열거나 새로고침하는 동안 라우팅과 헬스 응답이 멈춘다. 작성자는 stephen-drew 이다. 지금 열려 있다. 라벨은 bug, platform 이다. 지금 CURRENT dev HEAD 는 6b508d5 이다. 직전은 6e6d5cd 였고 이번 시간에 2603 부터 2609 와 2583 2595 2575 2427 2430 이 착지했다. origin/dev package.json 은 2.32.1-preview.20260825 다. 본문이 적은 산수는 이렇다. 예전 산 재현은 약 1GB / 49만 행에서 GROUP BY level 이 약 17초. 소스 주석의 큰 조각화 사례는 15.6GB / 30만 행에서 약 49초. 공식 2.32.0 을 읽기 전용으로 돌리면 1.47GB / 68만 행에서 첫 검사가 816ms 이고 메모 반복은 0.45ms. 로컬 완화는 64MiB 를 넘으면 비싼 집계를 건너뛰고 파일·스키마·능력만 남긴다. 차량 풀은 아직 없다. 내가 구현하지 않고 머지하지 않는다. 태그하지 말 것. 배포하지 말 것.

    HEAD 에서 구멍은 그대로다. src/codex/log-guard/inspect.ts 156줄부터 172줄 주석이 메모는 반복 비용만 막고 첫 검사는 막는다고 적는다. 319줄 readMetrics 는 count(*), level GROUP BY, target GROUP BY, estimated_bytes sum 을 bun:sqlite 동기 경로로 돌린다. 376줄 inspectCodexLogs 는 캐시 키가 같으면 바로 돌리고, 다르면 385줄 inspectCodexLogsUncached 를 탄다. 181줄 inspectionCacheKey 는 DB·WAL·SHM 의 dev/ino/size/mtimeNs/ctimeNs 로 키를 만든다. Codex 가 쓰는 동안 WAL 이 바뀌면 캐시는 매번 무효다. 그래서 메모는 반복 스톨만 줄이고, 찬 스캔과 살아 있는 WAL 무효화는 그대로다. src/server/management/storage-log-guard-routes.ts 112줄 /api/storage/codex-logs 와 166줄 /api/storage 가 getCodexLogGuardProtectionStatus 를 인라인으로 부른다. 프록시 스레드를 비우지 않는다.

    이번 시간 착지는 이 구멍을 막지 않는다. 2603 2604 2606 은 모델 프리셋 2465 열차다. model-presets.ts 가 생겼다. 2609 는 2464 차량이다. 2464 는 아직 열려 있다. 이 이슈로 닫지 말 것. 2607 2608 은 2568 OAuth 429 회전이다. 2568 은 아직 열려 있다. 닫지 말 것. 2595 는 콤보 미리보기 덩어리 한도다. 2583 은 Kiro 속성 이름이다. 2575 는 Daybreak 가격이다. 2427 은 시험 --parallel 이다. 2430 은 사이드바 발이다. 어느 것도 src/codex/log-guard/inspect.ts 의 찬 집계를 옮기지 않았다. 2499 는 카탈로그 상태 프로브 지연이다. 주제는 가깝지만 logs_2 집계가 아니다. 섞어 닫지 말 것. src/runtime 은 없다. default-aliases.ts 는 없다. 2463 을 이 이슈로 닫지 말 것. CursorCredentialRouter 는 여전히 src/providers/cursor-pool.ts 28줄 클래스뿐이다. 프리뷰 배포가 아니다.

    라인 156 - src/codex/log-guard/inspect.ts 주석이 네 집계가 프록시 스레드를 막는다고 적는다
    라인 170 - 같은 주석이 찬 검사는 여전히 막는다고 적는다. 메모는 반복만 줄인다
    라인 181 - inspectionCacheKey 는 DB/WAL/SHM 정체다. 살아 있는 Codex 쓰기는 캐시를 깨뜨린다
    라인 319 - readMetrics 가 count(*), 두 GROUP BY, sum(estimated_bytes) 를 동기로 돌린다
    라인 376 - inspectCodexLogs 는 캐시 히트만 바로 돌린다. 미스면 언캐시드 경로다
    라인 112 - src/server/management/storage-log-guard-routes.ts /api/storage/codex-logs 가 인라인 검사다
    라인 166 - 같은 파일 /api/storage 도 인라인 검사다. 오프스레드가 아니다
    2499 - 카탈로그 프로브 지연이다. 이 이슈로 닫지 말 것
    2463/2464/2568 - 다른 열차다. 이 이슈로 닫지 말 것

    메인테이너의 판단이 필요한 지점

    • 이 이슈를 버그로 받을지. HEAD 주석과 readMetrics 경로가 본문과 같다
    • 고침을 Worker 오프로드할지, 크기 임계에서 집계를 건너뛸지, 둘 다 할지. 본문 완화는 64MiB 임계다
    • 스키마·파일 크기·능력만 남기고 행 집계를 포기해도 Storage UX 가 충분한지
    • /api/storage 와 /api/storage/codex-logs 를 같은 한도로 묶을지. 둘 다 인라인이다
    • 2499 와 한 차량으로 묶을지. 묶지 말 것. 프로브 대상이 다르다
    • 지금 leftover-close 할지. 차량이 없다. 내가 닫지 않는다

    너의 추천
    버그로 받는다. 다만 지금 합칠 차량은 없다. 찬 검사와 WAL 무효화 뒤 검사를 프록시 스레드에서 빼거나, 큰 DB 에서는 행 집계를 건너뛰고 파일·스키마·능력만 돌리는 쪽이 맞다. 본문 64MiB 임계는 재현된 완화다. Worker 가 더 완전하다. 내가 구현하지 않고 닫지 않는다. 라벨은 그대로 둔다. 프리뷰 배포가 아니다.

    이 댓글은 grok-bot이 작성했습니다

  3. lidge-jun commented on Aug 25, 2026

    @lidge-jun
    Owner

    Fixed on dev by #2627 (squash 8091f2ea0b0a6b1d0515b1bc7e02b03a32b3b2d2).

    Your diagnosis was right on every point, including the one the source already admitted about itself — "This bounds the repeat cost, not the first one" — and the WAL-invalidation observation is what makes memoization insufficient rather than merely incomplete.

    Took your option: databases above 64 MiB skip the row aggregates and keep file sizes, schema validation and capabilities. A Worker was considered and rejected — it turns both endpoints async and adds Worker admission/teardown cost on Windows, which is where you hit this. Query limiting was rejected because it cannot preserve an exact count, grouping or sum, and a wrong number is worse than an absent one.

    metrics: null plus metricsSkipped: { reason, thresholdBytes } keeps "not scanned" distinguishable from "genuinely zero", and your measurements are in the threshold constant's comment so the next person can see where 64 MiB came from.

    One gap the server change alone left: with metrics null the GUI row block simply disappeared, so a 1 GB database rendered as one with no rows — the exact confusion the null-vs-zero distinction exists to prevent, and most misleading precisely where the database is largest. The row now states the reason and the threshold, in all nine locales, and I rendered it in a browser rather than trusting the assertion:

    Logs database  1.4 GiB
    DB rows        Row metrics skipped: the database is above 64 MiB, and scanning it would stall the proxy.
    

    One thing I did not do, stated plainly: a real-server test asserting /healthz stays responsive during a cold inspection. A sparse large fixture's aggregate path finishes too fast to reliably overlap the request, so that test would be timing-dependent and would pass whether or not the fix existed. The deterministic regression proves large databases never enter readMetrics at all, which removes the blocking operation rather than measuring around it.

    Thanks for the measurements — the 17.3s figure and the before/after from your local mitigation are what made the threshold choice defensible instead of a guess.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingplatformOS/service/tray/ACL (Windows-heavy, not Windows-only)

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions