Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
114 changes: 114 additions & 0 deletions apps/codex-plus-manager/src/renderer-inject.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void>;
}

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<number> = [];
const fakeWindow: Record<string, unknown> = {
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"]);
});
});
24 changes: 17 additions & 7 deletions assets/inject/renderer-inject.js
Original file line number Diff line number Diff line change
Expand Up @@ -2513,15 +2513,14 @@
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) => {
const score = (url) => {
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;
Expand Down Expand Up @@ -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) {
Expand All @@ -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();
}
}

Expand All @@ -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,
Expand Down
Loading