diff --git a/gui/pr-assets/storage-log-guard-ux.png b/gui/pr-assets/storage-log-guard-ux.png new file mode 100644 index 0000000000..c95154ed45 Binary files /dev/null and b/gui/pr-assets/storage-log-guard-ux.png differ diff --git a/gui/src/components/storage-workspace/StorageWorkspace.tsx b/gui/src/components/storage-workspace/StorageWorkspace.tsx index fb5ed7808d..bfeb98ea77 100644 --- a/gui/src/components/storage-workspace/StorageWorkspace.tsx +++ b/gui/src/components/storage-workspace/StorageWorkspace.tsx @@ -72,6 +72,11 @@ export interface CodexLogGuardReport { freelistPages: number; reclaimableBytes: number; estimatedLogBytes: number | null; + writePoster?: { + off: { keepRowsShare: number; keepBytesShare: number | null }; + compat: { keepRowsShare: number; keepBytesShare: number | null }; + quiet: { keepRowsShare: number; keepBytesShare: number | null }; + }; }; } @@ -138,6 +143,62 @@ function mutationErrorLabel(locale: Locale, code: unknown): string { } } +function writePosterShare( + mode: { keepRowsShare: number; keepBytesShare: number | null }, +): { share: number; unit: "bytes" | "rows" } { + if (mode.keepBytesShare !== null) { + return { share: mode.keepBytesShare, unit: "bytes" }; + } + return { share: mode.keepRowsShare, unit: "rows" }; +} + +function formatWritePosterPercent(share: number, locale: Locale): string { + return (share * 100).toLocaleString(locale, { + maximumFractionDigits: 0, + minimumFractionDigits: 0, + }) + "%"; +} + +function formatRankedSourceLabel(locale: Locale, target: string): string { + const match = /^TARGET_(\d+)$/.exec(target); + if (!match) return target; + const rank = Number(match[1]); + if (!Number.isFinite(rank) || rank <= 0) return target; + return logGuardLabel(locale, "rankedSourceRank").replace("{rank}", String(rank)); +} + +function WritePosterMeter({ + locale, + modeKey, + modeLabel, + share, + unit, +}: { + locale: Locale; + modeKey: "off" | "compat" | "quiet"; + modeLabel: string; + share: number; + unit: "bytes" | "rows"; +}) { + const clamped = Math.max(0, Math.min(1, share)); + const percent = formatWritePosterPercent(clamped, locale); + return ( +
+
+ {modeLabel} + {percent} +
+ +
+ ); +} + function CodexLogGuardPanel({ report, locale, @@ -213,23 +274,67 @@ function CodexLogGuardPanel({ + {metrics?.writePoster && ( +
+

+ {logGuardLabel(locale, "writePoster")} +

+

{logGuardLabel(locale, "writePosterHelp")}

+ {([ + ["off", logGuardLabel(locale, "writePosterOff")] as const, + ["compat", logGuardLabel(locale, "compat")] as const, + ["quiet", logGuardLabel(locale, "quiet")] as const, + ]).map(([modeKey, modeLabel]) => { + const poster = writePosterShare(metrics.writePoster![modeKey]); + return ( + + ); + })} +

+ {metrics.writePoster.off.keepBytesShare !== null + ? logGuardLabel(locale, "writePosterUnitBytes") + : logGuardLabel(locale, "writePosterUnitRows")} +

+
+ )} + {protection && (
-

{logGuardLabel(locale, "protection")}

-
- {logGuardProtectionStateLabel(locale, protection.state)} - - {logGuardProtectionModeLabel(locale, protection.desiredMode)} - {protection.observedMode !== protection.desiredMode ? <>{logGuardProtectionModeLabel(locale, protection.observedMode)} : null} - -
-
+

+ {logGuardLabel(locale, "protection")} +

+

{logGuardLabel(locale, "protectionHelp")}

+
+
+
{logGuardLabel(locale, "desiredMode")}
+
{logGuardProtectionModeLabel(locale, protection.desiredMode)}
+
+
+
{t("dash.status")}
+
{logGuardProtectionStateLabel(locale, protection.state)}
+
+ {protection.observedMode !== protection.desiredMode ? ( +
+
{logGuardLabel(locale, "observedMode")}
+
{logGuardProtectionModeLabel(locale, protection.observedMode)}
+
+ ) : null} +
+
- {displayedLogGuard ? ( - - ) : report.codexLogsError === "inspect_failed" ? ( - - ) : null} +
+ + +
- {largestAcross.length > 0 ? ( -
-

{t("storage.section.largest")}

- {largestAcross.map(entry => { - const owner = bucketByKey.get(entry.bucketKey); - return ( -
- {entry.path} - {owner && {bucketLabel(owner, t)}} - {formatBytes(entry.bytes, locale)} -
- ); - })} + {overviewPane === "logGuard" ? ( +
+ {displayedLogGuard ? ( + + ) : report.codexLogsError === "inspect_failed" ? ( + + ) : ( +

{logGuardLabel(locale, "inspectionUnavailable")}

+ )}
) : ( -

-

+
+ {largestAcross.length > 0 ? ( +
+

{t("storage.section.largest")}

+ {largestAcross.map(entry => { + const owner = bucketByKey.get(entry.bucketKey); + return ( +
+ {entry.path} + {owner && {bucketLabel(owner, t)}} + {formatBytes(entry.bytes, locale)} +
+ ); + })} +
+ ) : ( +

+

+ )} +
)}
)} diff --git a/gui/src/i18n/log-guard-labels.ts b/gui/src/i18n/log-guard-labels.ts index de7ed41af5..33ab36ffd8 100644 --- a/gui/src/i18n/log-guard-labels.ts +++ b/gui/src/i18n/log-guard-labels.ts @@ -5,11 +5,27 @@ export type LogGuardLabelKey = | "externalSqliteHome" | "inspectionUnavailable" | "protection" + | "protectionHelp" + | "desiredMode" + | "observedMode" | "compat" + | "compatHelp" | "quiet" + | "quietHelp" | "disable" + | "disableHelp" | "repair" + | "repairHelp" | "compact" + | "compactHelp" + | "rankedSources" + | "rankedSourcesHelp" + | "rankedSourceRank" + | "writePoster" + | "writePosterHelp" + | "writePosterOff" + | "writePosterUnitBytes" + | "writePosterUnitRows" | "pagesUnit" | "compactComplete" | "compactPartial" @@ -38,10 +54,26 @@ const LABELS: Record> = { externalSqliteHome: 'External SQLite storage', inspectionUnavailable: "Diagnostic log inspection is unavailable.", protection: "Protection", + protectionHelp: "Reduces future Codex diagnostic log writes. Codex must be fully quit first. Existing rows stay until free space is reclaimed later.", + desiredMode: "Wanted", + observedMode: "Installed", compat: "Compatibility", + compatHelp: "Keep Codex-compatible TRACE filtering: drop known high-noise targets (SSE transport, hyper_util, etc.) while preserving unrelated TRACE rows.", quiet: "Quiet", + quietHelp: "Drop all TRACE rows. Keep DEBUG, INFO, WARN, and ERROR. Strongest write reduction.", disable: "Disable protection", + disableHelp: "Remove OpenCodex log filters and allow normal Codex logging again.", repair: "Repair protection", + repairHelp: "Re-install the wanted protection mode after Codex rebuilt the logs database.", + compactHelp: "Reclaim free pages already inside the logs database with bounded incremental vacuum. Does not delete log history by itself.", + rankedSources: "Noisiest sources", + rankedSourcesHelp: "Real source names stay private. Rank 1 is noisiest. Counts show relative write pressure only.", + rankedSourceRank: "Rank {rank}", + writePoster: "Future write load", + writePosterHelp: "How much Codex would still write under each mode, based on your current log mix. Protection changes future inserts only. Compact later if you want the file to shrink.", + writePosterOff: "Off (stock Codex)", + writePosterUnitBytes: "Bars show estimated payload size, not just row counts.", + writePosterUnitRows: "Bars show row share. Payload size is unavailable here.", applying: "Applying protection…", "error.generic": "Could not change Codex log protection.", "error.codex_running": "Quit Codex before changing log protection.", @@ -55,19 +87,35 @@ const LABELS: Record> = { }, de: { compact: 'Komprimieren', - compactComplete: "Komprimierung abgeschlossen (logisch / auf Datentraeger freigegeben)", + compactComplete: "Komprimierung abgeschlossen (logisch / auf Datenträger freigegeben)", pagesUnit: "Seiten", - compactPartial: "Komprimierung teilweise (logisch / auf Datentraeger freigegeben)", + compactPartial: "Komprimierung teilweise (logisch / auf Datenträger freigegeben)", confirmCompact: 'Komprimierung bestätigen', cancel: 'Abbrechen', inspectionOnly: 'Nur Inspektion', externalSqliteHome: 'Externer SQLite-Speicher', inspectionUnavailable: "Die Diagnoseprotokoll-Inspektion ist nicht verfügbar.", protection: "Schutz", + protectionHelp: "Verringert künftige Schreibvorgänge ins Codex-Diagnoseprotokoll. Beende Codex zuerst vollständig. Vorhandene Einträge bleiben, bis du Speicher später freigibst.", + desiredMode: "Gewählt", + observedMode: "Installiert", compat: "Kompatibilität", - quiet: "Leise", + compatHelp: "Filtert bekannte laute TRACE-Quellen, behält den Rest. Guter Standard, wenn Codex weiter normal laufen soll.", + quiet: "Ruhig", + quietHelp: "Verwirft jedes TRACE. Behält DEBUG, INFO, WARN und ERROR. Stärkste Entlastung der SSD.", disable: "Schutz deaktivieren", + disableHelp: "Entfernt die OpenCodex-Filter. Codex schreibt danach wieder wie gewohnt.", repair: "Schutz reparieren", + repairHelp: "Setzt den gewählten Schutz neu, nachdem Codex die Protokolldatenbank neu aufgebaut hat.", + compactHelp: "Gibt bereits freie Seiten in der Datenbank zurück. Löscht keine Historie.", + rankedSources: "Lauteste Quellen", + rankedSourcesHelp: "Echte Quellnamen bleiben privat. Rang 1 ist am lautesten. Die Zahlen zeigen nur den relativen Schreibdruck.", + rankedSourceRank: "Rang {rank}", + writePoster: "Künftige Schreiblast", + writePosterHelp: "So viel würde Codex künftig noch schreiben, gemessen an deinem aktuellen Protokollmix. Der Schutz ändert nur neue Einträge. Die Datei schrumpft erst nach Komprimieren.", + writePosterOff: "Aus (normales Codex)", + writePosterUnitBytes: "Die Balken zeigen geschätzte Datenmenge, nicht nur Zeilen.", + writePosterUnitRows: "Die Balken zeigen Zeilenanteile. Die Datenmenge ist hier nicht verfügbar.", applying: "Schutz wird angewendet…", "error.generic": "Der Schutz der Codex-Protokolle konnte nicht geändert werden.", "error.codex_running": "Beende Codex, bevor du den Protokollschutz änderst.", @@ -90,10 +138,26 @@ const LABELS: Record> = { externalSqliteHome: "Stockage SQLite externe", inspectionUnavailable: "L’inspection des journaux de diagnostic est indisponible.", protection: "Protection", + protectionHelp: "Reduces future Codex diagnostic log writes. Codex must be fully quit first. Existing rows stay until free space is reclaimed later.", + desiredMode: "Wanted", + observedMode: "Installed", compat: "Compatibilité", + compatHelp: "Keep Codex-compatible TRACE filtering: drop known high-noise targets (SSE transport, hyper_util, etc.) while preserving unrelated TRACE rows.", quiet: "Silencieux", + quietHelp: "Drop all TRACE rows. Keep DEBUG, INFO, WARN, and ERROR. Strongest write reduction.", disable: "Désactiver la protection", + disableHelp: "Remove OpenCodex log filters and allow normal Codex logging again.", repair: "Réparer la protection", + repairHelp: "Re-install the wanted protection mode after Codex rebuilt the logs database.", + compactHelp: "Reclaim free pages already inside the logs database with bounded incremental vacuum. Does not delete log history by itself.", + rankedSources: "Top sources (ranked)", + rankedSourcesHelp: "Real log source names stay private. Ranks show relative write pressure only (Rank 1 is noisiest).", + rankedSourceRank: "Rank {rank}", + writePoster: "Future diagnostic write load", + writePosterHelp: "Estimated share of future Codex diagnostic log writes that would still hit disk under each mode, based on your current log mix. Protection only changes future inserts; it does not shrink the existing database by itself.", + writePosterOff: "Off (stock Codex)", + writePosterUnitBytes: "Bars use estimated log payload bytes from your current mix.", + writePosterUnitRows: "Bars use row counts from your current mix (payload size unavailable).", applying: "Application de la protection…", "error.generic": "Impossible de modifier la protection des journaux Codex.", "error.codex_running": "Quittez Codex avant de modifier la protection des journaux.", @@ -116,10 +180,26 @@ const LABELS: Record> = { externalSqliteHome: '외부 SQLite 저장소', inspectionUnavailable: "진단 로그 검사를 사용할 수 없습니다.", protection: "보호", + protectionHelp: "Reduces future Codex diagnostic log writes. Codex must be fully quit first. Existing rows stay until free space is reclaimed later.", + desiredMode: "Wanted", + observedMode: "Installed", compat: "호환 모드", + compatHelp: "Keep Codex-compatible TRACE filtering: drop known high-noise targets (SSE transport, hyper_util, etc.) while preserving unrelated TRACE rows.", quiet: "조용한 모드", + quietHelp: "Drop all TRACE rows. Keep DEBUG, INFO, WARN, and ERROR. Strongest write reduction.", disable: "보호 비활성화", + disableHelp: "Remove OpenCodex log filters and allow normal Codex logging again.", repair: "보호 복구", + repairHelp: "Re-install the wanted protection mode after Codex rebuilt the logs database.", + compactHelp: "Reclaim free pages already inside the logs database with bounded incremental vacuum. Does not delete log history by itself.", + rankedSources: "Top sources (ranked)", + rankedSourcesHelp: "Real log source names stay private. Ranks show relative write pressure only (Rank 1 is noisiest).", + rankedSourceRank: "Rank {rank}", + writePoster: "Future diagnostic write load", + writePosterHelp: "Estimated share of future Codex diagnostic log writes that would still hit disk under each mode, based on your current log mix. Protection only changes future inserts; it does not shrink the existing database by itself.", + writePosterOff: "Off (stock Codex)", + writePosterUnitBytes: "Bars use estimated log payload bytes from your current mix.", + writePosterUnitRows: "Bars use row counts from your current mix (payload size unavailable).", applying: "보호 적용 중…", "error.generic": "Codex 로그 보호를 변경하지 못했습니다.", "error.codex_running": "로그 보호를 변경하기 전에 Codex를 종료하세요.", @@ -142,10 +222,26 @@ const LABELS: Record> = { externalSqliteHome: '外部 SQLite 存储', inspectionUnavailable: "诊断日志检查当前不可用。", protection: "保护", + protectionHelp: "Reduces future Codex diagnostic log writes. Codex must be fully quit first. Existing rows stay until free space is reclaimed later.", + desiredMode: "Wanted", + observedMode: "Installed", compat: "兼容模式", + compatHelp: "Keep Codex-compatible TRACE filtering: drop known high-noise targets (SSE transport, hyper_util, etc.) while preserving unrelated TRACE rows.", quiet: "静默模式", + quietHelp: "Drop all TRACE rows. Keep DEBUG, INFO, WARN, and ERROR. Strongest write reduction.", disable: "禁用保护", + disableHelp: "Remove OpenCodex log filters and allow normal Codex logging again.", repair: "修复保护", + repairHelp: "Re-install the wanted protection mode after Codex rebuilt the logs database.", + compactHelp: "Reclaim free pages already inside the logs database with bounded incremental vacuum. Does not delete log history by itself.", + rankedSources: "Top sources (ranked)", + rankedSourcesHelp: "Real log source names stay private. Ranks show relative write pressure only (Rank 1 is noisiest).", + rankedSourceRank: "Rank {rank}", + writePoster: "Future diagnostic write load", + writePosterHelp: "Estimated share of future Codex diagnostic log writes that would still hit disk under each mode, based on your current log mix. Protection only changes future inserts; it does not shrink the existing database by itself.", + writePosterOff: "Off (stock Codex)", + writePosterUnitBytes: "Bars use estimated log payload bytes from your current mix.", + writePosterUnitRows: "Bars use row counts from your current mix (payload size unavailable).", applying: "正在应用保护…", "error.generic": "无法更改 Codex 日志保护。", "error.codex_running": "更改 Codex 日志保护前请先退出 Codex。", @@ -168,10 +264,26 @@ const LABELS: Record> = { externalSqliteHome: '外部 SQLite 儲存空間', inspectionUnavailable: "診斷記錄檢查目前無法使用。", protection: "保護", + protectionHelp: "Reduces future Codex diagnostic log writes. Codex must be fully quit first. Existing rows stay until free space is reclaimed later.", + desiredMode: "Wanted", + observedMode: "Installed", compat: "相容模式", + compatHelp: "Keep Codex-compatible TRACE filtering: drop known high-noise targets (SSE transport, hyper_util, etc.) while preserving unrelated TRACE rows.", quiet: "靜默模式", + quietHelp: "Drop all TRACE rows. Keep DEBUG, INFO, WARN, and ERROR. Strongest write reduction.", disable: "停用保護", + disableHelp: "Remove OpenCodex log filters and allow normal Codex logging again.", repair: "修復保護", + repairHelp: "Re-install the wanted protection mode after Codex rebuilt the logs database.", + compactHelp: "Reclaim free pages already inside the logs database with bounded incremental vacuum. Does not delete log history by itself.", + rankedSources: "Top sources (ranked)", + rankedSourcesHelp: "Real log source names stay private. Ranks show relative write pressure only (Rank 1 is noisiest).", + rankedSourceRank: "Rank {rank}", + writePoster: "Future diagnostic write load", + writePosterHelp: "Estimated share of future Codex diagnostic log writes that would still hit disk under each mode, based on your current log mix. Protection only changes future inserts; it does not shrink the existing database by itself.", + writePosterOff: "Off (stock Codex)", + writePosterUnitBytes: "Bars use estimated log payload bytes from your current mix.", + writePosterUnitRows: "Bars use row counts from your current mix (payload size unavailable).", applying: "正在套用保護…", "error.generic": "無法變更 Codex 日誌保護。", "error.codex_running": "變更 Codex 日誌保護前請先退出 Codex。", @@ -194,10 +306,26 @@ const LABELS: Record> = { externalSqliteHome: 'Внешнее хранилище SQLite', inspectionUnavailable: "Проверка диагностических журналов недоступна.", protection: "Защита", + protectionHelp: "Reduces future Codex diagnostic log writes. Codex must be fully quit first. Existing rows stay until free space is reclaimed later.", + desiredMode: "Wanted", + observedMode: "Installed", compat: "Совместимость", + compatHelp: "Keep Codex-compatible TRACE filtering: drop known high-noise targets (SSE transport, hyper_util, etc.) while preserving unrelated TRACE rows.", quiet: "Тихий режим", + quietHelp: "Drop all TRACE rows. Keep DEBUG, INFO, WARN, and ERROR. Strongest write reduction.", disable: "Отключить защиту", + disableHelp: "Remove OpenCodex log filters and allow normal Codex logging again.", repair: "Восстановить защиту", + repairHelp: "Re-install the wanted protection mode after Codex rebuilt the logs database.", + compactHelp: "Reclaim free pages already inside the logs database with bounded incremental vacuum. Does not delete log history by itself.", + rankedSources: "Top sources (ranked)", + rankedSourcesHelp: "Real log source names stay private. Ranks show relative write pressure only (Rank 1 is noisiest).", + rankedSourceRank: "Rank {rank}", + writePoster: "Future diagnostic write load", + writePosterHelp: "Estimated share of future Codex diagnostic log writes that would still hit disk under each mode, based on your current log mix. Protection only changes future inserts; it does not shrink the existing database by itself.", + writePosterOff: "Off (stock Codex)", + writePosterUnitBytes: "Bars use estimated log payload bytes from your current mix.", + writePosterUnitRows: "Bars use row counts from your current mix (payload size unavailable).", applying: "Применение защиты…", "error.generic": "Не удалось изменить защиту журналов Codex.", "error.codex_running": "Закройте Codex перед изменением защиты журналов.", @@ -220,10 +348,26 @@ const LABELS: Record> = { externalSqliteHome: '外部 SQLite ストレージ', inspectionUnavailable: "診断ログの検査を利用できません。", protection: "保護", + protectionHelp: "Reduces future Codex diagnostic log writes. Codex must be fully quit first. Existing rows stay until free space is reclaimed later.", + desiredMode: "Wanted", + observedMode: "Installed", compat: "互換モード", + compatHelp: "Keep Codex-compatible TRACE filtering: drop known high-noise targets (SSE transport, hyper_util, etc.) while preserving unrelated TRACE rows.", quiet: "静音モード", + quietHelp: "Drop all TRACE rows. Keep DEBUG, INFO, WARN, and ERROR. Strongest write reduction.", disable: "保護を無効化", + disableHelp: "Remove OpenCodex log filters and allow normal Codex logging again.", repair: "保護を修復", + repairHelp: "Re-install the wanted protection mode after Codex rebuilt the logs database.", + compactHelp: "Reclaim free pages already inside the logs database with bounded incremental vacuum. Does not delete log history by itself.", + rankedSources: "Top sources (ranked)", + rankedSourcesHelp: "Real log source names stay private. Ranks show relative write pressure only (Rank 1 is noisiest).", + rankedSourceRank: "Rank {rank}", + writePoster: "Future diagnostic write load", + writePosterHelp: "Estimated share of future Codex diagnostic log writes that would still hit disk under each mode, based on your current log mix. Protection only changes future inserts; it does not shrink the existing database by itself.", + writePosterOff: "Off (stock Codex)", + writePosterUnitBytes: "Bars use estimated log payload bytes from your current mix.", + writePosterUnitRows: "Bars use row counts from your current mix (payload size unavailable).", applying: "保護を適用中…", "error.generic": "Codex ログ保護を変更できませんでした。", "error.codex_running": "ログ保護を変更する前に Codex を終了してください。", @@ -246,10 +390,26 @@ const LABELS: Record> = { externalSqliteHome: 'Harici SQLite depolaması', inspectionUnavailable: "Tanılama günlüğü incelemesi kullanılamıyor.", protection: "Koruma", + protectionHelp: "Reduces future Codex diagnostic log writes. Codex must be fully quit first. Existing rows stay until free space is reclaimed later.", + desiredMode: "Wanted", + observedMode: "Installed", compat: "Uyumluluk", + compatHelp: "Keep Codex-compatible TRACE filtering: drop known high-noise targets (SSE transport, hyper_util, etc.) while preserving unrelated TRACE rows.", quiet: "Sessiz", + quietHelp: "Drop all TRACE rows. Keep DEBUG, INFO, WARN, and ERROR. Strongest write reduction.", disable: "Korumayı devre dışı bırak", + disableHelp: "Remove OpenCodex log filters and allow normal Codex logging again.", repair: "Korumayı onar", + repairHelp: "Re-install the wanted protection mode after Codex rebuilt the logs database.", + compactHelp: "Reclaim free pages already inside the logs database with bounded incremental vacuum. Does not delete log history by itself.", + rankedSources: "Top sources (ranked)", + rankedSourcesHelp: "Real log source names stay private. Ranks show relative write pressure only (Rank 1 is noisiest).", + rankedSourceRank: "Rank {rank}", + writePoster: "Future diagnostic write load", + writePosterHelp: "Estimated share of future Codex diagnostic log writes that would still hit disk under each mode, based on your current log mix. Protection only changes future inserts; it does not shrink the existing database by itself.", + writePosterOff: "Off (stock Codex)", + writePosterUnitBytes: "Bars use estimated log payload bytes from your current mix.", + writePosterUnitRows: "Bars use row counts from your current mix (payload size unavailable).", applying: "Koruma uygulanıyor…", "error.generic": "Codex günlük koruması değiştirilemedi.", "error.codex_running": "Günlük korumasını değiştirmeden önce Codex'i kapatın.", diff --git a/gui/src/i18n/log-guard-state-labels.ts b/gui/src/i18n/log-guard-state-labels.ts index 176f1778c6..754cb8092b 100644 --- a/gui/src/i18n/log-guard-state-labels.ts +++ b/gui/src/i18n/log-guard-state-labels.ts @@ -14,12 +14,12 @@ const LABELS: Record = { en: { schema: { compatible: "Compatible", missing: "Database not found", unreadable: "Database unavailable", unsupported: "Unsupported" }, protection: { off: "Off", active: "Active", drifted: "Needs repair", unsupported: "Unsupported", unknown: "Unknown" }, - mode: { off: "Off", compat: "Compatibility", quiet: "Quiet", collision: "Unknown" }, + mode: { off: "Disabled", compat: "Compatibility", quiet: "Quiet", collision: "Unknown" }, }, de: { schema: { compatible: "Kompatibel", missing: "Datenbank nicht gefunden", unreadable: "Datenbank nicht lesbar", unsupported: "Nicht unterstützt" }, protection: { off: "Aus", active: "Aktiv", drifted: "Reparatur erforderlich", unsupported: "Nicht unterstützt", unknown: "Unbekannt" }, - mode: { off: "Aus", compat: "Kompatibilität", quiet: "Leise", collision: "Unbekannt" }, + mode: { off: "Deaktiviert", compat: "Kompatibilität", quiet: "Leise", collision: "Unbekannt" }, }, fr: { schema: { compatible: "Compatible", missing: "Base de données introuvable", unreadable: "Base de données indisponible", unsupported: "Non pris en charge" }, @@ -29,32 +29,32 @@ const LABELS: Record = { ko: { schema: { compatible: "호환됨", missing: "데이터베이스 없음", unreadable: "데이터베이스를 읽을 수 없음", unsupported: "지원되지 않음" }, protection: { off: "꺼짐", active: "활성", drifted: "복구 필요", unsupported: "지원되지 않음", unknown: "알 수 없음" }, - mode: { off: "꺼짐", compat: "호환 모드", quiet: "조용한 모드", collision: "알 수 없음" }, + mode: { off: "비활성", compat: "호환 모드", quiet: "조용한 모드", collision: "알 수 없음" }, }, zh: { schema: { compatible: "兼容", missing: "未找到数据库", unreadable: "无法读取数据库", unsupported: "不受支持" }, protection: { off: "关闭", active: "已启用", drifted: "需要修复", unsupported: "不受支持", unknown: "未知" }, - mode: { off: "关闭", compat: "兼容模式", quiet: "静默模式", collision: "未知" }, + mode: { off: "已禁用", compat: "兼容模式", quiet: "静默模式", collision: "未知" }, }, "zh-TW": { schema: { compatible: "相容", missing: "找不到資料庫", unreadable: "無法讀取資料庫", unsupported: "不支援" }, protection: { off: "關閉", active: "已啟用", drifted: "需要修復", unsupported: "不支援", unknown: "未知" }, - mode: { off: "關閉", compat: "相容模式", quiet: "靜默模式", collision: "未知" }, + mode: { off: "已停用", compat: "相容模式", quiet: "靜默模式", collision: "未知" }, }, ru: { schema: { compatible: "Совместимо", missing: "База не найдена", unreadable: "База недоступна", unsupported: "Не поддерживается" }, protection: { off: "Выключено", active: "Активно", drifted: "Требуется восстановление", unsupported: "Не поддерживается", unknown: "Неизвестно" }, - mode: { off: "Выключено", compat: "Совместимость", quiet: "Тихий режим", collision: "Неизвестно" }, + mode: { off: "Отключено", compat: "Совместимость", quiet: "Тихий режим", collision: "Неизвестно" }, }, ja: { schema: { compatible: "互換", missing: "データベースが見つかりません", unreadable: "データベースを読み取れません", unsupported: "未対応" }, protection: { off: "オフ", active: "有効", drifted: "修復が必要", unsupported: "未対応", unknown: "不明" }, - mode: { off: "オフ", compat: "互換モード", quiet: "静音モード", collision: "不明" }, + mode: { off: "無効", compat: "互換モード", quiet: "静音モード", collision: "不明" }, }, tr: { schema: { compatible: "Uyumlu", missing: "Veritabanı bulunamadı", unreadable: "Veritabanı okunamıyor", unsupported: "Desteklenmiyor" }, protection: { off: "Kapalı", active: "Etkin", drifted: "Onarım gerekli", unsupported: "Desteklenmiyor", unknown: "Bilinmiyor" }, - mode: { off: "Kapalı", compat: "Uyumluluk", quiet: "Sessiz", collision: "Bilinmiyor" }, + mode: { off: "Devre dışı", compat: "Uyumluluk", quiet: "Sessiz", collision: "Bilinmiyor" }, }, }; diff --git a/gui/src/styles-storage-workspace.css b/gui/src/styles-storage-workspace.css index 1585ac0f56..fb1425b6ad 100644 --- a/gui/src/styles-storage-workspace.css +++ b/gui/src/styles-storage-workspace.css @@ -742,3 +742,90 @@ position: static; } } + + +/* Log Guard future-write poster: relative SSD write load by protection mode. */ +.log-guard-write-poster { + display: flex; + flex-direction: column; + gap: 0.55rem; +} + +.log-guard-write-poster-row { + display: flex; + flex-direction: column; + gap: 0.28rem; +} + +.log-guard-write-poster-meta { + display: flex; + align-items: baseline; + justify-content: space-between; + gap: 0.75rem; +} + +.log-guard-write-poster-mode { + color: var(--text-secondary, #9aa4b2); + font-size: 0.82rem; +} + +.log-guard-write-poster-value { + font-variant-numeric: tabular-nums; + font-size: 0.82rem; +} + +.log-guard-write-poster-track { + display: block; + width: 100%; + height: 0.55rem; + border: 0; + border-radius: 999px; + background: color-mix(in srgb, var(--border, #2a3340) 70%, transparent); + overflow: hidden; + appearance: none; +} + +.log-guard-write-poster-track::-webkit-meter-inner-element { + display: block; +} + +.log-guard-write-poster-track::-webkit-meter-bar { + background: color-mix(in srgb, var(--border, #2a3340) 70%, transparent); + border: 0; + border-radius: 999px; + height: 100%; +} + +.log-guard-write-poster-track::-webkit-meter-optimum-value, +.log-guard-write-poster-track::-webkit-meter-suboptimum-value, +.log-guard-write-poster-track::-webkit-meter-even-less-good-value { + background: var(--accent, #3b82f6); + border-radius: 999px; +} + +.log-guard-write-poster-track::-moz-meter-bar { + background: var(--accent, #3b82f6); + border-radius: 999px; +} + + +.stw-overview-tabs { + margin: 12px 0 14px; +} + +.stw-overview-pane { + min-height: 8rem; +} + +.log-guard-mode-actions { + display: flex; + flex-wrap: wrap; + gap: 6px; + padding-top: 2px; +} + +[data-testid="log-guard-reclaim"] { + margin-top: 18px; + padding-top: 12px; + border-top: 1px solid var(--border); +} diff --git a/gui/tests/storage-log-guard-protection.test.tsx b/gui/tests/storage-log-guard-protection.test.tsx index 67507fdce3..83546e6e79 100644 --- a/gui/tests/storage-log-guard-protection.test.tsx +++ b/gui/tests/storage-log-guard-protection.test.tsx @@ -56,7 +56,7 @@ test("Storage exposes compatibility, quiet, and disable protection controls", () expect(html).toContain("Disable protection"); expect(html).toContain("Active"); expect(html).not.toContain(">active<"); - expect(html).not.toContain("Compact"); + expect(html).not.toContain("data-testid=\"log-guard-compact\""); }); test("Storage exposes explicit repair only when protection drifted", () => { @@ -80,7 +80,7 @@ test("Storage localizes protection state and desired/observed modes", () => { expect(html).toContain("Reparatur erforderlich"); expect(html).toContain("Kompatibilität"); - expect(html).toContain("Aus"); + expect(html).toContain("Deaktiviert"); expect(html).not.toContain(">drifted<"); expect(html).not.toContain(">compat<"); }); @@ -99,5 +99,54 @@ test("Storage disables protection mutation controls for an unsupported schema", const html = render(value); expect(html).toMatch(/data-testid="log-guard-protect-compat"[^>]*disabled/); expect(html).toMatch(/data-testid="log-guard-protect-quiet"[^>]*disabled/); - expect(html).not.toContain("Compact"); + expect(html).not.toContain("data-testid=\"log-guard-compact\""); +}); + + +test("Storage separates protection status from wanted mode and exposes mode help", () => { + const html = render(report("off")); + expect(html).toContain("Wanted"); + expect(html).toContain("Disabled"); + expect(html).toContain("Off"); + expect(html).not.toMatch(/>Off<.*·.*>Off { + const value = report("active"); + value.codexLogs = { + ...value.codexLogs!, + metrics: { + totalRows: 1000, + rowsByLevel: { TRACE: 400, INFO: 600 }, + traceRows: 400, + traceShare: 0.4, + topTargets: [{ target: "TARGET_1", rows: 300 }], + pageSize: 4096, + pageCount: 10, + freelistPages: 0, + reclaimableBytes: 0, + estimatedLogBytes: 900, + writePoster: { + off: { keepRowsShare: 1, keepBytesShare: 1 }, + compat: { keepRowsShare: 0.8, keepBytesShare: 0.12 }, + quiet: { keepRowsShare: 0.6, keepBytesShare: 0.08 }, + }, + }, + }; + + const html = render(value); + expect(html).toContain('data-testid="log-guard-write-poster"'); + expect(html).toContain("Future write load"); + expect(html).toContain("12%"); + expect(html).toContain("8%"); + expect(html).toMatch(/data-testid="log-guard-write-poster-compat"[\s\S]*?value="12"/); + expect(html).toMatch(/data-testid="log-guard-write-poster-quiet"[\s\S]*?value="8"/); + expect(html).toContain("Rank 1"); + // Ranked sources stay redacted; help copy may mention class names abstractly. + expect(html).toContain("Rank 1"); + expect(html).not.toMatch(/codex_http_client::transport<\/code>/); + expect(html).not.toMatch(/hyper_util<\/code>/); }); diff --git a/gui/tests/storage-log-guard.test.tsx b/gui/tests/storage-log-guard.test.tsx index 272f925b14..f3805840e8 100644 --- a/gui/tests/storage-log-guard.test.tsx +++ b/gui/tests/storage-log-guard.test.tsx @@ -33,6 +33,11 @@ function report(): StorageReport { freelistPages: 1, reclaimableBytes: 4096, estimatedLogBytes: 350, + writePoster: { + off: { keepRowsShare: 1, keepBytesShare: 1 }, + compat: { keepRowsShare: 0.82, keepBytesShare: 0.05 }, + quiet: { keepRowsShare: 0.56, keepBytesShare: 0.04 }, + }, }, }, }; @@ -61,13 +66,29 @@ test("Storage overview renders read-only Codex diagnostic log health", () => { expect(html).toContain("WAL"); expect(html).toContain("SHM"); expect(html).toContain("0 B"); - expect(html).toContain("TARGET_1"); + expect(html).not.toContain("TARGET_1"); + expect(html).toContain("Noisiest sources"); + expect(html).toContain("Rank 1"); + expect(html).toContain("Real source names stay private"); expect(html).not.toContain("codex_api::sse"); + expect(html).toContain('data-testid="storage-tab-log-guard"'); + expect(html).toContain('data-testid="storage-tab-largest"'); + expect(html).toContain('data-testid="storage-pane-log-guard"'); + expect(html).not.toContain('data-testid="storage-pane-largest"'); + expect(html).toContain('data-testid="log-guard-write-poster"'); + expect(html).toContain("Future write load"); + expect(html).toContain("Off (stock Codex)"); + expect(html).toContain("100%"); + expect(html).toContain("5%"); + expect(html).toContain("4%"); + expect(html).toMatch(/data-testid="log-guard-write-poster-off"[\s\S]*?value="100"/); + expect(html).toMatch(/data-testid="log-guard-write-poster-compat"[\s\S]*?value="5"/); + expect(html).toMatch(/data-testid="log-guard-write-poster-quiet"[\s\S]*?value="4"/); + expect(html).toContain("Bars show estimated payload size"); expect(html).toContain("External SQLite storage"); - expect(html).toContain("snapshot=checkpointed"); expect(html).not.toContain("/state/codex"); - expect(html).not.toContain("Protect"); - expect(html).not.toContain("Compact"); + expect(html).not.toContain('data-testid="log-guard-protect-compat"'); + expect(html).not.toContain('data-testid="log-guard-compact"'); expect(html).not.toContain("High write activity"); }); diff --git a/src/codex/app-server-processes.ts b/src/codex/app-server-processes.ts index 60c778e6b3..e49ee41e29 100644 --- a/src/codex/app-server-processes.ts +++ b/src/codex/app-server-processes.ts @@ -384,7 +384,7 @@ export function listWindowsSnapshots(): ProcessSnapshot[] { "-NoProfile", "-NoLogo", "-NonInteractive", "-Command", psCommand, - ], { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"], timeout: 8_000, windowsHide: true }); + ], { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"], timeout: 30_000, windowsHide: true }); for (const line of output.split(/\r?\n/)) { // A candidate whose owner could not be verified makes the whole // enumeration incomplete — the staleness collector must not read the diff --git a/src/codex/log-guard/inspect.ts b/src/codex/log-guard/inspect.ts index d2c1719236..6970a5fb4f 100644 --- a/src/codex/log-guard/inspect.ts +++ b/src/codex/log-guard/inspect.ts @@ -8,6 +8,7 @@ import { resolveCodexSqliteHome, type CodexSqliteHomeDeps, } from "../paths"; +import { codexLogGuardCompatDropWhereSql } from "./protection"; const IMMUTABLE_READONLY_FLAGS = constants.SQLITE_OPEN_READONLY | constants.SQLITE_OPEN_URI; const KNOWN_LOG_LEVELS = new Set(["TRACE", "DEBUG", "INFO", "WARN", "ERROR"]); @@ -79,6 +80,20 @@ export type CodexLogGuardCapability = | { state: "supported" } | { state: "unsupported"; reason: CodexLogGuardCapabilityReason }; +export interface CodexLogGuardModeWriteShare { + /** Share of historical rows that would still be written under this mode. */ + keepRowsShare: number; + /** Share of historical estimated log bytes that would still be written, when available. */ + keepBytesShare: number | null; +} + +export interface CodexLogGuardWritePoster { + /** Counterfactual keep-shares for future-write guidance from the current snapshot mix. */ + off: CodexLogGuardModeWriteShare; + compat: CodexLogGuardModeWriteShare; + quiet: CodexLogGuardModeWriteShare; +} + export interface CodexLogGuardMetrics { totalRows: number; rowsByLevel: Record; @@ -90,6 +105,7 @@ export interface CodexLogGuardMetrics { freelistPages: number; reclaimableBytes: number; estimatedLogBytes: number | null; + writePoster: CodexLogGuardWritePoster; } /** @@ -347,6 +363,46 @@ function readMetrics(db: Database, columns: string[]): CodexLogGuardMetrics | nu ).get()?.bytes ?? 0) : null; + const compatWhere = codexLogGuardCompatDropWhereSql("target", "upper(level)"); + const quietWhere = "upper(level) = 'TRACE'"; + const droppedCompatRows = totalRows > 0 + ? Number(db.query("SELECT count(*) AS n FROM logs WHERE " + compatWhere).get()?.n ?? 0) + : 0; + const droppedQuietRows = totalRows > 0 + ? Number(db.query("SELECT count(*) AS n FROM logs WHERE " + quietWhere).get()?.n ?? 0) + : 0; + let droppedCompatBytes: number | null = null; + let droppedQuietBytes: number | null = null; + if (estimatedLogBytes !== null && estimatedLogBytes > 0) { + droppedCompatBytes = Number(db.query( + "SELECT COALESCE(sum(estimated_bytes), 0) AS bytes FROM logs WHERE " + compatWhere, + ).get()?.bytes ?? 0); + droppedQuietBytes = Number(db.query( + "SELECT COALESCE(sum(estimated_bytes), 0) AS bytes FROM logs WHERE " + quietWhere, + ).get()?.bytes ?? 0); + } + const share = (keep: number, total: number) => (total > 0 ? Math.min(1, Math.max(0, keep / total)) : 1); + const writePoster = { + off: { + keepRowsShare: 1, + // Keep unit selection consistent with compat/quiet: when estimated bytes + // exist but total 0, byte shares stay null so the GUI uses row shares. + keepBytesShare: estimatedLogBytes !== null && estimatedLogBytes > 0 ? 1 : null, + }, + compat: { + keepRowsShare: share(totalRows - droppedCompatRows, totalRows), + keepBytesShare: estimatedLogBytes === null || droppedCompatBytes === null + ? null + : share(estimatedLogBytes - droppedCompatBytes, estimatedLogBytes), + }, + quiet: { + keepRowsShare: share(totalRows - droppedQuietRows, totalRows), + keepBytesShare: estimatedLogBytes === null || droppedQuietBytes === null + ? null + : share(estimatedLogBytes - droppedQuietBytes, estimatedLogBytes), + }, + }; + return { totalRows, rowsByLevel, @@ -358,6 +414,7 @@ function readMetrics(db: Database, columns: string[]): CodexLogGuardMetrics | nu freelistPages, reclaimableBytes: pageSize * freelistPages, estimatedLogBytes, + writePoster, }; } catch { // A future schema may still have a `logs` table but change aggregate-compatible diff --git a/src/codex/log-guard/protection.ts b/src/codex/log-guard/protection.ts index 613d2677ce..3b90b6fb4b 100644 --- a/src/codex/log-guard/protection.ts +++ b/src/codex/log-guard/protection.ts @@ -79,23 +79,40 @@ function anyTargetOrDescendant(targets: readonly string[]): string { return `(${targets.map(targetOrDescendant).join(" OR ")})`; } + +/** + * SQL boolean predicate (no leading WHERE) matching the Compatibility trigger. + * Used by Inspect for privacy-safe historical impact meters without leaking target names. + */ +export function codexLogGuardCompatDropWhereSql( + targetExpr = "target", + levelExpr = "upper(level)", +): string { + const desc = (base: string) => + "(" + targetExpr + " = '" + base + "' OR substr(" + targetExpr + ", 1, " + (base.length + 2) + ") = '" + base + "::')"; + const anyDesc = (bases: readonly string[]) => "(" + bases.map(desc).join(" OR ") + ")"; + return [ + targetExpr + " = 'log'", + targetExpr + " = 'codex_otel.log_only'", + targetExpr + " = 'codex_otel.trace_safe'", + targetExpr + " = 'codex_api::responses_websocket_timing'", + targetExpr + " = 'codex_core::post_sampling_token_estimate'", + "(" + desc("hyper_util") + " AND " + levelExpr + " IN ('TRACE', 'DEBUG', 'INFO'))", + "(" + anyDesc(["codex_rmcp_client", "rmcp"]) + " AND " + levelExpr + " IN ('TRACE', 'DEBUG'))", + "(" + anyDesc([ + "codex_http_client::transport", + "codex_api::sse", + "codex_tui::streaming::controller", + "codex_tui::streaming::table_holdback", + ]) + " AND " + levelExpr + " = 'TRACE')", + "(" + targetExpr + " = 'opentelemetry_sdk' AND " + levelExpr + " IN ('TRACE', 'DEBUG'))", + ].join("\n OR "); +} + const COMPAT_TRIGGER_SQL = `CREATE TRIGGER ${COMPAT_TRIGGER} BEFORE INSERT ON logs WHEN - NEW.target = 'log' - OR NEW.target = 'codex_otel.log_only' - OR NEW.target = 'codex_otel.trace_safe' - OR NEW.target = 'codex_api::responses_websocket_timing' - OR NEW.target = 'codex_core::post_sampling_token_estimate' - OR (${targetOrDescendant("hyper_util")} AND upper(NEW.level) IN ('TRACE', 'DEBUG', 'INFO')) - OR (${anyTargetOrDescendant(["codex_rmcp_client", "rmcp"])} AND upper(NEW.level) IN ('TRACE', 'DEBUG')) - OR (${anyTargetOrDescendant([ - "codex_http_client::transport", - "codex_api::sse", - "codex_tui::streaming::controller", - "codex_tui::streaming::table_holdback", - ])} AND upper(NEW.level) = 'TRACE') - OR (NEW.target = 'opentelemetry_sdk' AND upper(NEW.level) IN ('TRACE', 'DEBUG')) + ${codexLogGuardCompatDropWhereSql("NEW.target", "upper(NEW.level)")} BEGIN SELECT RAISE(IGNORE); END`; diff --git a/tests/codex-app-server-processes.test.ts b/tests/codex-app-server-processes.test.ts index 7d89663ec3..127fd54da3 100644 --- a/tests/codex-app-server-processes.test.ts +++ b/tests/codex-app-server-processes.test.ts @@ -83,6 +83,24 @@ describe("collectCodexAppServerCatalogState (#857)", () => { expect(status.state).toBe("not_running"); }); + test("Windows snapshot enumeration uses a 30s PowerShell timeout", () => { + // Log Guard apply waits for Codex process exit; a short PowerShell timeout + // previously made "Codex is still running" look like "enumeration failed". + const source = readFileSync( + join(import.meta.dir, "../src/codex/app-server-processes.ts"), + "utf8", + ); + expect(source).toContain("export function listWindowsSnapshots"); + const start = source.indexOf("export function listWindowsSnapshots"); + expect(start).toBeGreaterThanOrEqual(0); + const nextExport = source.indexOf("\nexport function", start + 1); + const windowsSource = source.slice( + start, + nextExport === -1 ? source.length : nextExport, + ); + expect(windowsSource).toMatch(/execFileSync\([\s\S]*timeout:\s*30_000/); + }); + test("enumeration failure reports unknown, never not_running", () => { // On macOS the win32 enumeration path has no powershell.exe → it throws, // which must surface as unknown rather than "nothing is running". diff --git a/tests/codex-log-guard-inspect.test.ts b/tests/codex-log-guard-inspect.test.ts index 96deb06672..890e5d70ab 100644 --- a/tests/codex-log-guard-inspect.test.ts +++ b/tests/codex-log-guard-inspect.test.ts @@ -113,6 +113,33 @@ describe("Codex Log Guard inspection", () => { expect(report.metrics?.traceShare).toBe(0.5); expect(report.metrics?.topTargets[0]).toEqual({ target: "TARGET_1", rows: 2 }); expect(report.metrics?.reclaimableBytes).toBeGreaterThanOrEqual(0); + // Fixture: 2 TRACE codex_api::sse rows (300 bytes) + INFO/WARN (125 bytes). + // Compatibility and Quiet both drop those TRACE rows on this mix. + expect(report.metrics?.writePoster).toEqual({ + off: { keepRowsShare: 1, keepBytesShare: 1 }, + compat: { keepRowsShare: 0.5, keepBytesShare: 125 / 425 }, + quiet: { keepRowsShare: 0.5, keepBytesShare: 125 / 425 }, + }); + }); + + test("write poster byte shares stay null when estimated bytes total zero", () => { + const root = makeRoot(); + const codexHome = join(root, "codex-home"); + const databasePath = join(codexHome, "logs_2.sqlite"); + mkdirSync(codexHome); + writeFileSync(join(codexHome, "config.toml"), ""); + createCurrentLogsDb(databasePath); + + const db = new Database(databasePath); + db.exec("UPDATE logs SET estimated_bytes = 0"); + db.close(); + + const report = inspectCodexLogs({ codexHome }); + expect(report.metrics?.writePoster).toEqual({ + off: { keepRowsShare: 1, keepBytesShare: null }, + compat: { keepRowsShare: 0.5, keepBytesShare: null }, + quiet: { keepRowsShare: 0.5, keepBytesShare: null }, + }); }); test("never exposes feedback bodies, arbitrary levels, target names, or paths", () => { diff --git a/tests/codex-log-guard-protection.test.ts b/tests/codex-log-guard-protection.test.ts index a2e0a7be8f..3c977ac93f 100644 --- a/tests/codex-log-guard-protection.test.ts +++ b/tests/codex-log-guard-protection.test.ts @@ -5,6 +5,7 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; import { + codexLogGuardCompatDropWhereSql, getCodexLogGuardProtectionStatus, protectCodexLogs, repairCodexLogGuardProtection, @@ -327,6 +328,20 @@ describe("Codex Log Guard protection", () => { expect(unprotectCodexLogs(deps).ok).toBe(true); expect(triggers(databasePath)).toEqual([]); }); + test("compat trigger WHEN clause is generated from the inspect drop helper", () => { + // Write-poster impact meters use codexLogGuardCompatDropWhereSql(); the + // installed trigger must use that same predicate so reported shares match + // the rows Compatibility actually drops. + const { codexHome, databasePath } = fixture(); + const deps = testDeps(codexHome); + expect(protectCodexLogs("compat", deps).ok).toBe(true); + const sql = triggers(databasePath).find((row) => row.name === "opencodex_log_guard_compat_v1")?.sql ?? ""; + const expected = codexLogGuardCompatDropWhereSql("NEW.target", "upper(NEW.level)"); + // SQLite may reformat whitespace when storing CREATE TRIGGER SQL. + const normalize = (value: string) => value.replace(/\s+/g, " ").trim(); + expect(normalize(sql)).toContain(normalize(expected)); + }); + test("a schema change between inspection and the locked write is refused", () => { // TOCTOU: the lock serializes OpenCodex against itself, not against Codex or // another SQLite writer. The locked recheck used to compare column NAMES