Codex app repeatedly writes large SQLite etilqs temp files from internal Guardian thread metadata
What version of the Codex App are you using (From “About Codex” dialog)?
Codex App 26.707.51957 (build 5175); bundled codex-cli 0.144.0-alpha.4
What subscription do you have?
ChatGPT Pro
What platform is your computer?
Darwin 25.5.0 arm64 arm
What issue are you seeing?
The native Codex process repeatedly creates and writes large SQLite temporary files named etilqs_* under the macOS temporary directory, even while the app is otherwise idle.
In a 314.6-second fs_usage trace:
- Total
WrData: 510.6 MB (about 140.3 GB/day if sustained) - Direct writes by
codex: 304.6 MB (59.6% of all writes) - Writes to
etilqs_*: 285.0 MB (55.8% of all writes and 93.6% of direct Codex writes) - 13 separate SQLite temp files were created
- The first was about 44.2 MB; subsequent files were usually about 21-22 MB
- The pattern repeated every 18-42 seconds
This is not the previously reported TRACE-log WAL insertion issue. During the same trace, writes to logs_2.sqlite-wal were only 20 KB, and the logs table was empty because inserts had already been disabled.
The behavior appears tied to oversized metadata for internal Guardian threads in state_5.sqlite. The database contained 1,338 thread rows. Across all rows, each of title, preview, and first_user_message totaled about 16.1 MB. In 1,193 rows, all three fields were exactly identical. There were 784 titles longer than 1,000 characters, with a maximum title length of 81,403 characters. Most of this data came from internal rows whose source was {"subagent":{"other":"guardian"}}.
These internal Guardian threads remained unarchived, were apparently included in thread-list/search work, and could not be archived through the supported thread API, which returned No Codex thread found for an internal Guardian thread ID.
What steps can reproduce the bug?
- Use the Codex desktop app for many tasks that create internal auto-review/Guardian subagents.
- Allow completed internal Guardian thread rows to accumulate in
~/.codex/state_5.sqlite. - Quit unrelated high-I/O applications where practical, then run:
sudo fs_usage -w -f diskio -t 300 Codex codex
- Observe repeated writes by the native
codexprocess to files namedetilqs_*in the macOS temporary directory, typically every 18-42 seconds. - Inspect
state_5.sqliteread-only and group active threads bysource. Check the lengths and equality oftitle,preview, andfirst_user_message. - On an affected database, internal Guardian rows contain very long conversation-like text duplicated into all three fields and remain unarchived after the internal task has completed.
No single user session ID appears to be responsible; the issue accumulates across many internal Guardian threads.
What is the expected behavior?
Completed internal Guardian threads should not remain in the active user-visible thread index with full conversation text duplicated into title, preview, and first_user_message.
Thread titles and previews should have reasonable length limits. Internal threads should be automatically archived or excluded from normal thread-list/search queries. An idle Codex app should not repeatedly spill tens of megabytes into SQLite temporary files every 18-42 seconds.
Additional information
I backed up the databases before testing a mitigation. Archiving 512 completed Guardian rows older than one hour directly in state_5.sqlite reduced the active thread-list metadata payload from about 48.5 MB to 2.54 MB. No rollout JSONL files or conversation content were deleted.
A 45-second SMART sample after that change measured 23.55 MB written, or 0.523 MB/s. A prior 25-second sample during the affected behavior measured 21.48 MB/s. That is an approximately 97.6% reduction in the short-interval device write rate, strongly linking the excessive writes to the active Guardian-thread metadata/thread-list path.
The SSD itself reports healthy status, 2% percentage used, 100% available spare, and zero media/data-integrity errors. The concern is unnecessary write amplification and long-term SSD wear caused by repeated SQLite temp-file generation.
I have intentionally not attached the raw database or trace because they may contain private conversation data, but I can provide a redacted trace if needed.
3 Comments
Potential duplicates detected. Please review them and close your issue if it is a duplicate.
Powered by Codex Action
Analysis (community)
Residual analysis for #32588 (Guardian thread metadata in
state_5.sqlitespilling intoetilqs_*temp files) under docs/contributing.md — invitation-only. No unsolicited PR.Root-cause hypothesis
The thread-metadata extraction path stores
title/preview/first_user_messagewith no length bound at any layer, and every Guardian review spawns a subagent thread whose first "user message" is the full Guardian prompt (the parent transcript) — so each review persists a multi-KB-to-81KB metadata row, and the thread-list query that re-reads all 1,338 rows × ~16 MB of duplicated text is what spills into theetilqs_*SQLite temp files you measured. Verified at HEAD4c43465133:1. No truncation anywhere in the chain.
user_message_preview(codex-rs/protocol/src/protocol.rs:2357-2375) returnsSome(message.to_string())— the entire stripped user message, no cap.apply_user_message(codex-rs/state/src/extract.rs:136-146) then setsfirst_user_message = preview.clone(),preview = preview, andtitle = strip_user_message_prefix(...).to_string(), again with no length check. TheThreadMetadataschema fields (state/src/model/thread_metadata.rs:115-124) are unboundedString/Option<String>. Your 784 titles >1,000 chars (max 81,403) is exactly this path.2. Guardian reviews are the producer. Each Guardian review spawns a subagent session with
SubAgentSource::Other(GUARDIAN_REVIEWER_NAME)(core/src/guardian/review_session.rs:680), and the review's initial user message is the assembled Guardian transcript prompt — so the three metadata fields capture the prompt text wholesale. That matches yoursource={"subagent":{"other":"guardian"}}rows and the 1,193 rows with all three fields byte-identical (same parent transcript at similar points). It also explains why archiving 512 guardian rows cut the write rate 97.6% (21.48 → 0.523 MB/s): the query stopped materializing those rows.3. Why it thrashes while idle. The thread-list/discovery surface re-reads these rows on a cadence (sidebar refresh, search), and SQLite spills large result sets into
etilqs_*temp files under$TMPDIR— your 13 temp files / 285 MB over 314.6 s, repeating every 18–42 s, is the list query re-materializing ~16 MB of duplicated metadata per pass. This is independent of thelogs_2.sqliteTRACE path (which you correctly ruled out — logs table empty, WAL 20 KB).Ranked fix outline (all OSS, codex-rs):
title/preview/first_user_messageto a display-grade length (e.g. 256 chars) inapply_user_message(or insideuser_message_previewitself, which fixes every consumer at once). Display-only change; no behavioral risk. A schema migration could shrink existing bloated rows on open.sourceis the Guardian subagent marker, either auto-archive the thread row on review completion or exclude it from the default thread-list/search queries. This matches your measured workaround (manual archiving) and removes the write amplification rather than just shrinking it."Guardian review of <parent title>") instead of the raw prompt — the prompt is already persisted in the thread's own rollout, so the metadata copy is pure duplication.Fail-first tests I'd want before any merge: (1) an 81k-char Guardian prompt produces metadata fields ≤ the cap (today: stored verbatim); (2) 1,338 Guardian-sourced rows do not appear in the default thread-list query (today: they do); (3) the list query over a bloated DB stays under a temp-file spill threshold (today: ~16 MB re-read per pass). Happy to draft the test patch if a maintainer wants it.
One adjacent OSS note (not part of this issue's fix): the same Guardian lifecycle does a full O(N) collection of the parent transcript on every review (
core/src/guardian/prompt.rs:116→:419-521; theGuardianTranscriptCursorat:73-76is consulted only after collection for Delta rendering), so K reviews cost O(N·K) CPU/RAM — an incremental cursor overhistory.raw_items()[already_collected..]would collapse that. Mentioning here only because it shares the Guardian anchor; theetilqs_*mechanism above stands alone. No unsolicited PR from me.Windows corroboration: 8,551 active Guardian histories and severe Desktop degradation
I observed the same failure class on Windows at a substantially larger scale, with a strong mitigation/recovery A/B.
Environment
26.820.7780.00.149.110.0.26200on-requestapprovals_reviewer = "guardian_subagent"Observed state
Read-only inspection before maintenance found:
{"subagent":{"other":"guardian"}}thread_source = subagentthread_source = guardian_reviewlogs_2.sqlitehad grown to approximately 7.332 GBokUser-visible symptoms included:
Loading skill...thread ... already has an active writer (code -32600)failed to load a bounded thread item pageMitigation and outcome
ok, andcodex doctorcompleted successfully.Afterward, a previously unusable existing thread opened quickly, loaded skills, accepted a message, and returned a response normally. New threads also remained responsive.
The immediate improvement from disabling the reviewer suggests active review work/lifecycle contributed to the live CPU/UI problem. The larger improvement after archiving thousands of internal histories strongly corroborates this issue's finding that completed Guardian histories remaining active materially degrade the thread-list/history path. The log DB compaction was performed in the same maintenance window, so I cannot attribute every part of the final improvement to one database alone.
Requested product behavior
guardian_subagentstate should be migrated safely to the current canonical reviewer behavior without retaining old internal histories as active user history.codex doctorshould warn when completed internal approval histories accumulate abnormally.This also appears related to #20864, #21211, #32035, #38144, and #38653.
I am not attaching raw rollout files, SQLite databases, thread IDs, prompts, or local paths because they contain private conversation and repository data. I can provide additional sanitized counts or bounded diagnostics if maintainers request them.