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
12 changes: 11 additions & 1 deletion plugins/codex-lcm/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,16 @@ Storage defaults to `~/.codex-lcm`. Override storage for hook and MCP operations
CODEX_LCM_HOME=/path/to/lcm-home node bin/codex-lcm health
```

Raw history remains available forever unless `~/.codex-lcm/.env` sets a
positive retention window:

```dotenv
CODEX_LCM_RETENTION_DAYS=90
```

The process environment takes precedence over `.env`. Invalid, zero, negative,
and fractional values disable deletion and appear in `health` and `stats`.

Useful diagnostics and backfill commands:

```sh
Expand All @@ -57,7 +67,7 @@ reasoning metadata, and cumulative token usage from existing transcripts.
and cursor filters. `usage --roots-only` includes descendant session tokens, so
root-task totals account for delegated work. `cleanup` previews a high-signal
FTS rebuild and SQLite compaction; `--apply` performs it while preserving the
raw JSONL source of truth. `context-plan`
configured raw history. `context-plan`
estimates recent-session token pressure and recommends
when to pack LCM context; it does not control Codex compaction. The benchmark
command generates a temporary synthetic long session and verifies old evidence
Expand Down
19 changes: 17 additions & 2 deletions plugins/codex-lcm/docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,11 +51,26 @@ Default home:

```text
~/.codex-lcm/
events.jsonl
events.jsonl # active append target, capped at 64 MiB
segments/
manifest.json
*.jsonl.gz # verified closed segments
index.sqlite
```

`events.jsonl` is the source of truth. `index.sqlite` is derived and can be deleted or rebuilt later.
The active log and manifest-listed segments are the source of truth.
`index.sqlite` is derived and can be deleted or rebuilt later. Closed segments
use gzip level 1. SQLite keeps byte locators for archived events and drops its
duplicate JSON after the segment checksum and event IDs pass verification.

Writable startup cuts an older single-file store over under the raw writer
lock. The hook gets a fresh active log at once, while a one-shot worker migrates
the renamed legacy file in bounded batches. The manifest records progress, so a
stopped worker resumes at the last published byte offset.

The default retention policy is unlimited. A positive
`CODEX_LCM_RETENTION_DAYS` in `~/.codex-lcm/.env` expires closed raw segments,
detailed event rows, and orphan overflow files. Session and summary rows remain.

SQLite tables:

Expand Down
26 changes: 23 additions & 3 deletions plugins/codex-lcm/docs/troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,8 @@ CODEX_LCM_HOME=/private/tmp/codex-lcm-check node bin/codex-lcm health --json
CODEX_LCM_HOME=/private/tmp/codex-lcm-check node bin/codex-lcm stats --json
```

If SQLite cannot open `index.sqlite`, Codex LCM still appends `events.jsonl` and falls back to raw-log scanning.
If SQLite cannot open `index.sqlite`, Codex LCM still appends `events.jsonl` and
falls back to scanning the active log and archived segments.
Use `stats --json` when you need aggregate hook-event, summary-depth,
graph-count, and freshness checks without opening the SQLite database directly.
For compaction hook verification, check `hook_event_counts.PreCompact` and
Expand All @@ -106,8 +107,8 @@ The preview reports the current index size, retained search rows, duplicated
event-text bytes, and the projected high-signal search-row count. It does not
write to SQLite.

`events.jsonl` remains the source of truth, so cleanup never deletes transcript
events. If you also want a point-in-time copy of the derived index, make an
The active log and archived segments remain the source of truth, so cleanup
never deletes retained transcript events. If you also want a point-in-time copy of the derived index, make an
online SQLite backup first:

```sh
Expand All @@ -127,6 +128,25 @@ duplicate event-text column, refreshes deterministic summaries, and runs
first if SQLite reports that the database is busy. Set `CODEX_LCM_HOME` when
cleaning a non-default LCM home.

## Retention and automatic migration

Codex LCM keeps raw history forever by default. To set a limit, create
`~/.codex-lcm/.env` with one positive whole number:

```dotenv
CODEX_LCM_RETENTION_DAYS=90
```

A process-level value overrides the file. Check `config_error` in `health
--json` if deletion does not run. Finite retention removes exact old event
sources after the cutoff, but keeps session and summary records.

An upgraded store migrates without a manual command. `migration_state` reports
`pending`, `complete`, or `error`; `plain_segment_count`,
`compressed_segment_count`, and `archive_bytes` show its progress. Do not remove
`segments/legacy.jsonl` when migration reports an error because it remains the
forensic source for any quarantined record.

## Node Warnings

The implementation uses Node 22's `node:sqlite`. Test and smoke scripts run with `--no-warnings` so experimental runtime warnings do not interfere with MCP stdout parsing.
Expand Down
5 changes: 5 additions & 0 deletions plugins/codex-lcm/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { runLongContextBenchmark, runRetrievalQualityBenchmark } from "./benchma
import { importCodexSessions } from "./codex-import.ts";
import { buildDoctorReport } from "./doctor.ts";
import { runHook } from "./hook.ts";
import { runMaintenanceOnce } from "./maintenance.ts";
import { readStatus } from "./installer.ts";
import { startMcpServer } from "./mcp.ts";
import { createStorage } from "./storage.ts";
Expand All @@ -25,6 +26,10 @@ export async function main(argv: string[]): Promise<void> {
await runHook(rest);
return;
}
if (command === "maintain" && rest[0] === "--once") {
printObjectOrText(runMaintenanceOnce(loadConfig()));
return;
}
if (command === "status") {
printObjectOrText(readStatus({ codexHome: optionValue(rest, "--codex-home"), root: pluginRoot() }));
return;
Expand Down
38 changes: 38 additions & 0 deletions plugins/codex-lcm/src/config.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { fileURLToPath } from "node:url";
Expand All @@ -13,8 +14,13 @@ export type LcmLimits = {
export type LcmConfig = {
home: string;
rawLogPath: string;
segmentsDir: string;
manifestPath: string;
maintenancePath: string;
indexPath: string;
overflowDir: string;
retentionDays?: number;
configError?: string;
limits: LcmLimits;
};

Expand All @@ -32,15 +38,47 @@ function resolveHome(env: Record<string, string | undefined> = process.env): str

export function loadConfig(options: { home?: string; env?: Record<string, string | undefined> } = {}): LcmConfig {
const home = path.resolve(options.home || resolveHome(options.env));
const retention = retentionDays(home, options.env ?? process.env);
const segmentsDir = path.join(home, "segments");
return {
home,
rawLogPath: path.join(home, "events.jsonl"),
segmentsDir,
manifestPath: path.join(segmentsDir, "manifest.json"),
maintenancePath: path.join(home, "maintenance.lock.sqlite"),
indexPath: path.join(home, "index.sqlite"),
overflowDir: path.join(home, "overflow"),
retentionDays: retention.value,
configError: retention.error,
limits: DEFAULT_LIMITS,
};
}

function retentionDays(home: string, env: Record<string, string | undefined>): { value?: number; error?: string } {
const raw = env.CODEX_LCM_RETENTION_DAYS ?? envFileValue(path.join(home, ".env"));
if (raw === undefined) return {};
if (!/^[1-9][0-9]*$/u.test(raw)) {
return { error: "CODEX_LCM_RETENTION_DAYS must be a positive integer." };
}
const value = Number(raw);
if (!Number.isSafeInteger(value)) {
return { error: "CODEX_LCM_RETENTION_DAYS must be a positive safe integer." };
}
return { value };
}

function envFileValue(envPath: string): string | undefined {
if (!fs.existsSync(envPath)) return undefined;
let value: string | undefined;
for (const line of fs.readFileSync(envPath, "utf8").split(/\r?\n/u)) {
if (line.length === 0 || line.startsWith("#")) continue;
if (!line.startsWith("CODEX_LCM_RETENTION_DAYS=")) continue;
if (value !== undefined) return "";
value = line.slice("CODEX_LCM_RETENTION_DAYS=".length);
}
return value;
}

export function pluginRoot(): string {
return path.resolve(fileURLToPath(new URL("../", import.meta.url)));
}
Expand Down
2 changes: 2 additions & 0 deletions plugins/codex-lcm/src/hook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { DEFAULT_LIMITS, loadConfig } from "./config.ts";
import { importCodexSessions } from "./codex-import.ts";
import { normalizeHookEvent } from "./events.ts";
import { resolveGitMetadata } from "./git.ts";
import { queueMaintenance } from "./maintenance.ts";
import { sha256 } from "./redact.ts";
import { createStorage } from "./storage.ts";

Expand Down Expand Up @@ -71,6 +72,7 @@ export async function runHook(args: string[]): Promise<void> {
} finally {
storage.close();
}
queueMaintenance(config);
const output = postCompactRecoveryOutput({
home: config.home,
hookEvent: event.hook_event,
Expand Down
Loading