From ae6f3e1f3d090d00abbf4a24ed7dc0b7725a27a3 Mon Sep 17 00:00:00 2001 From: Ingwannu Date: Sat, 20 Jun 2026 14:56:22 +0000 Subject: [PATCH 1/2] Fix stale Codex shim repair --- src/codex-shim.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/codex-shim.ts b/src/codex-shim.ts index 1e8d6601553..0a545addf48 100644 --- a/src/codex-shim.ts +++ b/src/codex-shim.ts @@ -127,7 +127,7 @@ export function installCodexShim(): { installed: boolean; message: string } { return { installed: false, message: `Codex autostart shim already installed at ${existing.wrapperPath}.` }; } if (existing && existsSync(existing.backupPath) && (!existsSync(existing.wrapperPath) || !isShim(existing.wrapperPath))) { - if (existsSync(existing.wrapperPath)) renameSync(existing.wrapperPath, existing.backupPath); + if (existsSync(existing.wrapperPath)) unlinkSync(existing.wrapperPath); writeShim(existing.wrapperPath, existing.backupPath); writeState({ ...existing, platform: process.platform }); return { From 34d4b74f07a380f016fc72e1322f2511149e28d0 Mon Sep 17 00:00:00 2001 From: Ingwannu Date: Sun, 21 Jun 2026 01:08:07 +0000 Subject: [PATCH 2/2] Sync Codex resume providers during injection --- src/cli.ts | 6 ++ src/codex-history-provider.ts | 86 ++++++++++++++++++++++++++++ src/codex-inject.ts | 10 +++- src/codex-shim.ts | 2 + tests/codex-history-provider.test.ts | 71 +++++++++++++++++++++++ tests/codex-shim.test.ts | 2 + 6 files changed, 176 insertions(+), 1 deletion(-) create mode 100644 src/codex-history-provider.ts create mode 100644 tests/codex-history-provider.test.ts diff --git a/src/cli.ts b/src/cli.ts index ca1234a2d2f..b9b0ce8752b 100755 --- a/src/cli.ts +++ b/src/cli.ts @@ -20,6 +20,7 @@ Usage: ocx service Run as a background service (install|start|stop|status|uninstall) ocx codex-shim Auto-start proxy when \`codex\` launches (install|status|uninstall) ocx sync Fetch models from providers and inject into Codex config + ocx sync-cache Refresh Codex's model cache from the active catalog ocx status Check proxy server status ocx login OAuth login (xai) — opens browser, stores token in ~/.opencodex/auth.json ocx logout Remove a stored OAuth login @@ -201,6 +202,11 @@ switch (command) { await syncModelsToCodex(); break; } + case "sync-cache": { + const { invalidateCodexModelsCache } = await import("./codex-catalog"); + invalidateCodexModelsCache(); + break; + } case "gui": { const cfg = await import("./config"); const config = cfg.loadConfig(); diff --git a/src/codex-history-provider.ts b/src/codex-history-provider.ts new file mode 100644 index 00000000000..2540394d30f --- /dev/null +++ b/src/codex-history-provider.ts @@ -0,0 +1,86 @@ +import { existsSync, readFileSync, statSync, utimesSync, writeFileSync } from "node:fs"; +import { join } from "node:path"; +import { Database } from "bun:sqlite"; +import { CODEX_HOME } from "./codex-paths"; + +const STATE_DB_PATH = join(CODEX_HOME, "state_5.sqlite"); +const RESUMABLE_SOURCES = ["cli", "vscode"] as const; + +type CodexHistoryProvider = "openai" | "opencodex"; + +interface ThreadRow { + id: string; + rollout_path: string; +} + +function updateSessionMetaProvider(path: string, provider: CodexHistoryProvider): boolean { + if (!path || !existsSync(path)) return false; + const stat = statSync(path); + const raw = readFileSync(path, "utf8"); + const newline = raw.indexOf("\n"); + const firstLine = newline === -1 ? raw : raw.slice(0, newline); + const rest = newline === -1 ? "" : raw.slice(newline); + + let parsed: unknown; + try { + parsed = JSON.parse(firstLine); + } catch { + return false; + } + + if (!parsed || typeof parsed !== "object") return false; + const record = parsed as { type?: unknown; payload?: { model_provider?: unknown } }; + if (record.type !== "session_meta" || !record.payload || typeof record.payload !== "object") return false; + if (record.payload.model_provider === provider) return false; + + record.payload.model_provider = provider; + writeFileSync(path, `${JSON.stringify(record)}${rest}`, "utf8"); + utimesSync(path, stat.atime, stat.mtime); + return true; +} + +export function syncCodexHistoryProvider(provider: CodexHistoryProvider, stateDbPath = STATE_DB_PATH): { rows: number; files: number } { + if (!existsSync(stateDbPath)) return { rows: 0, files: 0 }; + const from = provider === "opencodex" ? "openai" : "opencodex"; + const db = new Database(stateDbPath); + try { + const placeholders = RESUMABLE_SOURCES.map(() => "?").join(","); + const rows = db + .query(` + SELECT id, rollout_path + FROM threads + WHERE model_provider = ? + AND source IN (${placeholders}) + `) + .all(from, ...RESUMABLE_SOURCES); + + let files = 0; + for (const row of rows) { + try { + if (updateSessionMetaProvider(row.rollout_path, provider)) files++; + } catch { + /* best-effort; keep DB migration moving even if one old rollout is malformed */ + } + } + + const update = db.transaction(() => { + db.query(` + UPDATE threads + SET has_user_event = 1 + WHERE source IN (${placeholders}) + AND trim(coalesce(first_user_message, '')) != '' + `).run(...RESUMABLE_SOURCES); + db.query(` + UPDATE threads + SET model_provider = ? + WHERE model_provider = ? + AND source IN (${placeholders}) + `).run(provider, from, ...RESUMABLE_SOURCES); + }); + update(); + + return { rows: rows.length, files }; + } finally { + db.close(); + } +} diff --git a/src/codex-inject.ts b/src/codex-inject.ts index 84f33531fc8..66d528f1472 100644 --- a/src/codex-inject.ts +++ b/src/codex-inject.ts @@ -1,6 +1,7 @@ import { existsSync, readFileSync, unlinkSync, writeFileSync } from "node:fs"; import { atomicWriteFile, websocketsEnabled } from "./config"; import { restoreCodexCatalog } from "./codex-catalog"; +import { syncCodexHistoryProvider } from "./codex-history-provider"; import { CODEX_CONFIG_PATH, CODEX_PROFILE_PATH, DEFAULT_CATALOG_PATH, parseTomlString, readRootTomlString, resolveCodexConfigPath, tomlString } from "./codex-paths"; import type { OcxConfig } from "./types"; @@ -228,14 +229,19 @@ export async function injectCodexConfig(port: number, config?: OcxConfig, option writeFileSync(CODEX_CONFIG_PATH, content, "utf-8"); writeFileSync(CODEX_PROFILE_PATH, buildProfileFile(port, catalogPath), "utf-8"); + const history = syncCodexHistoryProvider("opencodex"); const catalogMessage = catalogPath ? ` Codex model catalog: ${catalogPath}\n` : ` Codex model catalog not injected because no opencodex catalog file exists yet.\n`; + const historyMessage = history.rows > 0 + ? ` Codex resume history: ${history.rows} thread(s) mapped to opencodex.\n` + : ""; return { success: true, message: `Injected opencodex as default provider into Codex config.\n` + catalogMessage + + historyMessage + ` All models now route through opencodex proxy (like OpenRouter).\n` + ` OpenAI models (gpt-5.5, etc.) are passed through to OpenAI.\n` + ` Custom models route to their configured providers.\n` + @@ -311,10 +317,12 @@ export function removeCodexConfig(): { success: boolean; message: string } { export function restoreNativeCodex(): { success: boolean; message: string } { const cfg = removeCodexConfig(); const cat = restoreCodexCatalog(); + const history = syncCodexHistoryProvider("openai"); const msg = cat.removed > 0 ? `${cfg.message} Catalog restored to ${cat.kept} native model(s) (dropped ${cat.removed} proxy-routed).` : cfg.message; - return { success: cfg.success, message: msg }; + const historyMsg = history.rows > 0 ? ` Resume history restored to openai (${history.rows} thread(s)).` : ""; + return { success: cfg.success, message: `${msg}${historyMsg}` }; } export function getCodexConfigPath(): string { diff --git a/src/codex-shim.ts b/src/codex-shim.ts index 0a545addf48..35ed960d406 100644 --- a/src/codex-shim.ts +++ b/src/codex-shim.ts @@ -66,6 +66,7 @@ if [ -z "$OCX_SHIM_BYPASS" ]; then i=$((i + 1)) done fi + "${bunPath}" "${cliPath}" sync-cache >/dev/null 2>&1 || true fi exec "${realCodexPath}" "$@" `; @@ -85,6 +86,7 @@ for /l %%i in (1,1,50) do (\r powershell -NoProfile -Command "Start-Sleep -Milliseconds 100" >nul 2>nul\r )\r :run_codex\r +"${bunPath}" "${cliPath}" sync-cache >nul 2>nul\r "${realCodexPath}" %*\r `; } diff --git a/tests/codex-history-provider.test.ts b/tests/codex-history-provider.test.ts new file mode 100644 index 00000000000..28086f48a09 --- /dev/null +++ b/tests/codex-history-provider.test.ts @@ -0,0 +1,71 @@ +import { mkdirSync, readFileSync, statSync, utimesSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { Database } from "bun:sqlite"; +import { describe, expect, test } from "bun:test"; +import { syncCodexHistoryProvider } from "../src/codex-history-provider"; + +function makeFixture() { + const dir = join(tmpdir(), `ocx-history-${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}`); + mkdirSync(dir, { recursive: true }); + const rollout = join(dir, "rollout.jsonl"); + writeFileSync(rollout, [ + JSON.stringify({ + type: "session_meta", + payload: { id: "thread-1", model_provider: "openai", cwd: dir }, + }), + JSON.stringify({ type: "event_msg", timestamp: "2026-01-01T00:00:00.000Z", payload: { message: "x" } }), + ].join("\n") + "\n"); + const mtime = new Date("2026-01-02T03:04:05.000Z"); + utimesSync(rollout, mtime, mtime); + + const dbPath = join(dir, "state_5.sqlite"); + const db = new Database(dbPath); + db.run(` + CREATE TABLE threads ( + id TEXT PRIMARY KEY, + rollout_path TEXT NOT NULL, + model_provider TEXT NOT NULL, + source TEXT NOT NULL, + first_user_message TEXT NOT NULL, + has_user_event INTEGER NOT NULL DEFAULT 0 + ) + `); + db.run(` + INSERT INTO threads (id, rollout_path, model_provider, source, first_user_message, has_user_event) + VALUES ('thread-1', ?, 'openai', 'vscode', 'hello', 0) + `, rollout); + db.close(); + return { dbPath, rollout, mtime }; +} + +describe("Codex history provider sync", () => { + test("maps resumable Codex threads to opencodex without touching file mtime", () => { + const { dbPath, rollout, mtime } = makeFixture(); + + const result = syncCodexHistoryProvider("opencodex", dbPath); + + expect(result).toEqual({ rows: 1, files: 1 }); + const db = new Database(dbPath); + expect(db.query("SELECT model_provider FROM threads WHERE id = 'thread-1'").get()).toEqual({ model_provider: "opencodex" }); + expect(db.query("SELECT has_user_event FROM threads WHERE id = 'thread-1'").get()).toEqual({ has_user_event: 1 }); + db.close(); + const firstLine = readFileSync(rollout, "utf8").split("\n")[0]; + expect(JSON.parse(firstLine).payload.model_provider).toBe("opencodex"); + expect(statSync(rollout).mtime.getTime()).toBe(mtime.getTime()); + }); + + test("maps resumable Codex threads back to openai", () => { + const { dbPath, rollout } = makeFixture(); + syncCodexHistoryProvider("opencodex", dbPath); + + const result = syncCodexHistoryProvider("openai", dbPath); + + expect(result).toEqual({ rows: 1, files: 1 }); + const db = new Database(dbPath); + expect(db.query("SELECT model_provider FROM threads WHERE id = 'thread-1'").get()).toEqual({ model_provider: "openai" }); + db.close(); + const firstLine = readFileSync(rollout, "utf8").split("\n")[0]; + expect(JSON.parse(firstLine).payload.model_provider).toBe("openai"); + }); +}); diff --git a/tests/codex-shim.test.ts b/tests/codex-shim.test.ts index 398419fcc93..c1c78d23315 100644 --- a/tests/codex-shim.test.ts +++ b/tests/codex-shim.test.ts @@ -14,6 +14,7 @@ describe("Codex autostart shim", () => { expect(script).toContain("status"); expect(script).toContain("OCX_SERVICE=1"); expect(script).toContain("start"); + expect(script).toContain("sync-cache"); expect(script).toContain('exec "/usr/local/bin/codex-real" "$@"'); }); @@ -23,6 +24,7 @@ describe("Codex autostart shim", () => { expect(script).toContain(SHIM_MARKER); expect(script).toContain("findstr"); expect(script).toContain("OCX_SERVICE=1"); + expect(script).toContain("sync-cache"); expect(script).toContain('"C:\\Tools\\codex-real.exe" %*'); });