diff --git a/apps/codex-plus-manager/src/renderer-inject.test.ts b/apps/codex-plus-manager/src/renderer-inject.test.ts index 6a09ba66f..25bf15a63 100644 --- a/apps/codex-plus-manager/src/renderer-inject.test.ts +++ b/apps/codex-plus-manager/src/renderer-inject.test.ts @@ -958,3 +958,117 @@ describe("Stepwise generation mode contracts", () => { ); }); }); + +// issue #2256/#2255:app-server model request patch 的 miss 熔断以前被 provider +// 重试路径提前 return 绕过,失败变成 250ms 无限重试(每轮全量 fetch 全部 app asset)。 +describe("renderer injection app-server model request patch", () => { + const rendererPath = new URL("../../../assets/inject/renderer-inject.js", import.meta.url); + + interface AppServerPatchHarness { + install: () => void; + sweeps: () => number; + diagnostics: () => string[]; + settle: () => Promise; + } + + function appServerPatchRuntime(renderer: string, patchSucceeds: boolean): AppServerPatchHarness { + const start = renderer.indexOf(" const appServerModelRequestPatchMaxMisses = "); + const end = renderer.indexOf("\n function ensureCodexModelWhitelistInstalls(", start); + assert.ok(start >= 0 && end > start, "app-server model request patch block not found"); + const source = renderer.slice(start, end); + + let sweeps = 0; + let pending: Array<() => void> = []; + const diagnostics: string[] = []; + const timers: Array = []; + const fakeWindow: Record = { + setTimeout: ((fn: () => void) => { + timers.push(0); + pending.push(fn); + return 0; + }) as unknown, + clearTimeout: () => {}, + }; + + const factory = new Function( + "window", + "codexAppServerModelRequestPatchVersion", + "codexRemoteSessionProviderPatchEnabled", + "loadAppServerRequestCandidates", + "patchAppServerModelRequestClient", + "sendCodexPlusDiagnostic", + "Date", + `${source}\nreturn installAppServerModelRequestPatch;`, + ); + + const install = factory( + fakeWindow, + 1, + // provider patch 开关两态都要测:以前 enabled 时走提前 return 绕过熔断。 + () => true, + () => + new Promise((resolve) => { + sweeps += 1; + pending.push(() => resolve({ modules: [{}], candidates: [{}], sources: [], discovery: "fallback" })); + }), + () => patchSucceeds, + (event: string) => diagnostics.push(event), + Date, + ) as () => void; + + const settle = async () => { + // 重试定时器是挂起的回调:排空 sweep 再触发到期的 retry,直到没有新定时器。 + for (let round = 0; round < 32; round += 1) { + if (!pending.length) break; + const flushSweeps = pending; + pending = []; + flushSweeps.forEach((resolve) => resolve()); + await Promise.resolve(); + await Promise.resolve(); + await Promise.resolve(); + } + }; + + return { install, sweeps: () => sweeps, diagnostics: () => diagnostics, settle }; + } + + it("does not start a new sweep while the previous one is still running", async () => { + const harness = appServerPatchRuntime(await readFile(rendererPath, "utf8"), false); + + for (let i = 0; i < 20; i += 1) harness.install(); + + assert.equal(harness.sweeps(), 1); + await harness.settle(); + }); + + it("stops retrying via the provider path once maxMisses is reached", async () => { + const harness = appServerPatchRuntime(await readFile(rendererPath, "utf8"), false); + + // 反复 install + settle,让每轮 miss 走完 provider 重试调度。 + for (let i = 0; i < 40; i += 1) { + harness.install(); + await harness.settle(); + } + + // 关键回归断言:以前 provider 路径无限重试(40 轮 = 40 次 sweep), + // 现在到 maxMisses(8) 就熔断停手。 + assert.equal(harness.sweeps(), 8); + assert.equal(harness.diagnostics().filter((e) => e === "model_app_server_request_patch_not_found").length, 1); + assert.deepEqual(harness.diagnostics().at(-1), "model_app_server_request_patch_skipped"); + const settled = harness.sweeps(); + harness.install(); + await harness.settle(); + assert.equal(harness.sweeps(), settled); + }); + + it("keeps working normally when the patch actually lands", async () => { + const harness = appServerPatchRuntime(await readFile(rendererPath, "utf8"), true); + + harness.install(); + await harness.settle(); + for (let i = 0; i < 10; i += 1) harness.install(); + + assert.equal(harness.sweeps(), 1); + assert.deepEqual(harness.diagnostics(), ["model_app_server_request_patch_installed"]); + }); +}); diff --git a/assets/inject/renderer-inject.js b/assets/inject/renderer-inject.js index 00b88fbc2..df3bf7f0d 100644 --- a/assets/inject/renderer-inject.js +++ b/assets/inject/renderer-inject.js @@ -2513,7 +2513,7 @@ const urls = codexAppAssetCandidateUrls(); const preferred = urls.filter((url) => { const name = (url.split("/").pop() || "").toLowerCase(); - return /use-host-config|app-server-manager-signals|app-initial|app-main|page-|chatg|signals|server-manager|gwqc41kz|c1urrgy0|hsvsqcnf/.test(name); + return /use-host-config|app-server-manager-signals|app-initial|app-main|page-|chatg|signals|server-manager/.test(name); }); // Prefer known request-client modules, then the larger application bundles. preferred.sort((left, right) => { @@ -2521,7 +2521,6 @@ const name = (url.split("/").pop() || "").toLowerCase(); if (name.includes("use-host-config")) return 0; if (name.includes("app-server-manager-signals")) return 1; - if (name.includes("gwqc41kz") || name.includes("c1urrgy0") || name.includes("hsvsqcnf")) return 2; if (name.includes("app-initial") && name.includes("app-main")) return 3; if (name.includes("app-main")) return 4; return 5; @@ -7243,18 +7242,23 @@ } const appServerModelRequestPatchMaxMisses = 8; + const appServerModelRequestPatchMaxRetryDelayMs = 30000; let appServerModelRequestPatchMissCount = 0; let appServerModelRequestPatchDisabled = false; let appServerModelRequestPatchPromise = null; let appServerModelRequestPatchRetryTimer = 0; + let appServerModelRequestPatchRetryDelayMs = 250; function scheduleAppServerModelRequestPatchRetry() { if (!codexRemoteSessionProviderPatchEnabled()) return; if (appServerModelRequestPatchRetryTimer) return; + // issue #2256/#2255:固定 250ms 重试在 Codex 改 asset 命名后变成每秒 4 轮的全量 + // rescan(每轮 fetch 全部 app asset)。改为指数退避, miss 计满后由熔断停掉。 appServerModelRequestPatchRetryTimer = window.setTimeout(() => { appServerModelRequestPatchRetryTimer = 0; installAppServerModelRequestPatch(); - }, 250); + }, appServerModelRequestPatchRetryDelayMs); + appServerModelRequestPatchRetryDelayMs = Math.min(appServerModelRequestPatchRetryDelayMs * 4, appServerModelRequestPatchMaxRetryDelayMs); } function noteAppServerModelRequestPatchMiss(event, detail) { @@ -7271,16 +7275,21 @@ if (appServerModelRequestPatchMissCount === 1) { sendCodexPlusDiagnostic(event, detail); } - if (codexRemoteSessionProviderPatchEnabled()) { - scheduleAppServerModelRequestPatchRetry(); - return; - } + // issue #2256:provider 重试路径以前在这里提前 return,绕过下面的 maxMisses + // 熔断,失败变成 250ms 无限重试(每轮全量 rescan 全部 app assets)。 + // 现在两个路径统一计数:先按 maxMisses 熔断,未熔断时再走指数退避重试。 if (appServerModelRequestPatchMissCount >= appServerModelRequestPatchMaxMisses && !appServerModelRequestPatchDisabled) { appServerModelRequestPatchDisabled = true; + clearTimeout(appServerModelRequestPatchRetryTimer); + appServerModelRequestPatchRetryTimer = 0; sendCodexPlusDiagnostic("model_app_server_request_patch_skipped", { misses: appServerModelRequestPatchMissCount, lastEvent: event, }); + return; + } + if (!appServerModelRequestPatchDisabled) { + scheduleAppServerModelRequestPatchRetry(); } } @@ -7305,6 +7314,7 @@ clearTimeout(appServerModelRequestPatchRetryTimer); appServerModelRequestPatchRetryTimer = 0; appServerModelRequestPatchMissCount = 0; + appServerModelRequestPatchRetryDelayMs = 250; window.__codexPlusAppServerModelRequestPatchInstalled = codexAppServerModelRequestPatchVersion; sendCodexPlusDiagnostic("model_app_server_request_patch_installed", { moduleCount: modules.length,