Compare commits

...

1 Commits

Author SHA1 Message Date
Tomu Hirata fc2ce1aab1 fix(codex-native): prune stale per-session bridge dirs
The codex-native bridge root lives under ~/.omnigent/codex-native (unlike
the tmp-rooted claude/goose/hermes harnesses, which the OS reclaims), and
nothing ever deleted its per-session dirs. Each session's private CODEX_HOME
accumulates the Codex CLI's own state (a multi-tens-of-MB logs_*.sqlite plus
caches), so the root grew without bound — 11 GB across 127 sessions on one
heavy Polly user's machine.

Prune bridge dirs untouched for more than 7 days when a new session prepares
its dir. Liveness uses the newest mtime of the dir and its codex-home, so an
active or recently-resumed session is never pruned. Best-effort and never
raises so cleanup can't break session startup.

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
2026-07-28 14:24:00 +09:00
2 changed files with 127 additions and 0 deletions
+62
View File
@@ -4,15 +4,20 @@ from __future__ import annotations
import hashlib
import json
import logging
import os
import secrets
import shutil
import sys
import tempfile
import time
from dataclasses import dataclass
from pathlib import Path
import tomllib
logger = logging.getLogger(__name__)
CODEX_NATIVE_BRIDGE_ID_LABEL_KEY = "omnigent.codex_native.bridge_id"
CODEX_NATIVE_BRIDGE_DIR_ENV_VAR = "HARNESS_CODEX_NATIVE_BRIDGE_DIR"
CODEX_NATIVE_REQUEST_SESSION_ID_ENV_VAR = "HARNESS_CODEX_NATIVE_REQUEST_SESSION_ID"
@@ -46,6 +51,13 @@ _MCP_CONFIG_FILE = "bridge.json"
_POLICY_HOOK_FILE = "policy_hook.json"
_BRIDGE_ROOT = Path.home() / ".omnigent" / "codex-native"
# Each session's private ``CODEX_HOME`` accumulates the Codex CLI's own state
# (a multi-tens-of-MB ``logs_*.sqlite``, caches, snapshots) and, unlike the
# tmp-rooted native harnesses, this root lives under ``~/.omnigent`` and the OS
# never reclaims it. Prune bridge dirs untouched for longer than this so the
# root does not grow without bound across many sessions.
_BRIDGE_DIR_MAX_AGE_SECONDS = 7 * 24 * 60 * 60
def bridge_root() -> Path:
"""
@@ -125,12 +137,62 @@ def prepare_bridge_dir(bridge_id: str) -> Path:
:param bridge_id: Opaque bridge id, e.g. ``"bridge_abc123"``.
:returns: Prepared absolute bridge directory.
"""
prune_stale_bridge_dirs()
bridge_dir = bridge_dir_for_bridge_id(bridge_id)
bridge_dir.mkdir(mode=0o700, parents=True, exist_ok=True)
os.chmod(bridge_dir, 0o700)
return bridge_dir
def _bridge_dir_last_active(bridge_dir: Path) -> float:
"""
Return the most recent activity time for a bridge directory.
Turn/thread updates rewrite ``state.json`` (bumping the dir's mtime) and
the Codex CLI writes into ``codex-home``, so the newest of the two mtimes
tracks liveness better than the bridge dir's mtime alone.
:param bridge_dir: A ``~/.omnigent/codex-native/<hash>`` directory.
:returns: Unix timestamp of the newest observed activity, or ``0.0`` when
the directory cannot be stat'd.
"""
newest = 0.0
for candidate in (bridge_dir, codex_home_for_bridge_dir(bridge_dir)):
try:
newest = max(newest, candidate.stat().st_mtime)
except OSError:
continue
return newest
def prune_stale_bridge_dirs(*, max_age_seconds: float = _BRIDGE_DIR_MAX_AGE_SECONDS) -> None:
"""
Delete bridge directories untouched for longer than *max_age_seconds*.
Best-effort and never raises: this runs opportunistically before a new
session prepares its own dir, so a permission error or a dir vanishing
mid-sweep (a concurrent launcher) must not break session startup.
:param max_age_seconds: Prune dirs whose last activity is older than this.
:returns: None.
"""
root = bridge_root()
try:
entries = list(root.iterdir())
except OSError:
return
cutoff = time.time() - max_age_seconds
for entry in entries:
if not entry.is_dir():
continue
if _bridge_dir_last_active(entry) >= cutoff:
continue
try:
shutil.rmtree(entry)
except OSError as exc:
logger.warning("could not prune stale codex-native bridge dir %s (%s)", entry, exc)
def write_mcp_bridge_config(bridge_dir: Path) -> None:
"""
Write a minimal ``bridge.json`` so ``serve-mcp`` can boot.
+65
View File
@@ -15,6 +15,7 @@ from omnigent.codex_native_bridge import (
mcp_startup_waiting_detail,
pending_mcp_servers,
prepare_bridge_dir,
prune_stale_bridge_dirs,
read_bridge_startup_error,
read_bridge_state,
read_codex_config_model,
@@ -324,3 +325,67 @@ def test_clear_bridge_state_removes_mcp_startup(bridge_dir: Path) -> None:
clear_bridge_state(bridge_dir)
assert read_mcp_startup(bridge_dir) == {}
def test_prune_stale_bridge_dirs_removes_old_and_keeps_recent(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
"""
``prune_stale_bridge_dirs`` deletes dirs untouched past the age cutoff
while leaving recently-active ones (and non-dir siblings) in place.
"""
import os
import time
root = tmp_path / "codex-native"
monkeypatch.setattr("omnigent.codex_native_bridge._BRIDGE_ROOT", root)
stale = prepare_bridge_dir("stale")
fresh = prepare_bridge_dir("fresh")
stray = root / "loose-file"
stray.write_text("keep me")
old = time.time() - 30 * 24 * 60 * 60
os.utime(stale, (old, old))
stale_home = codex_home_for_bridge_dir(stale)
if stale_home.exists():
os.utime(stale_home, (old, old))
prune_stale_bridge_dirs(max_age_seconds=7 * 24 * 60 * 60)
assert not stale.exists()
assert fresh.exists()
assert stray.exists()
def test_prune_stale_bridge_dirs_no_root_is_noop(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
"""A missing bridge root prunes nothing and does not raise."""
monkeypatch.setattr("omnigent.codex_native_bridge._BRIDGE_ROOT", tmp_path / "absent")
prune_stale_bridge_dirs()
def test_prune_keeps_dir_when_codex_home_recently_written(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
"""
A dir whose own mtime is old but whose ``codex-home`` was written
recently is kept: the Codex CLI's writes count as activity.
"""
import os
import time
root = tmp_path / "codex-native"
monkeypatch.setattr("omnigent.codex_native_bridge._BRIDGE_ROOT", root)
active = prepare_bridge_dir("active")
home = codex_home_for_bridge_dir(active)
home.mkdir(parents=True, exist_ok=True)
old = time.time() - 30 * 24 * 60 * 60
os.utime(active, (old, old))
prune_stale_bridge_dirs(max_age_seconds=7 * 24 * 60 * 60)
assert active.exists()