Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 69 additions & 0 deletions docs-site/src/content/docs/guides/codex-log-guard.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
---
title: Codex Log Guard
description: Inspect Codex diagnostic-log storage safely before enabling future protection or reclaim actions.
---

OpenCodex can inspect Codex's persistent diagnostic-log database from the **Storage** page and from the CLI. The inspection surface is deliberately read-only: it does not install triggers, delete logs, checkpoint SQLite, vacuum the database, or change Codex configuration.

## What Inspect reports

OpenCodex resolves Codex's effective `sqlite_home` using Codex's existing precedence and inspects the canonical `logs_2.sqlite` database there. A higher-numbered or legacy `logs_N.sqlite` file is never substituted as the mutation-capable target.

The Storage view reports:

- the main database, WAL, and SHM file sizes;
- total log rows and the share stored at `TRACE` level;
- the largest log-target buckets by row count, using rank labels instead of target names;
- SQLite freelist space that may be reclaimable later; and
- whether the observed schema is compatible with the currently known Codex log schema.

If `sqlite_home` is outside `CODEX_HOME`, the diagnostic database is shown separately. Its bytes are not silently folded into the existing `CODEX_HOME` storage total.

OpenCodex does not select or expose `feedback_log_body` while producing these diagnostics. Log levels are reduced to the fixed known level set plus `OTHER`, and target names are not serialized.

## CLI

```bash
ocx storage codex-logs status
ocx storage codex-logs status --json
ocx doctor
```

The existing command remains unchanged:

```bash
ocx storage --json
```

Its response now also carries the same Codex-log inspection report used by the Storage page.
Comment thread
coderabbitai[bot] marked this conversation as resolved.

## Management API

```text
GET /api/storage/codex-logs
```

`GET /api/storage` also includes the report as `codexLogs` so the dashboard can refresh the normal storage breakdown and Codex-log diagnostics from one snapshot request.

## Read-only snapshot semantics

Inspection opens the database read-only with SQLite `immutable=1`. This prevents the diagnostic read itself from creating or updating `-wal` or `-shm` sidecars.

The trade-off is important: SQL aggregates describe the last checkpointed database snapshot. If Codex is actively writing, the live WAL can contain newer rows than the aggregate counts. OpenCodex therefore reports the WAL file size separately and does **not** label the result as SSD write rate, NAND writes, or drive-wear/TBW consumption.

## Compatibility states

A known schema reports inspection, future protection, and future reclaim capabilities as supported. A missing, unreadable, or unknown future schema remains inspectable as metadata but is reported as unsupported for mutation-capable operations.

An unknown schema is not guessed into compatibility. This lets a newer Codex version remain observable while preventing later Log Guard releases from treating an unreviewed database layout as safe to modify.

## What is not in this stage

This is **Inspect**, the first Log Guard stage. It does not reduce Codex writes by itself and does not reclaim database pages.

Later stages are intentionally separate:

- **Protect** will add explicit write-reduction modes after safety checks and Codex-process quiescence.
- **Reclaim** will add explicit, offline, bounded SQLite space reclamation.

Neither action is automatically enabled by Inspect.
119 changes: 119 additions & 0 deletions gui/src/components/storage-workspace/StorageWorkspace.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
import { useMemo, useState } from "react";
import { IconChevron, IconHardDrive } from "../../icons";
import { useT, type TFn, type TKey, type Locale } from "../../i18n/shared";
import { logGuardLabel } from "../../i18n/log-guard-labels";
import { logGuardSchemaStateLabel } from "../../i18n/log-guard-state-labels";
import { formatBytes } from "../../format-bytes";

export interface StorageLargestEntry {
Expand All @@ -26,11 +28,46 @@ export interface StorageBucket {
rows?: number | null;
}

type LogGuardReason = "database_missing" | "database_unreadable" | "unknown_schema";
type LogGuardCapability = { state: "supported" } | { state: "unsupported"; reason: LogGuardReason };
type LogGuardSchema =
| { state: "compatible" }
| { state: "missing"; reason: "database_missing" }
| { state: "unreadable"; reason: "database_unreadable" }
| { state: "unsupported"; reason: "unknown_schema" };

export interface CodexLogGuardReport {
generatedAt: number;
externalSqliteHome: boolean;
snapshot: "checkpointed";
files: { databaseBytes: number; walBytes: number; shmBytes: number };
schema: LogGuardSchema;
capabilities: {
inspection: LogGuardCapability;
protection: LogGuardCapability;
reclaim: LogGuardCapability;
};
metrics: null | {
totalRows: number;
rowsByLevel: Record<string, number>;
traceRows: number;
traceShare: number;
topTargets: Array<{ target: string; rows: number }>;
pageSize: number;
pageCount: number;
freelistPages: number;
reclaimableBytes: number;
estimatedLogBytes: number | null;
};
}

export interface StorageReport {
codexHome: string;
generatedAt: number;
total: { bytes: number; fileCount: number };
buckets: StorageBucket[];
codexLogs?: CodexLogGuardReport | null;
codexLogsError?: string;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
error?: string;
}

Expand Down Expand Up @@ -60,6 +97,82 @@ function rowsDisplay(bucket: StorageBucket, locale: Locale, t: TFn): string {
return bucket.rows.toLocaleString(locale);
}

function CodexLogGuardPanel({ report, locale, t }: { report: CodexLogGuardReport; locale: Locale; t: TFn }) {
const metrics = report.metrics;
const inspectOnly = report.capabilities.protection.state === "unsupported"
|| report.capabilities.reclaim.state === "unsupported";

return (
<div className="stw-section" data-testid="codex-log-guard">
<h3 className="stw-section-title">{t("storage.bucket.logs_db")}</h3>
<dl className="stw-kv">
<div className="stw-kv-row">
<dt>{t("dash.status")}</dt>
<dd className="stw-kv-mono">
<code>{logGuardSchemaStateLabel(locale, report.schema.state)}</code>
{inspectOnly && report.schema.state === "unsupported" ? <><span aria-hidden="true"> · </span><code>{logGuardLabel(locale, "inspectionOnly")}</code></> : null}
</dd>
</div>
<div className="stw-kv-row">
<dt>{t("storage.bucket.logs_db")}</dt>
<dd className="stw-kv-mono">{formatBytes(report.files.databaseBytes, locale)}</dd>
</div>
<div className="stw-kv-row">
<dt><code>WAL</code></dt>
<dd className="stw-kv-mono">{formatBytes(report.files.walBytes, locale)}</dd>
</div>
Comment thread
coderabbitai[bot] marked this conversation as resolved.
<div className="stw-kv-row">
<dt><code>SHM</code></dt>
<dd className="stw-kv-mono">{formatBytes(report.files.shmBytes, locale)}</dd>
</div>
{metrics && (
<>
<div className="stw-kv-row">
<dt>{t("storage.col.rows")}</dt>
<dd className="stw-kv-mono">{metrics.totalRows.toLocaleString(locale)}</dd>
</div>
<div className="stw-kv-row">
<dt><code>TRACE</code></dt>
<dd className="stw-kv-mono">{(metrics.traceShare * 100).toFixed(1)}%</dd>
</div>
<div className="stw-kv-row">
<dt><code>freelist</code></dt>
<dd className="stw-kv-mono">{formatBytes(metrics.reclaimableBytes, locale)}</dd>
Comment thread
coderabbitai[bot] marked this conversation as resolved.
</div>
</>
)}
<div className="stw-kv-row">
<dt><code>sqlite_home</code></dt>
<dd className="stw-kv-mono">
<code>{report.externalSqliteHome ? logGuardLabel(locale, "externalSqliteHome") : "CODEX_HOME"}</code>
</dd>
</div>
</dl>
{metrics && metrics.topTargets.length > 0 && (
<div className="stw-section">
<h4 className="stw-section-title"><code>target</code></h4>
{metrics.topTargets.slice(0, 5).map(target => (
<div key={target.target} className="stw-file-row">
<span className="stw-file-path" title={target.target}><code>{target.target}</code></span>
<span className="stw-file-size">{target.rows.toLocaleString(locale)}</span>
Comment thread
coderabbitai[bot] marked this conversation as resolved.
</div>
))}
</div>
)}
<p className="stw-hint"><code>immutable=1 · snapshot={report.snapshot}</code></p>
</div>
);
}

function CodexLogGuardUnavailablePanel({ locale, t }: { locale: Locale; t: TFn }) {
return (
<div className="stw-section" data-testid="codex-log-guard-unavailable">
<h3 className="stw-section-title">{t("storage.bucket.logs_db")}</h3>
<p className="stw-hint">{logGuardLabel(locale, "inspectionUnavailable")}</p>
</div>
);
}

export interface StorageWorkspaceProps {
report: StorageReport;
locale: Locale;
Expand Down Expand Up @@ -184,6 +297,12 @@ export default function StorageWorkspace({ report, locale }: StorageWorkspacePro
</div>
</div>

{report.codexLogs ? (
<CodexLogGuardPanel report={report.codexLogs} locale={locale} t={t} />
) : report.codexLogsError === "inspect_failed" ? (
<CodexLogGuardUnavailablePanel locale={locale} t={t} />
) : null}

{largestAcross.length > 0 ? (
<div className="stw-section">
<h3 className="stw-section-title">{t("storage.section.largest")}</h3>
Expand Down
4 changes: 2 additions & 2 deletions gui/src/format-bytes.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import type { Locale } from "./i18n/shared";

/** Human-readable byte size (1.5 MB, 320 KB). Unit symbols are locale-invariant like model ids. */
/** Human-readable byte size (1.5 MiB, 320 KiB). Binary unit symbols are locale-invariant like model ids. */
export function formatBytes(bytes: number, locale: Locale): string {
if (bytes < 1024) return `${bytes} B`;
const units = ["KB", "MB", "GB", "TB"];
const units = ["KiB", "MiB", "GiB", "TiB"];
let value = bytes;
let unit = -1;
do {
Expand Down
55 changes: 55 additions & 0 deletions gui/src/i18n/log-guard-labels.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import type { Locale } from "./catalogs";

export type LogGuardLabelKey = "inspectionOnly" | "externalSqliteHome" | "inspectionUnavailable";

const LABELS: Record<Locale, Record<LogGuardLabelKey, string>> = {
en: {
inspectionOnly: "Inspection only",
externalSqliteHome: "External SQLite storage",
inspectionUnavailable: "Diagnostic log inspection is unavailable.",
},
de: {
inspectionOnly: "Nur Inspektion",
externalSqliteHome: "Externer SQLite-Speicher",
inspectionUnavailable: "Die Diagnoseprotokoll-Inspektion ist nicht verfügbar.",
},
fr: {
inspectionOnly: "Inspection uniquement",
externalSqliteHome: "Stockage SQLite externe",
inspectionUnavailable: "L’inspection des journaux de diagnostic est indisponible.",
},
ko: {
inspectionOnly: "검사 전용",
externalSqliteHome: "외부 SQLite 저장소",
inspectionUnavailable: "진단 로그 검사를 사용할 수 없습니다.",
},
zh: {
inspectionOnly: "仅检查",
externalSqliteHome: "外部 SQLite 存储",
inspectionUnavailable: "诊断日志检查当前不可用。",
},
"zh-TW": {
inspectionOnly: "僅檢查",
externalSqliteHome: "外部 SQLite 儲存空間",
inspectionUnavailable: "診斷記錄檢查目前無法使用。",
},
ru: {
inspectionOnly: "Только проверка",
externalSqliteHome: "Внешнее хранилище SQLite",
inspectionUnavailable: "Проверка диагностических журналов недоступна.",
},
ja: {
inspectionOnly: "検査のみ",
externalSqliteHome: "外部 SQLite ストレージ",
inspectionUnavailable: "診断ログの検査を利用できません。",
},
tr: {
inspectionOnly: "Yalnızca inceleme",
externalSqliteHome: "Harici SQLite depolaması",
inspectionUnavailable: "Tanılama günlüğü incelemesi kullanılamıyor.",
},
};

export function logGuardLabel(locale: Locale, key: LogGuardLabelKey): string {
return LABELS[locale][key];
}
64 changes: 64 additions & 0 deletions gui/src/i18n/log-guard-state-labels.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import type { Locale } from "./catalogs";

export type LogGuardSchemaState = "compatible" | "missing" | "unreadable" | "unsupported";

const SCHEMA_LABELS: Record<Locale, Record<LogGuardSchemaState, string>> = {
en: {
compatible: "Compatible",
missing: "Database not found",
unreadable: "Database unavailable",
unsupported: "Unsupported",
},
de: {
compatible: "Kompatibel",
missing: "Datenbank nicht gefunden",
unreadable: "Datenbank nicht lesbar",
unsupported: "Nicht unterstützt",
},
fr: {
compatible: "Compatible",
missing: "Base de données introuvable",
unreadable: "Base de données indisponible",
unsupported: "Non pris en charge",
},
ko: {
compatible: "호환됨",
missing: "데이터베이스 없음",
unreadable: "데이터베이스를 읽을 수 없음",
unsupported: "지원되지 않음",
},
zh: {
compatible: "兼容",
missing: "未找到数据库",
unreadable: "无法读取数据库",
unsupported: "不受支持",
},
"zh-TW": {
compatible: "相容",
missing: "找不到資料庫",
unreadable: "無法讀取資料庫",
unsupported: "不支援",
},
ru: {
compatible: "Совместимо",
missing: "База не найдена",
unreadable: "База недоступна",
unsupported: "Не поддерживается",
},
ja: {
compatible: "互換",
missing: "データベースが見つかりません",
unreadable: "データベースを読み取れません",
unsupported: "未対応",
},
tr: {
compatible: "Uyumlu",
missing: "Veritabanı bulunamadı",
unreadable: "Veritabanı okunamıyor",
unsupported: "Desteklenmiyor",
},
};

export function logGuardSchemaStateLabel(locale: Locale, state: LogGuardSchemaState): string {
return SCHEMA_LABELS[locale][state];
}
Loading
Loading