Skip to content
This repository was archived by the owner on Mar 30, 2026. It is now read-only.

Commit 35d5149

Browse files
mrm007ampcode-com
andcommitted
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 Co-authored-by: Amp <amp@ampcode.com> Amp-Thread-ID: https://ampcode.com/threads/T-019c6dae-36aa-7524-849b-d8c515ba0cd1
1 parent f7e0c50 commit 35d5149

3 files changed

Lines changed: 59 additions & 29 deletions

File tree

src/plugin.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1699,8 +1699,6 @@ export const createAntigravityPlugin = (providerId: string) => async (
16991699
accountManager.markToastShown(account.index);
17001700
}
17011701

1702-
accountManager.requestSaveToDisk();
1703-
17041702
let authRecord = accountManager.toAuthDetails(account);
17051703

17061704
if (accessTokenExpired(authRecord)) {
@@ -2330,7 +2328,8 @@ export const createAntigravityPlugin = (providerId: string) => async (
23302328
account.consecutiveFailures = 0;
23312329
getHealthTracker().recordSuccess(account.index);
23322330
accountManager.markAccountUsed(account.index);
2333-
2331+
accountManager.requestSaveToDisk();
2332+
23342333
void triggerAsyncQuotaRefreshForAccount(
23352334
accountManager,
23362335
account.index,

src/plugin/accounts.test.ts

Lines changed: 41 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { beforeEach, describe, expect, it, vi } from "vitest";
22

33
import { AccountManager, type ModelFamily, type HeaderStyle, parseRateLimitReason, calculateBackoffMs, type RateLimitReason, resolveQuotaGroup } from "./accounts";
4-
import type { AccountStorageV4 } from "./storage";
4+
import { saveAccounts, type AccountStorageV4 } from "./storage";
55
import type { OAuthAuthDetails } from "./types";
66

77
// Mock storage to prevent test data from leaking to real config files
@@ -1069,6 +1069,8 @@ describe("AccountManager", () => {
10691069
describe("Issue #174: saveToDisk throttling", () => {
10701070
it("requestSaveToDisk coalesces multiple calls into one write", async () => {
10711071
vi.useFakeTimers();
1072+
const saveMock = vi.mocked(saveAccounts);
1073+
saveMock.mockClear();
10721074

10731075
const stored: AccountStorageV4 = {
10741076
version: 4,
@@ -1079,23 +1081,22 @@ describe("AccountManager", () => {
10791081
};
10801082

10811083
const manager = new AccountManager(undefined, stored);
1082-
const saveSpy = vi.spyOn(manager, "saveToDisk").mockResolvedValue();
10831084

10841085
manager.requestSaveToDisk();
10851086
manager.requestSaveToDisk();
10861087
manager.requestSaveToDisk();
10871088

1088-
expect(saveSpy).not.toHaveBeenCalled();
1089+
expect(saveMock).not.toHaveBeenCalled();
10891090

1090-
await vi.advanceTimersByTimeAsync(1500);
1091+
await vi.advanceTimersByTimeAsync(5500);
10911092

1092-
expect(saveSpy).toHaveBeenCalledTimes(1);
1093-
1094-
saveSpy.mockRestore();
1093+
expect(saveMock).toHaveBeenCalledTimes(1);
10951094
});
10961095

10971096
it("flushSaveToDisk waits for pending save to complete", async () => {
10981097
vi.useFakeTimers();
1098+
const saveMock = vi.mocked(saveAccounts);
1099+
saveMock.mockClear();
10991100

11001101
const stored: AccountStorageV4 = {
11011102
version: 4,
@@ -1106,22 +1107,21 @@ describe("AccountManager", () => {
11061107
};
11071108

11081109
const manager = new AccountManager(undefined, stored);
1109-
const saveSpy = vi.spyOn(manager, "saveToDisk").mockResolvedValue();
11101110

11111111
manager.requestSaveToDisk();
11121112

11131113
const flushPromise = manager.flushSaveToDisk();
11141114

1115-
await vi.advanceTimersByTimeAsync(1500);
1115+
await vi.advanceTimersByTimeAsync(5500);
11161116
await flushPromise;
11171117

1118-
expect(saveSpy).toHaveBeenCalledTimes(1);
1119-
1120-
saveSpy.mockRestore();
1118+
expect(saveMock).toHaveBeenCalledTimes(1);
11211119
});
11221120

11231121
it("does not save again if no new requestSaveToDisk after flush", async () => {
11241122
vi.useFakeTimers();
1123+
const saveMock = vi.mocked(saveAccounts);
1124+
saveMock.mockClear();
11251125

11261126
const stored: AccountStorageV4 = {
11271127
version: 4,
@@ -1132,18 +1132,41 @@ describe("AccountManager", () => {
11321132
};
11331133

11341134
const manager = new AccountManager(undefined, stored);
1135-
const saveSpy = vi.spyOn(manager, "saveToDisk").mockResolvedValue();
11361135

11371136
manager.requestSaveToDisk();
1138-
await vi.advanceTimersByTimeAsync(1500);
1137+
await vi.advanceTimersByTimeAsync(5500);
1138+
1139+
expect(saveMock).toHaveBeenCalledTimes(1);
11391140

1140-
expect(saveSpy).toHaveBeenCalledTimes(1);
1141+
await vi.advanceTimersByTimeAsync(6000);
1142+
1143+
expect(saveMock).toHaveBeenCalledTimes(1);
1144+
});
1145+
1146+
it("skips write when account state has not changed", async () => {
1147+
vi.useFakeTimers();
1148+
const saveMock = vi.mocked(saveAccounts);
1149+
saveMock.mockClear();
1150+
1151+
const stored: AccountStorageV4 = {
1152+
version: 4,
1153+
accounts: [
1154+
{ refreshToken: "r1", projectId: "p1", addedAt: 1, lastUsed: 0 },
1155+
],
1156+
activeIndex: 0,
1157+
};
11411158

1142-
await vi.advanceTimersByTimeAsync(3000);
1159+
const manager = new AccountManager(undefined, stored);
11431160

1144-
expect(saveSpy).toHaveBeenCalledTimes(1);
1161+
// First save — should write
1162+
manager.requestSaveToDisk();
1163+
await vi.advanceTimersByTimeAsync(5500);
1164+
expect(saveMock).toHaveBeenCalledTimes(1);
11451165

1146-
saveSpy.mockRestore();
1166+
// Second save with no state change — should skip
1167+
manager.requestSaveToDisk();
1168+
await vi.advanceTimersByTimeAsync(5500);
1169+
expect(saveMock).toHaveBeenCalledTimes(1);
11471170
});
11481171
});
11491172

src/plugin/accounts.ts

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,7 @@ export class AccountManager {
312312
private savePending = false;
313313
private saveTimeout: ReturnType<typeof setTimeout> | null = null;
314314
private savePromiseResolvers: Array<() => void> = [];
315+
private lastSavedSnapshot: string | null = null;
315316

316317
static async loadFromDisk(authFallback?: OAuthAuthDetails): Promise<AccountManager> {
317318
const stored = await loadAccounts();
@@ -973,11 +974,11 @@ export class AccountManager {
973974
return [...this.accounts];
974975
}
975976

976-
async saveToDisk(): Promise<void> {
977+
private buildStorageState(): AccountStorageV4 {
977978
const claudeIndex = Math.max(0, this.currentAccountIndexByFamily.claude);
978979
const geminiIndex = Math.max(0, this.currentAccountIndexByFamily.gemini);
979-
980-
const storage: AccountStorageV4 = {
980+
981+
return {
981982
version: 4,
982983
accounts: this.accounts.map((a) => ({
983984
email: a.email,
@@ -1005,9 +1006,11 @@ export class AccountManager {
10051006
claude: claudeIndex,
10061007
gemini: geminiIndex,
10071008
},
1008-
};
1009+
}
1010+
}
10091011

1010-
await saveAccounts(storage);
1012+
async saveToDisk(): Promise<void> {
1013+
await saveAccounts(this.buildStorageState());
10111014
}
10121015

10131016
requestSaveToDisk(): void {
@@ -1017,7 +1020,7 @@ export class AccountManager {
10171020
this.savePending = true;
10181021
this.saveTimeout = setTimeout(() => {
10191022
void this.executeSave();
1020-
}, 1000);
1023+
}, 5000);
10211024
}
10221025

10231026
async flushSaveToDisk(): Promise<void> {
@@ -1032,9 +1035,14 @@ export class AccountManager {
10321035
private async executeSave(): Promise<void> {
10331036
this.savePending = false;
10341037
this.saveTimeout = null;
1035-
1038+
10361039
try {
1037-
await this.saveToDisk();
1040+
const state = this.buildStorageState();
1041+
const snapshot = JSON.stringify(state);
1042+
if (snapshot !== this.lastSavedSnapshot) {
1043+
await saveAccounts(state);
1044+
this.lastSavedSnapshot = snapshot;
1045+
}
10381046
} catch {
10391047
// best-effort persistence; avoid unhandled rejection from timer-driven saves
10401048
} finally {

0 commit comments

Comments
 (0)