From bed5277d29969991ec5770616ab51dab03be69c9 Mon Sep 17 00:00:00 2001 From: dongyu <1410875946@qq.com> Date: Mon, 21 Sep 2026 23:14:01 +0800 Subject: [PATCH] =?UTF-8?q?fix(inject):=20app-server=20patch=20=E7=86=94?= =?UTF-8?q?=E6=96=AD=E4=B8=8D=E5=86=8D=E8=A2=AB=20provider=20=E9=87=8D?= =?UTF-8?q?=E8=AF=95=E7=BB=95=E8=BF=87=EF=BC=8C=E9=87=8D=E8=AF=95=E6=94=B9?= =?UTF-8?q?=E6=8C=87=E6=95=B0=E9=80=80=E9=81=BF=20(issue=20#2256/#2255)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - noteAppServerModelRequestPatchMiss 删除 provider 模式提前 return,两条路径统一 miss 计数,maxMisses(8) 熔断对所有模式生效;熔断时清掉挂起的重试定时器。 - scheduleAppServerModelRequestPatchRetry 重试间隔从固定 250ms 改为指数退避 (250ms ×4 递增,30s 封顶),补丁装成功后 miss 计数与退避间隔复位。 - appServerFallbackAssetUrls 删除硬编码的旧版 bundle 哈希(gwqc41kz 等), 26.911+ 上本就匹配不到任何文件,属死代码;稳定模块名模式保持不变。 - 按 renderer-inject.test.ts 既有 harness 模式补 3 个回归测试。 --- .../src/renderer-inject.test.ts | 114 ++++++++++++++++++ assets/inject/renderer-inject.js | 24 ++-- 2 files changed, 131 insertions(+), 7 deletions(-) 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,