From 86443f5058d49882e37d7cdd7f1f334e905b284a Mon Sep 17 00:00:00 2001 From: mouzhi Date: Tue, 25 Aug 2026 21:34:11 +0800 Subject: [PATCH 1/4] fix(renderer): stop background retry storms --- .../src/renderer-binding-probe.ts | 34 +++++-- .../src/renderer-settings-lifecycle.ts | 2 + .../test/renderer-binding-probe.test.ts | 45 +++++++++ .../test/renderer-settings-lifecycle.test.ts | 95 +++++++++++++++++++ 4 files changed, 167 insertions(+), 9 deletions(-) create mode 100644 packages/renderer-extension/test/renderer-settings-lifecycle.test.ts diff --git a/packages/renderer-extension/src/renderer-binding-probe.ts b/packages/renderer-extension/src/renderer-binding-probe.ts index c0e52377..c00c59f4 100644 --- a/packages/renderer-extension/src/renderer-binding-probe.ts +++ b/packages/renderer-extension/src/renderer-binding-probe.ts @@ -90,10 +90,20 @@ const externalAgents: readonly ExternalRendererAgent[] = [ "grok", ]; type HarnessAvailability = Partial>; +type HarnessAvailabilityErrors = Record; + +export function retryableHarnessAvailabilityAgents( + availability: HarnessAvailability, + errors: HarnessAvailabilityErrors, +): ExternalRendererAgent[] { + return externalAgents.filter( + (agent) => availability[agent] === "error" && errors[agent]?.retryable === true, + ); +} interface HostHarnessAvailabilityState { availability: HarnessAvailability; - errors: Record; + errors: HarnessAvailabilityErrors; requestGeneration: number; request: { client: RendererModelClient; promise: Promise } | null; retryTimer: number | null; @@ -1578,12 +1588,18 @@ export function installRendererBindingProbe( return Promise.resolve(); } if (state.request?.client === client) return state.request.promise; - state.availability = Object.fromEntries( - externalAgents.map((agent) => [ - agent, - state.availability[agent] === "ready" ? "ready" : "checking", - ]), - ) as HarnessAvailability; + const agentsToInspect = retry + ? retryableHarnessAvailabilityAgents(state.availability, state.errors) + : externalAgents; + if (agentsToInspect.length === 0) { + resetHarnessAvailabilityRetry(hostId); + return Promise.resolve(); + } + const nextAvailability = { ...state.availability }; + for (const agent of agentsToInspect) { + if (nextAvailability[agent] !== "ready") nextAvailability[agent] = "checking"; + } + state.availability = nextAvailability; if (hostId === activeAvailabilityHostId) { publishConnectionStatus(); for (const mounted of mountedByComposer.values()) renderMounted(mounted); @@ -1591,7 +1607,7 @@ export function installRendererBindingProbe( const generation = ++state.requestGeneration; const promise = (async () => { await Promise.all( - externalAgents.map(async (agent) => { + agentsToInspect.map(async (agent) => { let status: RendererAgentAvailability = "error"; let nextError: CodexhostError | undefined; try { @@ -1658,7 +1674,7 @@ export function installRendererBindingProbe( }), ); if (generation !== state.requestGeneration || disposed) return; - if (externalAgents.every((agent) => state.availability[agent] === "ready")) { + if (retryableHarnessAvailabilityAgents(state.availability, state.errors).length === 0) { resetHarnessAvailabilityRetry(hostId); } else { scheduleHarnessAvailabilityRetry(hostId); diff --git a/packages/renderer-extension/src/renderer-settings-lifecycle.ts b/packages/renderer-extension/src/renderer-settings-lifecycle.ts index ebe10b91..e8076c7e 100644 --- a/packages/renderer-extension/src/renderer-settings-lifecycle.ts +++ b/packages/renderer-extension/src/renderer-settings-lifecycle.ts @@ -164,6 +164,8 @@ export function installRendererSettingsLifecycle( clearUpdateRetry(); retryUpdateClient = client; updateRetryAttempt = 0; + } else if (updateRetryTimer !== null) { + return; } checkedUpdateClient = client; const generation = ++updateCheckGeneration; diff --git a/packages/renderer-extension/test/renderer-binding-probe.test.ts b/packages/renderer-extension/test/renderer-binding-probe.test.ts index 95146681..a4b2f13d 100644 --- a/packages/renderer-extension/test/renderer-binding-probe.test.ts +++ b/packages/renderer-extension/test/renderer-binding-probe.test.ts @@ -16,6 +16,7 @@ import { isOwnershipSubmissionBlocked, lateConversationTargetResolution, restoredThreadOwnership, + retryableHarnessAvailabilityAgents, rendererUsageRefreshDelay, shouldApplyDraftAgentCarrier, shouldPersistNewThreadConfigurationSelection, @@ -44,6 +45,50 @@ import { } from "../src/renderer-usage-control.js"; describe("Renderer Composer DOM behavior", () => { + it("does not re-probe ready Agents when an optional Harness is not installed", () => { + expect( + retryableHarnessAvailabilityAgents( + { + pi: "ready", + "claude-code": "ready", + "deepseek-harness": "notInstalled", + grok: "ready", + }, + { + pi: undefined, + "claude-code": undefined, + "deepseek-harness": { + code: "notInstalled", + message: "DeepSeek Harness is not installed", + retryable: false, + }, + grok: undefined, + }, + ), + ).toEqual([]); + + expect( + retryableHarnessAvailabilityAgents( + { + pi: "ready", + "claude-code": "ready", + "deepseek-harness": "error", + grok: "ready", + }, + { + pi: undefined, + "claude-code": undefined, + "deepseek-harness": { + code: "internalError", + message: "Remote request manager is temporarily unavailable", + retryable: true, + }, + grok: undefined, + }, + ), + ).toEqual(["deepseek-harness"]); + }); + it("keeps a ready external Model catalog stable during repeated availability checks", () => { expect(shouldReloadExternalCatalogAfterAvailabilityRefresh("ready", "ready", true)).toBe(false); expect(shouldReloadExternalCatalogAfterAvailabilityRefresh("ready", "ready", false)).toBe(true); diff --git a/packages/renderer-extension/test/renderer-settings-lifecycle.test.ts b/packages/renderer-extension/test/renderer-settings-lifecycle.test.ts new file mode 100644 index 00000000..c712b4fa --- /dev/null +++ b/packages/renderer-extension/test/renderer-settings-lifecycle.test.ts @@ -0,0 +1,95 @@ +import type { UpdateCheckResult } from "@codexhost/shared-contracts"; +import { afterEach, describe, expect, it, vi } from "vitest"; + +const triggerRefresh = vi.fn(() => true); +const triggerSetUpdateAvailable = vi.fn(); + +vi.mock("../src/codex-locale-adapter.js", () => ({ + readCodexLocaleSettings: vi.fn(async () => ({ preferredLocale: "en" })), +})); + +vi.mock("../src/settings/localization.js", () => ({ + rendererSettingsMessages: vi.fn(() => ({})), + resolveRendererSettingsLocale: vi.fn(() => "en"), +})); + +vi.mock("../src/settings/pages.js", () => ({ + createDefaultRendererSettingsPages: vi.fn(() => []), +})); + +vi.mock("../src/settings/shell.js", () => ({ + installRendererSettingsShell: vi.fn(() => ({ + supported: true, + open: false, + activePageId: undefined, + openSettings: vi.fn(), + dispose: vi.fn(), + })), +})); + +vi.mock("../src/settings/trigger.js", () => ({ + installRendererSettingsHeaderTrigger: vi.fn(() => ({ + root: null, + refresh: triggerRefresh, + setUpdateAvailable: triggerSetUpdateAvailable, + dispose: vi.fn(), + })), +})); + +import { installRendererSettingsLifecycle } from "../src/renderer-settings-lifecycle.js"; + +function failedUpdateCheck(): UpdateCheckResult { + return { + currentVersion: "0.3.2", + installation: "npm", + latestVersion: null, + updateAvailable: false, + installationAvailable: false, + releaseNotes: null, + releaseNotesUrl: null, + status: null, + error: "Update metadata is temporarily unavailable", + }; +} + +describe("Renderer Settings lifecycle", () => { + afterEach(() => { + vi.useRealTimers(); + vi.clearAllMocks(); + }); + + it("does not bypass update backoff when DOM reconciliation refreshes repeatedly", async () => { + vi.useFakeTimers(); + const checkUpdate = vi.fn(async () => failedUpdateCheck()); + const client = { + checkUpdate, + startUpdate: vi.fn(), + readUpdateStatus: vi.fn(), + }; + const ownerWindow = { + navigator: { languages: ["en"] }, + document: {}, + setTimeout, + clearTimeout, + } as unknown as Window; + + const lifecycle = installRendererSettingsLifecycle(ownerWindow, { + getUpdateClient: () => client, + }); + await Promise.resolve(); + await Promise.resolve(); + expect(checkUpdate).toHaveBeenCalledTimes(1); + + for (let index = 0; index < 50; index += 1) lifecycle.refresh(); + await Promise.resolve(); + await Promise.resolve(); + expect(checkUpdate).toHaveBeenCalledTimes(1); + + await vi.advanceTimersByTimeAsync(999); + expect(checkUpdate).toHaveBeenCalledTimes(1); + await vi.advanceTimersByTimeAsync(1); + expect(checkUpdate).toHaveBeenCalledTimes(2); + + lifecycle.dispose(); + }); +}); From d9bad7a347859685370391672f628d36a8faf594 Mon Sep 17 00:00:00 2001 From: mouzhi Date: Tue, 25 Aug 2026 22:10:24 +0800 Subject: [PATCH 2/4] fix(renderer): keep terminal harness states stable --- .../src/renderer-binding-probe.ts | 20 +++++++-- .../test/renderer-binding-probe.test.ts | 41 +++++++++++++++++++ 2 files changed, 57 insertions(+), 4 deletions(-) diff --git a/packages/renderer-extension/src/renderer-binding-probe.ts b/packages/renderer-extension/src/renderer-binding-probe.ts index c00c59f4..25aa4948 100644 --- a/packages/renderer-extension/src/renderer-binding-probe.ts +++ b/packages/renderer-extension/src/renderer-binding-probe.ts @@ -101,6 +101,17 @@ export function retryableHarnessAvailabilityAgents( ); } +export function passiveHarnessAvailabilityAgents( + availability: HarnessAvailability, + errors: HarnessAvailabilityErrors, +): ExternalRendererAgent[] { + return externalAgents.filter( + (agent) => + availability[agent] === "checking" || + (availability[agent] === "error" && errors[agent]?.retryable === true), + ); +} + interface HostHarnessAvailabilityState { availability: HarnessAvailability; errors: HarnessAvailabilityErrors; @@ -1579,6 +1590,7 @@ export function installRendererBindingProbe( hostId: string, refresh = false, retry = false, + force = false, ): Promise { const state = hostHarnessAvailabilityState(hostId); if (!retry) resetHarnessAvailabilityRetry(hostId); @@ -1588,9 +1600,9 @@ export function installRendererBindingProbe( return Promise.resolve(); } if (state.request?.client === client) return state.request.promise; - const agentsToInspect = retry - ? retryableHarnessAvailabilityAgents(state.availability, state.errors) - : externalAgents; + const agentsToInspect = force + ? externalAgents + : passiveHarnessAvailabilityAgents(state.availability, state.errors); if (agentsToInspect.length === 0) { resetHarnessAvailabilityRetry(hostId); return Promise.resolve(); @@ -1765,7 +1777,7 @@ export function installRendererBindingProbe( }, refresh(): Promise { for (const hostId of harnessAvailabilityByHost.keys()) { - void refreshHarnessAvailabilityForHost(hostId, true); + void refreshHarnessAvailabilityForHost(hostId, true, false, true); } return Promise.resolve(); }, diff --git a/packages/renderer-extension/test/renderer-binding-probe.test.ts b/packages/renderer-extension/test/renderer-binding-probe.test.ts index a4b2f13d..88936d84 100644 --- a/packages/renderer-extension/test/renderer-binding-probe.test.ts +++ b/packages/renderer-extension/test/renderer-binding-probe.test.ts @@ -15,6 +15,7 @@ import { isComposerModelWriteAllowed, isOwnershipSubmissionBlocked, lateConversationTargetResolution, + passiveHarnessAvailabilityAgents, restoredThreadOwnership, retryableHarnessAvailabilityAgents, rendererUsageRefreshDelay, @@ -89,6 +90,46 @@ describe("Renderer Composer DOM behavior", () => { ).toEqual(["deepseek-harness"]); }); + it("keeps terminal Harness availability stable across passive focus refreshes", () => { + expect( + passiveHarnessAvailabilityAgents( + { + pi: "checking", + "claude-code": "checking", + "deepseek-harness": "checking", + grok: "checking", + }, + { + pi: undefined, + "claude-code": undefined, + "deepseek-harness": undefined, + grok: undefined, + }, + ), + ).toEqual(["pi", "claude-code", "deepseek-harness", "grok"]); + + expect( + passiveHarnessAvailabilityAgents( + { + pi: "ready", + "claude-code": "ready", + "deepseek-harness": "notInstalled", + grok: "ready", + }, + { + pi: undefined, + "claude-code": undefined, + "deepseek-harness": { + code: "notInstalled", + message: "DeepSeek Harness is not installed", + retryable: false, + }, + grok: undefined, + }, + ), + ).toEqual([]); + }); + it("keeps a ready external Model catalog stable during repeated availability checks", () => { expect(shouldReloadExternalCatalogAfterAvailabilityRefresh("ready", "ready", true)).toBe(false); expect(shouldReloadExternalCatalogAfterAvailabilityRefresh("ready", "ready", false)).toBe(true); From e9469afcf864ef0f7737aaeccfa9494ecbab32b8 Mon Sep 17 00:00:00 2001 From: mouzhi Date: Tue, 25 Aug 2026 22:45:25 +0800 Subject: [PATCH 3/4] chore: format merged usage changes --- packages/harness-adapter/src/usage.ts | 10 ++-------- .../host-runtime/test/app-server-host.test.ts | 4 +--- .../src/renderer-usage-control.ts | 3 +-- packages/shared-contracts/src/thread-usage.ts | 16 ++++++++++++---- 4 files changed, 16 insertions(+), 17 deletions(-) diff --git a/packages/harness-adapter/src/usage.ts b/packages/harness-adapter/src/usage.ts index c4c45abf..093efffc 100644 --- a/packages/harness-adapter/src/usage.ts +++ b/packages/harness-adapter/src/usage.ts @@ -113,18 +113,12 @@ export function parseHostUsage(value: unknown): HostUsage { if (hasContextWindow && value.contextWindowTokens === 0) { throw new Error("Harness Usage 'contextWindowTokens' must be greater than zero"); } - if ( - value.planFiveHourResetsAtUnix !== undefined && - value.planFiveHourUsedPercent === undefined - ) { + if (value.planFiveHourResetsAtUnix !== undefined && value.planFiveHourUsedPercent === undefined) { throw new Error( "Harness Usage 'planFiveHourResetsAtUnix' must be provided with 'planFiveHourUsedPercent'", ); } - if ( - value.planSevenDayResetsAtUnix !== undefined && - value.planSevenDayUsedPercent === undefined - ) { + if (value.planSevenDayResetsAtUnix !== undefined && value.planSevenDayUsedPercent === undefined) { throw new Error( "Harness Usage 'planSevenDayResetsAtUnix' must be provided with 'planSevenDayUsedPercent'", ); diff --git a/packages/host-runtime/test/app-server-host.test.ts b/packages/host-runtime/test/app-server-host.test.ts index ba8ca8e1..397becb5 100644 --- a/packages/host-runtime/test/app-server-host.test.ts +++ b/packages/host-runtime/test/app-server-host.test.ts @@ -2208,9 +2208,7 @@ describe("AppServerHost HarnessAdapter projection", () => { method: "codexhost/thread/usage/inspect", params: { threadId: claudeThreadId }, }); - await expect( - fixture.collector.waitFor((message) => requestId(message, 72)), - ).resolves.toEqual({ + await expect(fixture.collector.waitFor((message) => requestId(message, 72))).resolves.toEqual({ id: 72, result: { threadId: claudeThreadId, diff --git a/packages/renderer-extension/src/renderer-usage-control.ts b/packages/renderer-extension/src/renderer-usage-control.ts index 2ec26ab0..af28ab65 100644 --- a/packages/renderer-extension/src/renderer-usage-control.ts +++ b/packages/renderer-extension/src/renderer-usage-control.ts @@ -129,8 +129,7 @@ export function applyRendererPopoverChrome(popover: HTMLElement): void { popover.style.borderRadius = "14px"; popover.style.backgroundColor = "color-mix(in srgb, Canvas 92%, CanvasText 8%)"; popover.style.color = "CanvasText"; - popover.style.boxShadow = - "0 20px 45px rgba(0, 0, 0, 0.35), 0 2px 10px rgba(0, 0, 0, 0.22)"; + popover.style.boxShadow = "0 20px 45px rgba(0, 0, 0, 0.35), 0 2px 10px rgba(0, 0, 0, 0.22)"; } function addDetailRow(parent: HTMLElement, label: string, value: string): void { diff --git a/packages/shared-contracts/src/thread-usage.ts b/packages/shared-contracts/src/thread-usage.ts index 7c825d26..bede1b0e 100644 --- a/packages/shared-contracts/src/thread-usage.ts +++ b/packages/shared-contracts/src/thread-usage.ts @@ -45,17 +45,25 @@ export const threadUsageSnapshotSchema = z path: ["contextWindowTokens"], }); } - if (usage.planFiveHourResetsAtUnix !== undefined && usage.planFiveHourUsedPercent === undefined) { + if ( + usage.planFiveHourResetsAtUnix !== undefined && + usage.planFiveHourUsedPercent === undefined + ) { context.addIssue({ code: "custom", - message: "Thread Usage planFiveHourResetsAtUnix must be provided with planFiveHourUsedPercent", + message: + "Thread Usage planFiveHourResetsAtUnix must be provided with planFiveHourUsedPercent", path: ["planFiveHourResetsAtUnix"], }); } - if (usage.planSevenDayResetsAtUnix !== undefined && usage.planSevenDayUsedPercent === undefined) { + if ( + usage.planSevenDayResetsAtUnix !== undefined && + usage.planSevenDayUsedPercent === undefined + ) { context.addIssue({ code: "custom", - message: "Thread Usage planSevenDayResetsAtUnix must be provided with planSevenDayUsedPercent", + message: + "Thread Usage planSevenDayResetsAtUnix must be provided with planSevenDayUsedPercent", path: ["planSevenDayResetsAtUnix"], }); } From e36c6cff806c19351b906b3c387dfbb1cad84e0a Mon Sep 17 00:00:00 2001 From: BytePioneer-AI <28417416@qq.com> Date: Wed, 26 Aug 2026 10:55:42 +0800 Subject: [PATCH 4/4] fix(renderer): retry transient unavailable harnesses --- .../src/renderer-binding-probe.ts | 18 ++++++-- .../test/renderer-binding-probe.test.ts | 42 +++++++++++++++++++ 2 files changed, 57 insertions(+), 3 deletions(-) diff --git a/packages/renderer-extension/src/renderer-binding-probe.ts b/packages/renderer-extension/src/renderer-binding-probe.ts index 5ab00539..d71c2fb1 100644 --- a/packages/renderer-extension/src/renderer-binding-probe.ts +++ b/packages/renderer-extension/src/renderer-binding-probe.ts @@ -92,12 +92,24 @@ const externalAgents: readonly ExternalRendererAgent[] = [ type HarnessAvailability = Partial>; type HarnessAvailabilityErrors = Record; +function isRetryableHarnessAvailability( + availability: RendererAgentAvailability | undefined, + error: CodexhostError | undefined, +): boolean { + return ( + availability !== undefined && + availability !== "ready" && + availability !== "notInstalled" && + error?.retryable === true + ); +} + export function retryableHarnessAvailabilityAgents( availability: HarnessAvailability, errors: HarnessAvailabilityErrors, ): ExternalRendererAgent[] { - return externalAgents.filter( - (agent) => availability[agent] === "error" && errors[agent]?.retryable === true, + return externalAgents.filter((agent) => + isRetryableHarnessAvailability(availability[agent], errors[agent]), ); } @@ -108,7 +120,7 @@ export function passiveHarnessAvailabilityAgents( return externalAgents.filter( (agent) => availability[agent] === "checking" || - (availability[agent] === "error" && errors[agent]?.retryable === true), + isRetryableHarnessAvailability(availability[agent], errors[agent]), ); } diff --git a/packages/renderer-extension/test/renderer-binding-probe.test.ts b/packages/renderer-extension/test/renderer-binding-probe.test.ts index 0275e16f..e1b1a7d8 100644 --- a/packages/renderer-extension/test/renderer-binding-probe.test.ts +++ b/packages/renderer-extension/test/renderer-binding-probe.test.ts @@ -88,6 +88,27 @@ describe("Renderer Composer DOM behavior", () => { }, ), ).toEqual(["deepseek-harness"]); + + expect( + retryableHarnessAvailabilityAgents( + { + pi: "ready", + "claude-code": "ready", + "deepseek-harness": "unavailable", + grok: "ready", + }, + { + pi: undefined, + "claude-code": undefined, + "deepseek-harness": { + code: "unavailable", + message: "DeepSeek Harness is temporarily unavailable", + retryable: true, + }, + grok: undefined, + }, + ), + ).toEqual(["deepseek-harness"]); }); it("keeps terminal Harness availability stable across passive focus refreshes", () => { @@ -128,6 +149,27 @@ describe("Renderer Composer DOM behavior", () => { }, ), ).toEqual([]); + + expect( + passiveHarnessAvailabilityAgents( + { + pi: "ready", + "claude-code": "ready", + "deepseek-harness": "unavailable", + grok: "ready", + }, + { + pi: undefined, + "claude-code": undefined, + "deepseek-harness": { + code: "unavailable", + message: "DeepSeek Harness is temporarily unavailable", + retryable: true, + }, + grok: undefined, + }, + ), + ).toEqual(["deepseek-harness"]); }); it("keeps a ready external Model catalog stable during repeated availability checks", () => {