From 4153865309baec6d3015908ea62effb63941809b Mon Sep 17 00:00:00 2001 From: Remus Mate Date: Wed, 18 Feb 2026 00:27:52 +0200 Subject: [PATCH] fix: reduce excessive file writes from account state updates - Remove unconditional requestSaveToDisk() on every API request (line 1702) - Add snapshot deduplication to skip no-op writes - Increase debounce from 1s to 5s for rate-limit storm resilience - Move save trigger to after markAccountUsed() where lastUsed is persisted Fixes #436 Co-authored-by: Amp Amp-Thread-ID: https://ampcode.com/threads/T-019c6dae-36aa-7524-849b-d8c515ba0cd1 --- src/plugin.ts | 5 ++--- src/plugin/accounts.test.ts | 35 +++++++++++++++++++++++++++++++---- src/plugin/accounts.ts | 23 +++++++++++++++-------- 3 files changed, 48 insertions(+), 15 deletions(-) diff --git a/src/plugin.ts b/src/plugin.ts index cad8209..09257a3 100644 --- a/src/plugin.ts +++ b/src/plugin.ts @@ -1699,8 +1699,6 @@ export const createAntigravityPlugin = (providerId: string) => async ( accountManager.markToastShown(account.index); } - accountManager.requestSaveToDisk(); - let authRecord = accountManager.toAuthDetails(account); if (accessTokenExpired(authRecord)) { @@ -2330,7 +2328,8 @@ export const createAntigravityPlugin = (providerId: string) => async ( account.consecutiveFailures = 0; getHealthTracker().recordSuccess(account.index); accountManager.markAccountUsed(account.index); - + accountManager.requestSaveToDisk() + void triggerAsyncQuotaRefreshForAccount( accountManager, account.index, diff --git a/src/plugin/accounts.test.ts b/src/plugin/accounts.test.ts index 24a02a5..2b86a7d 100644 --- a/src/plugin/accounts.test.ts +++ b/src/plugin/accounts.test.ts @@ -1087,7 +1087,7 @@ describe("AccountManager", () => { expect(saveSpy).not.toHaveBeenCalled(); - await vi.advanceTimersByTimeAsync(1500); + await vi.advanceTimersByTimeAsync(5500); expect(saveSpy).toHaveBeenCalledTimes(1); @@ -1112,7 +1112,7 @@ describe("AccountManager", () => { const flushPromise = manager.flushSaveToDisk(); - await vi.advanceTimersByTimeAsync(1500); + await vi.advanceTimersByTimeAsync(5500); await flushPromise; expect(saveSpy).toHaveBeenCalledTimes(1); @@ -1135,16 +1135,43 @@ describe("AccountManager", () => { const saveSpy = vi.spyOn(manager, "saveToDisk").mockResolvedValue(); manager.requestSaveToDisk(); - await vi.advanceTimersByTimeAsync(1500); + await vi.advanceTimersByTimeAsync(5500); expect(saveSpy).toHaveBeenCalledTimes(1); - await vi.advanceTimersByTimeAsync(3000); + await vi.advanceTimersByTimeAsync(6000); expect(saveSpy).toHaveBeenCalledTimes(1); saveSpy.mockRestore(); }); + + it("skips write when account state has not changed", async () => { + vi.useFakeTimers(); + + const stored: AccountStorageV4 = { + version: 4, + accounts: [ + { refreshToken: "r1", projectId: "p1", addedAt: 1, lastUsed: 0 }, + ], + activeIndex: 0, + }; + + const manager = new AccountManager(undefined, stored); + const saveSpy = vi.spyOn(manager, "saveToDisk").mockResolvedValue(); + + // First save — should write + manager.requestSaveToDisk(); + await vi.advanceTimersByTimeAsync(5500); + expect(saveSpy).toHaveBeenCalledTimes(1); + + // Second save with no state change — should skip + manager.requestSaveToDisk(); + await vi.advanceTimersByTimeAsync(5500); + expect(saveSpy).toHaveBeenCalledTimes(1); + + saveSpy.mockRestore(); + }); }); describe("Rate Limit Reason Classification", () => { diff --git a/src/plugin/accounts.ts b/src/plugin/accounts.ts index e86ed5f..303a118 100644 --- a/src/plugin/accounts.ts +++ b/src/plugin/accounts.ts @@ -312,6 +312,7 @@ export class AccountManager { private savePending = false; private saveTimeout: ReturnType | null = null; private savePromiseResolvers: Array<() => void> = []; + private lastSavedSnapshot: string | null = null; static async loadFromDisk(authFallback?: OAuthAuthDetails): Promise { const stored = await loadAccounts(); @@ -973,11 +974,11 @@ export class AccountManager { return [...this.accounts]; } - async saveToDisk(): Promise { + private buildStorageState(): AccountStorageV4 { const claudeIndex = Math.max(0, this.currentAccountIndexByFamily.claude); const geminiIndex = Math.max(0, this.currentAccountIndexByFamily.gemini); - - const storage: AccountStorageV4 = { + + return { version: 4, accounts: this.accounts.map((a) => ({ email: a.email, @@ -1005,9 +1006,11 @@ export class AccountManager { claude: claudeIndex, gemini: geminiIndex, }, - }; + } + } - await saveAccounts(storage); + async saveToDisk(): Promise { + await saveAccounts(this.buildStorageState()); } requestSaveToDisk(): void { @@ -1017,7 +1020,7 @@ export class AccountManager { this.savePending = true; this.saveTimeout = setTimeout(() => { void this.executeSave(); - }, 1000); + }, 5000); } async flushSaveToDisk(): Promise { @@ -1032,9 +1035,13 @@ export class AccountManager { private async executeSave(): Promise { this.savePending = false; this.saveTimeout = null; - + try { - await this.saveToDisk(); + const snapshot = JSON.stringify(this.buildStorageState()); + if (snapshot !== this.lastSavedSnapshot) { + await this.saveToDisk(); + this.lastSavedSnapshot = snapshot; + } } catch { // best-effort persistence; avoid unhandled rejection from timer-driven saves } finally {