Repository navigation
[Bug][Windows]: cold Storage inspection blocks the proxy thread on large Codex SQLite logs #2605
Description
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
- Use a Codex
logs_2.sqlitewith hundreds of thousands of rows. - Start an unmodified OpenCodex build.
- Open the Storage page or request
GET /api/storageafter the inspection cache is cold or after the WAL changes. - In parallel, request
/healthzor send a routed model request. - 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 msGET /api/storage/codex-logs: 9 msGET /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
lidge-jun commentedon Aug 25, 2026
리뷰 · 우선순위 70 / 80
설명: 이 이슈는 Storage / Log Guard 첫 검사가 Codex
logs_2.sqlite에 대해 네 개의 한도 없는 SQLite 집계를 프록시 스레드에서 동기적으로 돌린다고 한다. 큰 로그면 Storage 를 열거나 새로고침하는 동안 라우팅과 헬스 응답이 멈춘다. 작성자는 stephen-drew 이다. 지금 열려 있다. 라벨은 bug, platform 이다. 지금 CURRENTdevHEAD 는 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 - 다른 열차다. 이 이슈로 닫지 말 것
메인테이너의 판단이 필요한 지점
너의 추천
버그로 받는다. 다만 지금 합칠 차량은 없다. 찬 검사와 WAL 무효화 뒤 검사를 프록시 스레드에서 빼거나, 큰 DB 에서는 행 집계를 건너뛰고 파일·스키마·능력만 돌리는 쪽이 맞다. 본문 64MiB 임계는 재현된 완화다. Worker 가 더 완전하다. 내가 구현하지 않고 닫지 않는다. 라벨은 그대로 둔다. 프리뷰 배포가 아니다.
이 댓글은 grok-bot이 작성했습니다
fix(storage): bound the cold Codex log inspection and surface when it…
lidge-jun commentedon Aug 25, 2026
Fixed on
devby #2627 (squash8091f2ea0b0a6b1d0515b1bc7e02b03a32b3b2d2).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: nullplusmetricsSkipped: { 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
metricsnull 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:One thing I did not do, stated plainly: a real-server test asserting
/healthzstays 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 enterreadMetricsat 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.
fix(storage): bound the cold Codex log inspection and surface when it…
fix(storage): bound the cold Codex log inspection and surface when it…