From 016a93e725f792974121475d6c46c3275698dc44 Mon Sep 17 00:00:00 2001 From: luvs01 Date: Thu, 13 Aug 2026 09:37:17 +0900 Subject: [PATCH] fix(service): bind lifecycle to SQLite home --- src/service.ts | 15 ++++++++++++++- tests/service.test.ts | 21 +++++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/src/service.ts b/src/service.ts index 668f16520..3f1b5acf7 100644 --- a/src/service.ts +++ b/src/service.ts @@ -15,6 +15,7 @@ import { loadConfig } from "./config"; import { restoreNativeCodex, restoreNativeCodexAsync } from "./codex/inject"; import { stripGrokConfig } from "./grok/inject"; import { isWslRuntime, resolveCodexHomeDir, type CodexHomeDeps } from "./codex/home"; +import { resolveCodexSqliteHome } from "./codex/paths"; import { BUN_RUNTIME_PATH_ENV, BUN_RUNTIME_SOURCE_ENV, durableBunRuntime } from "./lib/bun-runtime"; import type { BunRuntimeSource } from "./lib/bun-runtime"; import { isProcessAlive, stopProxy } from "./lib/process-control"; @@ -130,6 +131,8 @@ export interface ServiceInstallState { version: 1 | 2; codexHome: string; opencodexHome: string; + /** Effective Codex SQLite home used by this service's history integration. */ + codexSqliteHome?: string; /** Baked at install; lets status flag paths gone stale after npm prefix/nvm moves. */ bunPath?: string; cliPath?: string; @@ -145,7 +148,7 @@ export function parseServiceInstallState(value: unknown): ServiceInstallState | if (state.version !== 1 && state.version !== 2) return null; if (typeof state.codexHome !== "string" || state.codexHome.length === 0) return null; if (typeof state.opencodexHome !== "string" || state.opencodexHome.length === 0) return null; - for (const key of ["bunPath", "cliPath", "winswVersion", "winswSha256"] as const) { + for (const key of ["codexSqliteHome", "bunPath", "cliPath", "winswVersion", "winswSha256"] as const) { if (state[key] !== undefined && (typeof state[key] !== "string" || state[key].length === 0)) return null; } if (state.version === 1) { @@ -162,6 +165,7 @@ function writeServiceInstallState(backend: ServiceBackend = "scheduler"): void { version: 2, codexHome: currentCodexHome(), opencodexHome: currentOpenCodexHome(), + codexSqliteHome: resolveCodexSqliteHome({ codexHome: currentCodexHome() }), bunPath: bun, cliPath: cli, backend, @@ -315,6 +319,15 @@ export function assertServiceEnvironmentMatchesInstall(): void { "Run the service command from the same OpenCodex home so service state and secrets match.", ); } + if (state.codexSqliteHome !== undefined) { + const actualCodexSqliteHome = resolveCodexSqliteHome({ codexHome: actualCodexHome }); + if (normalizePathForCompare(state.codexSqliteHome) !== normalizePathForCompare(actualCodexSqliteHome)) { + throw new ServiceOwnershipError( + `Service was installed with Codex SQLite home=${state.codexSqliteHome}, but the current Codex SQLite home=${actualCodexSqliteHome}. ` + + "Run the service command with the same sqlite_home configuration and CODEX_SQLITE_HOME so native Codex history restore updates the correct database.", + ); + } + } } diff --git a/tests/service.test.ts b/tests/service.test.ts index 161304244..7e3f7ce67 100644 --- a/tests/service.test.ts +++ b/tests/service.test.ts @@ -14,6 +14,7 @@ import type { OcxConfig } from "../src/types"; const TEST_DIR = join(import.meta.dir, ".tmp-service-test"); const previousOpenCodexHome = process.env.OPENCODEX_HOME; const previousCodexHome = process.env.CODEX_HOME; +const previousCodexSqliteHome = process.env.CODEX_SQLITE_HOME; const previousApiAuthToken = process.env.OPENCODEX_API_AUTH_TOKEN; afterEach(() => { @@ -21,6 +22,8 @@ afterEach(() => { else process.env.OPENCODEX_HOME = previousOpenCodexHome; if (previousCodexHome === undefined) delete process.env.CODEX_HOME; else process.env.CODEX_HOME = previousCodexHome; + if (previousCodexSqliteHome === undefined) delete process.env.CODEX_SQLITE_HOME; + else process.env.CODEX_SQLITE_HOME = previousCodexSqliteHome; if (previousApiAuthToken === undefined) delete process.env.OPENCODEX_API_AUTH_TOKEN; else process.env.OPENCODEX_API_AUTH_TOKEN = previousApiAuthToken; if (existsSync(TEST_DIR)) rmSync(TEST_DIR, { recursive: true }); @@ -207,6 +210,23 @@ describe("service install auth preflight", () => { expect(() => assertServiceEnvironmentMatchesInstall()).toThrow("Service was installed with CODEX_HOME"); }); + + test("rejects restore operations against a different Codex SQLite home than service install", () => { + if (existsSync(TEST_DIR)) rmSync(TEST_DIR, { recursive: true }); + mkdirSync(TEST_DIR, { recursive: true }); + process.env.OPENCODEX_HOME = TEST_DIR; + process.env.CODEX_HOME = join(TEST_DIR, "codex-home"); + delete process.env.CODEX_SQLITE_HOME; + writeFileSync(join(TEST_DIR, "service-state.json"), JSON.stringify({ + version: 2, + codexHome: process.env.CODEX_HOME, + codexSqliteHome: join(TEST_DIR, "installed-sqlite-home"), + opencodexHome: TEST_DIR, + backend: "scheduler", + }) + "\n"); + + expect(() => assertServiceEnvironmentMatchesInstall()).toThrow("Codex SQLite home"); + }); }); describe("Windows service task", () => { @@ -1138,6 +1158,7 @@ describe("service diagnostics", () => { expect(parseServiceInstallState({ ...valid, backend: undefined })).toBeNull(); expect(parseServiceInstallState({ ...valid, version: 1, backend: "scheduler" })).toBeNull(); expect(parseServiceInstallState({ ...valid, version: 1, backend: undefined })?.version).toBe(1); + expect(parseServiceInstallState({ ...valid, codexSqliteHome: "" })).toBeNull(); }); test("status summary exposes the service log path", () => {