fix(auth): serialize profile stats updates

This commit is contained in:
Peter Steinberger
2026-01-07 01:06:51 +01:00
parent 96d72ff91e
commit 19c95d0ff7
2 changed files with 24 additions and 25 deletions

View File

@@ -107,16 +107,16 @@ function syncAuthProfileStore(
target.usageStats = source.usageStats; target.usageStats = source.usageStats;
} }
function updateAuthProfileStoreWithLock(params: { async function updateAuthProfileStoreWithLock(params: {
agentDir?: string; agentDir?: string;
updater: (store: AuthProfileStore) => boolean; updater: (store: AuthProfileStore) => boolean;
}): AuthProfileStore | null { }): Promise<AuthProfileStore | null> {
const authPath = resolveAuthStorePath(params.agentDir); const authPath = resolveAuthStorePath(params.agentDir);
ensureAuthStoreFile(authPath); ensureAuthStoreFile(authPath);
let release: (() => void) | undefined; let release: (() => Promise<void>) | undefined;
try { try {
release = lockfile.lockSync(authPath, AUTH_STORE_LOCK_OPTIONS); release = await lockfile.lock(authPath, AUTH_STORE_LOCK_OPTIONS);
const store = ensureAuthProfileStore(params.agentDir); const store = ensureAuthProfileStore(params.agentDir);
const shouldSave = params.updater(store); const shouldSave = params.updater(store);
if (shouldSave) { if (shouldSave) {
@@ -128,16 +128,12 @@ function updateAuthProfileStoreWithLock(params: {
} finally { } finally {
if (release) { if (release) {
try { try {
release(); await release();
} catch {
try {
lockfile.unlockSync(authPath);
} catch { } catch {
// ignore unlock errors // ignore unlock errors
} }
} }
} }
}
} }
function buildOAuthApiKey( function buildOAuthApiKey(
@@ -403,13 +399,13 @@ export function isProfileInCooldown(
* Mark a profile as successfully used. Resets error count and updates lastUsed. * Mark a profile as successfully used. Resets error count and updates lastUsed.
* Uses store lock to avoid overwriting concurrent usage updates. * Uses store lock to avoid overwriting concurrent usage updates.
*/ */
export function markAuthProfileUsed(params: { export async function markAuthProfileUsed(params: {
store: AuthProfileStore; store: AuthProfileStore;
profileId: string; profileId: string;
agentDir?: string; agentDir?: string;
}): void { }): Promise<void> {
const { store, profileId, agentDir } = params; const { store, profileId, agentDir } = params;
const updated = updateAuthProfileStoreWithLock({ const updated = await updateAuthProfileStoreWithLock({
agentDir, agentDir,
updater: (freshStore) => { updater: (freshStore) => {
if (!freshStore.profiles[profileId]) return false; if (!freshStore.profiles[profileId]) return false;
@@ -452,13 +448,13 @@ export function calculateAuthProfileCooldownMs(errorCount: number): number {
* Cooldown times: 1min, 5min, 25min, max 1 hour. * Cooldown times: 1min, 5min, 25min, max 1 hour.
* Uses store lock to avoid overwriting concurrent usage updates. * Uses store lock to avoid overwriting concurrent usage updates.
*/ */
export function markAuthProfileCooldown(params: { export async function markAuthProfileCooldown(params: {
store: AuthProfileStore; store: AuthProfileStore;
profileId: string; profileId: string;
agentDir?: string; agentDir?: string;
}): void { }): Promise<void> {
const { store, profileId, agentDir } = params; const { store, profileId, agentDir } = params;
const updated = updateAuthProfileStoreWithLock({ const updated = await updateAuthProfileStoreWithLock({
agentDir, agentDir,
updater: (freshStore) => { updater: (freshStore) => {
if (!freshStore.profiles[profileId]) return false; if (!freshStore.profiles[profileId]) return false;
@@ -503,13 +499,13 @@ export function markAuthProfileCooldown(params: {
* Clear cooldown for a profile (e.g., manual reset). * Clear cooldown for a profile (e.g., manual reset).
* Uses store lock to avoid overwriting concurrent usage updates. * Uses store lock to avoid overwriting concurrent usage updates.
*/ */
export function clearAuthProfileCooldown(params: { export async function clearAuthProfileCooldown(params: {
store: AuthProfileStore; store: AuthProfileStore;
profileId: string; profileId: string;
agentDir?: string; agentDir?: string;
}): void { }): Promise<void> {
const { store, profileId, agentDir } = params; const { store, profileId, agentDir } = params;
const updated = updateAuthProfileStoreWithLock({ const updated = await updateAuthProfileStoreWithLock({
agentDir, agentDir,
updater: (freshStore) => { updater: (freshStore) => {
if (!freshStore.usageStats?.[profileId]) return false; if (!freshStore.usageStats?.[profileId]) return false;
@@ -693,14 +689,14 @@ export async function resolveApiKeyForProfile(params: {
} }
} }
export function markAuthProfileGood(params: { export async function markAuthProfileGood(params: {
store: AuthProfileStore; store: AuthProfileStore;
provider: string; provider: string;
profileId: string; profileId: string;
agentDir?: string; agentDir?: string;
}): void { }): Promise<void> {
const { store, provider, profileId, agentDir } = params; const { store, provider, profileId, agentDir } = params;
const updated = updateAuthProfileStoreWithLock({ const updated = await updateAuthProfileStoreWithLock({
agentDir, agentDir,
updater: (freshStore) => { updater: (freshStore) => {
const profile = freshStore.profiles[profileId]; const profile = freshStore.profiles[profileId];

View File

@@ -1000,7 +1000,7 @@ export async function runEmbeddedPiAgent(params: {
if (shouldRotate) { if (shouldRotate) {
// Mark current profile for cooldown before rotating // Mark current profile for cooldown before rotating
if (lastProfileId) { if (lastProfileId) {
markAuthProfileCooldown({ await markAuthProfileCooldown({
store: authStore, store: authStore,
profileId: lastProfileId, profileId: lastProfileId,
}); });
@@ -1091,13 +1091,16 @@ export async function runEmbeddedPiAgent(params: {
`embedded run done: runId=${params.runId} sessionId=${params.sessionId} durationMs=${Date.now() - started} aborted=${aborted}`, `embedded run done: runId=${params.runId} sessionId=${params.sessionId} durationMs=${Date.now() - started} aborted=${aborted}`,
); );
if (lastProfileId) { if (lastProfileId) {
markAuthProfileGood({ await markAuthProfileGood({
store: authStore, store: authStore,
provider, provider,
profileId: lastProfileId, profileId: lastProfileId,
}); });
// Track usage for round-robin rotation // Track usage for round-robin rotation
markAuthProfileUsed({ store: authStore, profileId: lastProfileId }); await markAuthProfileUsed({
store: authStore,
profileId: lastProfileId,
});
} }
return { return {
payloads: payloads.length ? payloads : undefined, payloads: payloads.length ? payloads : undefined,