diff --git a/src/codex/app-server-processes.ts b/src/codex/app-server-processes.ts index a33a1091fd6..4f4e949677f 100644 --- a/src/codex/app-server-processes.ts +++ b/src/codex/app-server-processes.ts @@ -14,7 +14,7 @@ import { resolveTrustedWindowsPowerShellExe, resolveTrustedWindowsTaskkillExe, } from "../lib/windows-elevation"; -import { readCodexCatalogPath } from "./catalog/parsing"; +import { activeCodexModelsCachePath, readCodexCatalogPath } from "./catalog/parsing"; export const STALE_CODEX_APP_SERVER_HINT = "If Codex still shows an older model list, run `ocx sync --restart-codex`: it restarts the long-lived app-server " @@ -141,6 +141,7 @@ export interface CodexAppServerProcessIo { /** Async batch start-time seam used by the request-path Windows collector. */ readStartMsBatchAsync?: (pids: readonly number[]) => Promise>; catalogMtimeMs?: () => number | null; + modelsCacheMtimeMs?: () => number | null; } function execFileTextAsync( @@ -754,6 +755,10 @@ function defaultCatalogMtimeMs(): number | null { } } +function defaultModelsCacheMtimeMs(): number | null { + try { return statSync(activeCodexModelsCachePath()).mtimeMs; } catch { return null; } +} + function codexAppServerProcessesFromSnapshots( snapshots: readonly ProcessSnapshot[], ): CodexAppServerProcess[] { @@ -1254,12 +1259,19 @@ export function afterCatalogWriteHandleAppServers( const observed = computeCodexAppServerCatalogStatus(options.io ?? {}); const starts = new Map(observed.status.processes.map(process => [process.pid, process.startedAtMs])); const catalogMtimeMs = observed.status.catalogMtimeMs; + // Injected catalog clocks are self-contained test/probe observations. Production + // also includes cache-only writes, whose mtime can be newer than the catalog. + const cacheMtimeMs = options.io?.modelsCacheMtimeMs + ? options.io.modelsCacheMtimeMs() + : options.io?.catalogMtimeMs ? null : defaultModelsCacheMtimeMs(); + const writeMtimeMs = catalogMtimeMs === null ? cacheMtimeMs + : cacheMtimeMs === null ? catalogMtimeMs : Math.max(catalogMtimeMs, cacheMtimeMs); const processes = observed.processes.filter(process => !excluded.has(process.pid)); - const staleProcesses = catalogMtimeMs === null + const staleProcesses = writeMtimeMs === null ? [] : processes.filter(process => { const startedAtMs = starts.get(process.pid); - return startedAtMs !== null && startedAtMs !== undefined && startedAtMs <= catalogMtimeMs; + return startedAtMs !== null && startedAtMs !== undefined && startedAtMs <= writeMtimeMs; }); if (staleProcesses.length === 0) return { processes, warned: false, hint }; options.log?.error(formatStaleCodexAppServerWarning(staleProcesses)); diff --git a/structure/runtime.md b/structure/runtime.md index 384b6b394e6..a7a76340716 100644 --- a/structure/runtime.md +++ b/structure/runtime.md @@ -65,7 +65,7 @@ Catalog-derived reasoning-level diagnostics are escaped only at the human-output `ocx system codex-restart` requests a full Codex desktop-app restart and app-server restarts through the management endpoint. `src/cli/capabilities.ts` names that scope in its summary and `--yes` description; `src/cli/system-command.ts` explains the desktop interruption when confirmation is missing and sends no restart request. Human output says the restart was requested, while `--json` preserves the complete server result, including skipped or refused desktop outcomes. After a CLI catalog/cache write, advisory restart guidance compares each running Codex app-server's -start time with the written catalog mtime. It reports only processes proven stale; a fresh or +start time with the newest catalog or models-cache mtime. It reports only processes proven stale; a fresh or unreadable observation does not claim that another restart is required. Explicit `--restart-codex` and `--restart-app-server-only` retain their operator-consent semantics and act on verified matching processes regardless of the advisory freshness result. diff --git a/tests/codex-integration/codex-app-server-processes.test.ts b/tests/codex-integration/codex-app-server-processes.test.ts index 8f170d2f1ea..6dcef6b8afa 100644 --- a/tests/codex-integration/codex-app-server-processes.test.ts +++ b/tests/codex-integration/codex-app-server-processes.test.ts @@ -716,13 +716,13 @@ describe("Codex app-server process matching (#476)", () => { listSnapshots: () => snapshots, readStartMs: pid => pid === 7 ? 1_000 : 3_000, catalogMtimeMs: () => 2_000, + modelsCacheMtimeMs: () => 4_000, }, }); expect(result.warned).toBe(true); - expect(result.processes.map(process => process.pid)).toEqual([7]); + expect(result.processes.map(process => process.pid)).toEqual([7, 8]); expect(errors).toHaveLength(1); - expect(errors[0]).toContain("PID: 7"); - expect(errors[0]).not.toContain("8"); + expect(errors[0]).toContain("PIDs: 7, 8"); }); test("afterCatalogWriteHandleAppServers stays quiet for fresh and unknown observations", () => {