From 36550e6af4cd9cd73b65943b4e8884c91cc6a801 Mon Sep 17 00:00:00 2001 From: Jonathan Li <47408717+jonathanli12@users.noreply.github.com> Date: Thu, 13 Aug 2026 00:37:59 -0700 Subject: [PATCH 1/2] fix(responses): scope continuation replay to client task --- src/responses/spill-store.ts | 6 +++- src/responses/state.ts | 53 ++++++++++++++++++++++++++-- src/server/responses/core.ts | 35 ++++++++++++------- tests/responses-state.test.ts | 66 +++++++++++++++++++++++++++++++++-- 4 files changed, 142 insertions(+), 18 deletions(-) diff --git a/src/responses/spill-store.ts b/src/responses/spill-store.ts index a35f08e5db..c338155730 100644 --- a/src/responses/spill-store.ts +++ b/src/responses/spill-store.ts @@ -34,6 +34,7 @@ export interface ResponseSpillPayload { version: 1; responseId: string; createdAt: number; + clientThreadId?: string; items: unknown[]; providers?: OcxProviderContinuationState; } @@ -260,9 +261,11 @@ function validPayload(value: unknown, responseId: string): value is ResponseSpil if (!value || typeof value !== "object" || Array.isArray(value)) return false; const payload = value as Record; const keys = Object.keys(payload); - if (keys.some(key => !["version", "responseId", "createdAt", "items", "providers"].includes(key))) return false; + if (keys.some(key => !["version", "responseId", "createdAt", "clientThreadId", "items", "providers"].includes(key))) return false; if (payload.version !== 1 || payload.responseId !== responseId) return false; if (typeof payload.createdAt !== "number" || !Number.isFinite(payload.createdAt)) return false; + if (payload.clientThreadId !== undefined + && (typeof payload.clientThreadId !== "string" || payload.clientThreadId.trim().length === 0)) return false; if (!Array.isArray(payload.items)) return false; if (payload.providers !== undefined) { if (!payload.providers || typeof payload.providers !== "object" || Array.isArray(payload.providers)) return false; @@ -284,6 +287,7 @@ export function writeResponseSpillDurably( version: 1, responseId, createdAt: state.createdAt, + ...(state.clientThreadId ? { clientThreadId: state.clientThreadId } : {}), items: state.items, ...(state.providers ? { providers: state.providers } : {}), }; diff --git a/src/responses/state.ts b/src/responses/state.ts index a0ef3eb9dc..cd7dc11d90 100644 --- a/src/responses/state.ts +++ b/src/responses/state.ts @@ -37,6 +37,7 @@ const MAX_SNAPSHOT_REWRITE_ATTEMPTS = 4; interface ResidentResponseState { kind: "resident"; createdAt: number; + clientThreadId?: string; items: unknown[]; providers?: OcxProviderContinuationState; sizeBytes: number; @@ -45,6 +46,7 @@ interface ResidentResponseState { interface SpilledResponseState { kind: "spill"; createdAt: number; + clientThreadId?: string; providers?: OcxProviderContinuationState; spill: ResponseSpillRef; sizeBytes: number; @@ -64,7 +66,9 @@ export type PreviousResponseReplayFailure = { reason: "spill_missing" | "spill_corrupt" | "spill_failed" | "spill_too_large"; }; +// OCX_LOCAL_PATCH_RESPONSE_STATE_TASK_SCOPE: prevent cross-task continuation replay. const states = new Map(); +const replayScopeMismatches = new WeakSet(); let storedResponseBytes = 0; let residentResponseBytes = 0; let oldestResidentId: string | undefined; @@ -80,6 +84,7 @@ const spillCounters = { writes: 0, writeFailures: 0, readFailures: 0 }; * snapshot files refused before parse. */ const admissionCounters = { directSpills: 0, oversizedDrops: 0, snapshotOversizedRefusals: 0 }; +let replayScopeMismatchDrops = 0; /** Test-only: admission-boundary counters (proves the new paths fire). */ export function responseAdmissionCountersForTests(): Readonly { @@ -124,6 +129,7 @@ function measureResidentEntry(id: string, entry: ResidentInput): ResidentRespons const sizeBytes = serializedBytes({ responseId: id, createdAt: entry.createdAt, + ...(entry.clientThreadId ? { clientThreadId: entry.clientThreadId } : {}), items: entry.items, ...(entry.providers ? { providers: entry.providers } : {}), }); @@ -224,6 +230,7 @@ function swapResidentForSpill(id: string, expected: ResidentResponseState, ref: const base: Omit = { kind: "spill", createdAt: expected.createdAt, + ...(expected.clientThreadId ? { clientThreadId: expected.clientThreadId } : {}), ...(expected.providers ? { providers: expected.providers } : {}), spill: ref, }; @@ -244,12 +251,14 @@ function replaceSpillEntryAtomically( try { const ref = writeResponseSpillDurably(id, { createdAt: candidate.createdAt, + ...(candidate.clientThreadId ? { clientThreadId: candidate.clientThreadId } : {}), items: candidate.items, ...(candidate.providers ? { providers: candidate.providers } : {}), }); const base: Omit = { kind: "spill", createdAt: candidate.createdAt, + ...(candidate.clientThreadId ? { clientThreadId: candidate.clientThreadId } : {}), ...(candidate.providers ? { providers: candidate.providers } : {}), spill: ref, }; @@ -322,6 +331,7 @@ function admitOversizedCandidate( try { const ref = writeResponseSpillDurably(id, { createdAt: candidate.createdAt, + ...(candidate.clientThreadId ? { clientThreadId: candidate.clientThreadId } : {}), items: candidate.items, ...(candidate.providers ? { providers: candidate.providers } : {}), }); @@ -337,6 +347,7 @@ function admitOversizedCandidate( const base: Omit = { kind: "spill", createdAt: candidate.createdAt, + ...(candidate.clientThreadId ? { clientThreadId: candidate.clientThreadId } : {}), ...(candidate.providers ? { providers: candidate.providers } : {}), spill: ref, }; @@ -385,6 +396,7 @@ function snapshotPath(): string { interface LegacySnapshotState { createdAt?: unknown; + clientThreadId?: unknown; items?: unknown; providers?: OcxProviderContinuationState; conversationId?: unknown; @@ -405,11 +417,15 @@ function loadSnapshotEntry(id: string, value: unknown): void { if (!value || typeof value !== "object" || Array.isArray(value)) return; const rec = value as LegacySnapshotState & { kind?: unknown; spill?: unknown }; if (typeof rec.createdAt !== "number" || !Number.isFinite(rec.createdAt)) return; + const clientThreadId = typeof rec.clientThreadId === "string" && rec.clientThreadId.trim().length > 0 + ? rec.clientThreadId.trim() + : undefined; if (rec.kind === "spill") { if (!isSpillRef(rec.spill)) return; const base: Omit = { kind: "spill", createdAt: rec.createdAt, + ...(clientThreadId ? { clientThreadId } : {}), ...(rec.providers ? { providers: rec.providers } : {}), spill: rec.spill, }; @@ -434,6 +450,7 @@ function loadSnapshotEntry(id: string, value: unknown): void { : undefined); const resident = measureResidentEntry(id, { createdAt: rec.createdAt, + ...(clientThreadId ? { clientThreadId } : {}), items: rec.items, ...(providers ? { providers } : {}), }); @@ -747,6 +764,7 @@ function pruneResponses(at = now()): void { try { const ref = writeResponseSpillDurably(oldestId, { createdAt: entry.createdAt, + ...(entry.clientThreadId ? { clientThreadId: entry.clientThreadId } : {}), items: entry.items, ...(entry.providers ? { providers: entry.providers } : {}), }); @@ -788,6 +806,7 @@ export function evictOldestResponseContinuationForBudget(): number { try { const ref = writeResponseSpillDurably(id, { createdAt: entry.createdAt, + ...(entry.clientThreadId ? { clientThreadId: entry.clientThreadId } : {}), items: entry.items, ...(entry.providers ? { providers: entry.providers } : {}), }); @@ -828,6 +847,7 @@ function materializeEntry( } const state = measureResidentEntry(id, { createdAt: result.payload.createdAt, + ...(result.payload.clientThreadId ? { clientThreadId: result.payload.clientThreadId } : {}), items: result.payload.items, ...(result.payload.providers ? { providers: result.payload.providers } : {}), }); @@ -840,7 +860,16 @@ function materializeEntry( return { ok: true, state }; } -export function expandPreviousResponseInput(body: unknown): unknown { +function normalizedClientThreadId(value: unknown): string | undefined { + return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined; +} + +function withoutPreviousResponseId(request: Record): Record { + const { previous_response_id: _previousResponseId, ...freshRequest } = request; + return freshRequest; +} + +export function expandPreviousResponseInput(body: unknown, clientThreadId?: string): unknown { if (!body || typeof body !== "object" || Array.isArray(body)) return body; const request = body as Record; const previousId = typeof request.previous_response_id === "string" ? request.previous_response_id : undefined; @@ -854,6 +883,16 @@ export function expandPreviousResponseInput(body: unknown): unknown { replayFailures.set(request, materialized.failure); return body; } + const requestThreadId = normalizedClientThreadId(clientThreadId); + const storedThreadId = normalizedClientThreadId(materialized.state.clientThreadId); + // A Codex task must never inherit another task's continuation, nor a legacy unscoped entry. + // Unscoped callers retain backward-compatible replay only with other unscoped entries. + if (requestThreadId !== storedThreadId) { + const freshRequest = withoutPreviousResponseId(request); + replayScopeMismatches.add(freshRequest); + replayScopeMismatchDrops += 1; + return freshRequest; + } const expanded = { ...request, input: [...materialized.state.items, ...inputItems(request.input)], @@ -873,6 +912,11 @@ export function previousResponseReplayPrefixLength(body: unknown): number { return replayedInputPrefixLengths.get(body) ?? 0; } +/** True when a stale or foreign previous_response_id was removed from this exact request body. */ +export function previousResponseScopeMismatch(body: unknown): boolean { + return !!body && typeof body === "object" && replayScopeMismatches.has(body as object); +} + export function previousResponseConversationId(responseId: string | undefined): string | undefined { return previousResponseProviderState(responseId)?.cursor?.conversationId; } @@ -898,6 +942,7 @@ export interface ResponseStateMetrics { spillWrites: number; spillWriteFailures: number; spillReadFailures: number; + replayScopeMismatchDrops: number; } /** @@ -940,6 +985,7 @@ export function responseStateMetrics(): ResponseStateMetrics { spillWrites: spillCounters.writes, spillWriteFailures: spillCounters.writeFailures, spillReadFailures: spillCounters.readFailures, + replayScopeMismatchDrops, }; } @@ -972,7 +1018,7 @@ export function rememberResponseState( requestBody: unknown, response: { id?: unknown; output?: unknown; status?: unknown; incomplete_details?: unknown }, providerState?: OcxProviderContinuationState | string, - opts?: { force?: boolean }, + opts?: { force?: boolean; clientThreadId?: string }, ): void { if (!requestBody || typeof requestBody !== "object" || Array.isArray(requestBody)) return; const request = requestBody as Record; @@ -998,8 +1044,10 @@ export function rememberResponseState( return !!item && typeof item === "object" && (item as { type?: unknown }).type === "function_call"; }); } + const clientThreadId = normalizedClientThreadId(opts?.clientThreadId); setResidentEntry(response.id, { createdAt: now(), + ...(clientThreadId ? { clientThreadId } : {}), items: [...inputItems(request.input), ...response.output], // Always preserve the Cursor conversation id so the next tool-result turn can continue the SAME // Cursor conversation (multi-turn continuation). Separately track whether Cursor's own @@ -1045,6 +1093,7 @@ export function clearResponseStateMemoryForTests(): void { spillCounters.writes = 0; spillCounters.writeFailures = 0; spillCounters.readFailures = 0; + replayScopeMismatchDrops = 0; persistAttemptHookForTests = null; loaded = false; } diff --git a/src/server/responses/core.ts b/src/server/responses/core.ts index 7fd2b87b03..437a999176 100644 --- a/src/server/responses/core.ts +++ b/src/server/responses/core.ts @@ -22,6 +22,7 @@ import { markBodyNonPersistable, previousResponseProviderState, previousResponseReplayFailure, + previousResponseScopeMismatch, rememberResponseState, } from "../../responses/state"; import { @@ -1503,8 +1504,12 @@ async function handleResponsesInner( let unreadableEncryptedAgentTask = hasUnreadableEncryptedAgentTask( (body as { input?: unknown } | undefined)?.input, ); + const inboundClientThreadId = req.headers.get("x-codex-parent-thread-id")?.trim() || undefined; const originalBody = body; - body = expandPreviousResponseInput(body); + body = expandPreviousResponseInput(body, inboundClientThreadId); + if (previousResponseScopeMismatch(body)) { + console.warn("[opencodex] dropped a previous_response_id with a mismatched client task scope; continuing fresh"); + } if (previousResponseReplayFailure(body)) { return formatErrorResponse( 400, @@ -1512,7 +1517,8 @@ async function handleResponsesInner( "Continuation state is unavailable or corrupt; resend the full conversation without previous_response_id.", ); } - const previousResponseInputExpanded = body !== originalBody; + const previousResponseInputExpanded = body !== originalBody + && typeof (body as { previous_response_id?: unknown }).previous_response_id === "string"; // Spawn-message compatibility (both directions): agent_message task payloads ride in // encrypted_content slots as plaintext. Rewrite them to input_text on the RAW body BEFORE @@ -1529,7 +1535,7 @@ async function handleResponsesInner( ); } - let parsed; + let parsed: OcxParsedRequest; let toolBridgeMaps: ReturnType; try { parsed = parseRequest(body); @@ -1537,10 +1543,9 @@ async function handleResponsesInner( if (previousResponseInputExpanded) parsed._previousResponseInputExpanded = true; parsed._providerContinuation = previousResponseProviderState(parsed.previousResponseId); parsed._cursorConversationId = parsed._providerContinuation?.cursor?.conversationId; - const clientThreadId = req.headers.get("x-codex-parent-thread-id")?.trim(); - if (clientThreadId) { - parsed._clientThreadId = clientThreadId; - parsed._reasoningReplayScope = { clientThreadId }; + if (inboundClientThreadId) { + parsed._clientThreadId = inboundClientThreadId; + parsed._reasoningReplayScope = { clientThreadId: inboundClientThreadId }; } } catch (err) { if (isTranslatorBudgetExceededError(err)) { @@ -1550,6 +1555,10 @@ async function handleResponsesInner( } return formatErrorResponse(400, "invalid_request_error", err instanceof Error ? err.message : String(err)); } + const responseStateOptions = (force = false): { force?: boolean; clientThreadId?: string } => ({ + ...(force ? { force: true } : {}), + ...(parsed._clientThreadId ? { clientThreadId: parsed._clientThreadId } : {}), + }); // Prefer a pre-populated id (routed Claude) over Responses headers that may be // absent or synthetically injected (session_id from prompt_cache_key). if (!logCtx.conversationId) { @@ -2137,7 +2146,7 @@ async function handleResponsesInner( && (!parsed.previousResponseId || parsed._previousResponseInputExpanded === true); const rememberPassthroughResponse = passthroughRecordEligible ? (response: { id?: unknown; output?: unknown; status?: unknown }) => - rememberResponseState(parsed._rawBody, response, undefined, { force: true }) + rememberResponseState(parsed._rawBody, response, undefined, responseStateOptions(true)) : undefined; if (parsed.previousResponseId && !parsed._previousResponseInputExpanded) { console.warn( @@ -2962,7 +2971,7 @@ async function handleResponsesInner( parsed._rawBody, response, continuationStateForResponse(providerState), - adapterNeedsForcedContinuation(adapter.name) ? { force: true } : undefined, + responseStateOptions(adapterNeedsForcedContinuation(adapter.name)), ), }); if (imgResponse.body) { @@ -3118,7 +3127,7 @@ async function handleResponsesInner( parsed._rawBody, response, continuationStateForResponse(providerState), - adapterNeedsForcedContinuation(adapter.name) ? { force: true } : undefined, + responseStateOptions(adapterNeedsForcedContinuation(adapter.name)), ), }), }, @@ -3164,7 +3173,7 @@ async function handleResponsesInner( parsed._rawBody, json, continuationStateForResponse(providerState), - adapterNeedsForcedContinuation(adapter.name) ? { force: true } : undefined, + responseStateOptions(adapterNeedsForcedContinuation(adapter.name)), ); } return new Response(JSON.stringify(json), { headers: { "Content-Type": "application/json" } }); @@ -3864,7 +3873,7 @@ async function handleResponsesInner( parsed._rawBody, response, continuationStateForResponse(providerState), - activeAdapter.name === "kiro" ? { force: true } : undefined, + responseStateOptions(activeAdapter.name === "kiro"), ), }), }, @@ -3920,7 +3929,7 @@ async function handleResponsesInner( parsed._rawBody, json, continuationStateForResponse(providerState), - activeAdapter.name === "kiro" ? { force: true } : undefined, + responseStateOptions(activeAdapter.name === "kiro"), ); } return new Response(JSON.stringify(json), { headers: { "Content-Type": "application/json" } }); diff --git a/tests/responses-state.test.ts b/tests/responses-state.test.ts index 7bd3178a72..e4c53693f6 100644 --- a/tests/responses-state.test.ts +++ b/tests/responses-state.test.ts @@ -33,6 +33,7 @@ import { previousResponseProviderState, previousResponseReplayFailure, previousResponseReplayPrefixLength, + previousResponseScopeMismatch, recoverStaleResponseStateTemps, rememberResponseState, responseAdmissionCountersForTests, @@ -151,6 +152,51 @@ describe("Responses previous_response_id state", () => { ]); }); + test("replays continuation only inside the originating client task", () => { + const firstBody = { model: "cursor/auto", input: "private task A history", store: true }; + const first = fixedResponse("resp_task_scoped", [ + { type: "message", role: "assistant", content: "task A answer" }, + ]); + rememberResponseState(firstBody, first, undefined, { clientThreadId: "task-a" }); + + const sameTask = expandPreviousResponseInput({ + model: "cursor/auto", + previous_response_id: first.id, + input: "continue task A", + }, "task-a") as { previous_response_id?: string; input: unknown[] }; + expect(sameTask.previous_response_id).toBe(first.id); + expect(sameTask.input).toEqual([ + { role: "user", content: "private task A history" }, + first.output[0], + { role: "user", content: "continue task A" }, + ]); + expect(previousResponseScopeMismatch(sameTask)).toBe(false); + + const foreignTask = expandPreviousResponseInput({ + model: "cursor/auto", + previous_response_id: first.id, + input: "brand-new task B", + }, "task-b") as { previous_response_id?: string; input: string }; + expect(foreignTask).toEqual({ model: "cursor/auto", input: "brand-new task B" }); + expect(previousResponseScopeMismatch(foreignTask)).toBe(true); + expect(previousResponseReplayPrefixLength(foreignTask)).toBe(0); + expect(responseStateMetrics().replayScopeMismatchDrops).toBe(1); + }); + + test("scoped tasks reject legacy unscoped continuation state", () => { + const first = fixedResponse("resp_legacy_unscoped", [ + { type: "message", role: "assistant", content: "legacy answer" }, + ]); + rememberResponseState({ input: "legacy history" }, first); + + const freshTask = expandPreviousResponseInput({ + previous_response_id: first.id, + input: "new task", + }, "task-new"); + expect(freshTask).toEqual({ input: "new task" }); + expect(previousResponseScopeMismatch(freshTask)).toBe(true); + }); + test("stores incomplete partial output for previous_response_id replay", () => { const partial = { type: "message", @@ -710,13 +756,27 @@ describe("Responses previous_response_id state", () => { test("replays a durable spill after simulated process restart", async () => { setResponseStateByteCapForTests(1_024); - rememberLarge("resp_spill_restart", "r".repeat(8_000)); + rememberResponseState( + { model: "test/model", input: "r".repeat(8_000), store: false }, + fixedResponse("resp_spill_restart", [{ type: "message", role: "assistant", content: "stored" }]), + undefined, + { force: true, clientThreadId: "task-spill" }, + ); await flushResponseState(); clearResponseStateMemoryForTests(); setResponseStateByteCapForTests(1_024); - const expanded = expandPreviousResponseInput({ previous_response_id: "resp_spill_restart", input: "next" }); + const expanded = expandPreviousResponseInput( + { previous_response_id: "resp_spill_restart", input: "next" }, + "task-spill", + ); expect((expanded as { input: unknown[] }).input).toHaveLength(3); expect(responseStateMetrics().spillStubCount).toBe(1); + + const foreign = expandPreviousResponseInput( + { previous_response_id: "resp_spill_restart", input: "foreign" }, + "task-other", + ); + expect(foreign).toEqual({ input: "foreign" }); }); test("spill references bind the expected response id and use the locked digest basename", () => { @@ -1757,6 +1817,7 @@ describe("Responses previous_response_id state", () => { spillWrites: 0, spillWriteFailures: 0, spillReadFailures: 0, + replayScopeMismatchDrops: 0, }); }); @@ -1814,6 +1875,7 @@ describe("Responses previous_response_id state", () => { spillWrites: 0, spillWriteFailures: 0, spillReadFailures: 0, + replayScopeMismatchDrops: 0, }); // The real request path DOES load; the probe then reflects the loaded entry. From 896530645ba8baa64eae164a44f7cb2c62436106 Mon Sep 17 00:00:00 2001 From: Jonathan Li <47408717+jonathanli12@users.noreply.github.com> Date: Thu, 13 Aug 2026 00:54:04 -0700 Subject: [PATCH 2/2] fix(server): await initial startup health probe --- src/responses/state.ts | 1 - src/server/startup-health-cache.ts | 12 ++++++++++++ tests/autostart-health.test.ts | 19 ++++++++++++------- 3 files changed, 24 insertions(+), 8 deletions(-) diff --git a/src/responses/state.ts b/src/responses/state.ts index cd7dc11d90..06aa9329bb 100644 --- a/src/responses/state.ts +++ b/src/responses/state.ts @@ -66,7 +66,6 @@ export type PreviousResponseReplayFailure = { reason: "spill_missing" | "spill_corrupt" | "spill_failed" | "spill_too_large"; }; -// OCX_LOCAL_PATCH_RESPONSE_STATE_TASK_SCOPE: prevent cross-task continuation replay. const states = new Map(); const replayScopeMismatches = new WeakSet(); let storedResponseBytes = 0; diff --git a/src/server/startup-health-cache.ts b/src/server/startup-health-cache.ts index d878be9ee4..63dd67f651 100644 --- a/src/server/startup-health-cache.ts +++ b/src/server/startup-health-cache.ts @@ -10,6 +10,7 @@ import { truncateRetainedUtf8 } from "../lib/admission"; const CACHE_TTL_MS = 30_000; const PROBE_TIMEOUT_MS = 5_000; +const INITIAL_PROBE_WAIT_MS = 5_500; const MAX_DIAGNOSTIC_VALUE_BYTES = 8 * 1024; let cached: { timestamp: number; value: StartupHealth } | null = null; let inflight: Promise | null = null; @@ -109,6 +110,17 @@ function refreshInBackground(config: Pick): void { export async function getCachedStartupHealth(config: Pick): Promise { if (cached && Date.now() - cached.timestamp < CACHE_TTL_MS) return cached.value; refreshInBackground(config); + // An expired or empty read is an explicit protection check. Wait for the + // isolated probe instead of presenting a synthetic failure while that probe + // is still running. The probe remains child-process isolated and hard-capped + // at 5s; stale state is returned only if that bounded probe cannot settle. + if (inflight) { + const settled = await Promise.race([ + inflight, + new Promise(resolve => setTimeout(() => resolve(null), INITIAL_PROBE_WAIT_MS)), + ]); + if (settled) return settled; + } return cached ? markStartupHealthDiagnosticStale(cached.value) : conservativeFallback(config); } diff --git a/tests/autostart-health.test.ts b/tests/autostart-health.test.ts index 0ed31e33a2..157f1e95ab 100644 --- a/tests/autostart-health.test.ts +++ b/tests/autostart-health.test.ts @@ -192,19 +192,14 @@ describe("Codex startup health", () => { expect(startupHealthSummary(custom)).not.toContain("ocx service install"); }); - test("exposes a secret-free startup health DTO to the dashboard", async () => { + test("exposes fresh secret-free startup health across cache expiry", async () => { invalidateStartupHealthCache(); const url = new URL("http://localhost/api/startup-health"); - let timerFired = false; - const timer = setTimeout(() => { timerFired = true; }, 25); const responsePromise = handleManagementAPI( new Request(url), url, { port: 10100, providers: {}, defaultProvider: "openai", codexAutoStart: true } as OcxConfig, ); - await Bun.sleep(75); - expect(timerFired).toBe(true); // service-manager probes run in a child, not the proxy event loop - clearTimeout(timer); const response = await responsePromise; expect(response?.status).toBe(200); @@ -212,6 +207,7 @@ describe("Codex startup health", () => { expect(["native", "protected", "at-risk"]).toContain(body.status); expect(typeof body.rebootSafe).toBe("boolean"); expect(typeof body.routingInjected).toBe("boolean"); + expect(body.diagnosticStale).toBe(false); expect(body.commands).toEqual({ installService: "ocx service install", repairService: "ocx service repair", @@ -223,6 +219,15 @@ describe("Codex startup health", () => { for (const secretName of ["api_key", "apikey", "authorization", "access_token", "refresh_token"]) { expect(serialized).not.toContain(secretName); } - }); + + await Bun.sleep(30_050); + const refreshed = await handleManagementAPI( + new Request(url), + url, + { port: 10100, providers: {}, defaultProvider: "openai", codexAutoStart: true } as OcxConfig, + ); + const refreshedBody = await refreshed!.json() as Record; + expect(refreshedBody.diagnosticStale).toBe(false); + }, 40_000); }); import { ManagementRequest as Request } from "./helpers/management-auth";