Skip to content

Commit cfcc34f

Browse files
CodeDrift Testmcaxtr
authored andcommitted
perf: consolidate auth profile success writes
1 parent 6110d87 commit cfcc34f

11 files changed

Lines changed: 158 additions & 170 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ Docs: https://docs.openclaw.ai
3636
### Fixes
3737

3838
- Gateway: avoid synchronous restart-sentinel state probes during post-attach startup, preventing slow Windows or redirected state directories from blocking channel turns. Fixes #79264. Thanks @liyi58.
39+
- Agents/auth: update successful model auth profile status with one locked store write, reducing post-model reply latency from duplicate `auth-profiles.json` saves. Thanks @mcaxtr.
3940
- Agents/image: honor explicit `image` tool model overrides even when `agents.defaults.imageModel` is unset, restoring one-off vision calls for configured multimodal providers. Fixes #79341. Thanks @haumanto.
4041
- Codex app-server: preserve prompt-local current-turn context through context-engine prompt projection, so replied-to Telegram messages stay visible to the Codex model input.
4142
- Telegram: pass agent-scoped media roots through gateway message actions so workspace-local media from the active agent is not rejected as cross-agent access. Thanks @frankekn.

src/agents/auth-profiles.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ export {
2828
export {
2929
dedupeProfileIds,
3030
listProfilesForProvider,
31-
markAuthProfileGood,
31+
markAuthProfileSuccess,
3232
setAuthProfileOrder,
3333
upsertAuthProfile,
3434
upsertAuthProfileWithLock,
@@ -77,7 +77,6 @@ export {
7777
isProfileInCooldown,
7878
markAuthProfileCooldown,
7979
markAuthProfileFailure,
80-
markAuthProfileUsed,
8180
resolveProfilesUnavailableReason,
8281
resolveProfileUnusableUntilForDisplay,
8382
} from "./auth-profiles/usage.js";

src/agents/auth-profiles/order.test.ts

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,11 @@ async function importAuthProfileModulesWithAliasRegistry() {
3131
vi.doMock("../../plugins/manifest-registry.js", () => ({
3232
loadPluginManifestRegistry,
3333
}));
34-
const [{ resolveAuthProfileOrder }, { markAuthProfileGood }] = await Promise.all([
34+
const [{ resolveAuthProfileOrder }, { markAuthProfileSuccess }] = await Promise.all([
3535
import("./order.js"),
3636
import("./profiles.js"),
3737
]);
38-
return { markAuthProfileGood, resolveAuthProfileOrder };
38+
return { markAuthProfileSuccess, resolveAuthProfileOrder };
3939
}
4040

4141
describe("resolveAuthProfileOrder", () => {
@@ -221,23 +221,32 @@ describe("resolveAuthProfileOrder", () => {
221221
expect(order).toStrictEqual([]);
222222
});
223223

224-
it("marks aliased provider profiles good under the canonical auth provider", async () => {
225-
const { markAuthProfileGood } = await importAuthProfileModulesWithAliasRegistry();
226-
const agentDir = await mkdtemp(path.join(os.tmpdir(), "openclaw-auth-profile-alias-"));
224+
it("marks profile success with one canonical last-good and usage update", async () => {
225+
const { markAuthProfileSuccess } = await importAuthProfileModulesWithAliasRegistry();
226+
const agentDir = await mkdtemp(path.join(os.tmpdir(), "openclaw-auth-profile-success-"));
227227
try {
228228
const store: AuthProfileStore = {
229229
version: 1,
230230
profiles: {
231231
"fixture-provider:default": {
232-
type: "api_key",
232+
type: "oauth",
233233
provider: "fixture-provider",
234-
key: "sk-test",
234+
access: "token",
235+
refresh: "refresh",
236+
expires: Date.now() + 60_000,
237+
},
238+
},
239+
usageStats: {
240+
"fixture-provider:default": {
241+
errorCount: 3,
242+
cooldownUntil: Date.now() + 60_000,
243+
cooldownReason: "rate_limit",
235244
},
236245
},
237246
};
238247
saveAuthProfileStore(store, agentDir);
239248

240-
await markAuthProfileGood({
249+
await markAuthProfileSuccess({
241250
store,
242251
provider: "fixture-provider-plan",
243252
profileId: "fixture-provider:default",
@@ -247,6 +256,12 @@ describe("resolveAuthProfileOrder", () => {
247256
expect(store.lastGood).toEqual({
248257
"fixture-provider": "fixture-provider:default",
249258
});
259+
expect(store.usageStats?.["fixture-provider:default"]).toMatchObject({
260+
errorCount: 0,
261+
cooldownUntil: undefined,
262+
cooldownReason: undefined,
263+
});
264+
expect(store.usageStats?.["fixture-provider:default"]?.lastUsed).toEqual(expect.any(Number));
250265
} finally {
251266
await rm(agentDir, { force: true, recursive: true });
252267
}
Lines changed: 96 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,56 +1,98 @@
1-
import fs from "node:fs";
2-
import os from "node:os";
3-
import path from "node:path";
4-
import { describe, expect, it } from "vitest";
5-
import { AUTH_STORE_VERSION } from "./constants.js";
6-
import { promoteAuthProfileInOrder } from "./profiles.js";
7-
import { loadAuthProfileStoreForRuntime, saveAuthProfileStore } from "./store.js";
8-
9-
describe("promoteAuthProfileInOrder", () => {
10-
it("moves a relogin profile to the front of an existing per-agent provider order", async () => {
11-
const agentDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-auth-order-promote-"));
12-
try {
13-
const newProfileId = "openai-codex:[email protected]";
14-
const staleProfileId = "openai-codex:[email protected]";
15-
saveAuthProfileStore(
16-
{
17-
version: AUTH_STORE_VERSION,
18-
profiles: {
19-
[newProfileId]: {
20-
type: "oauth",
21-
provider: "openai-codex",
22-
access: "new-access",
23-
refresh: "new-refresh",
24-
expires: Date.now() + 60 * 60 * 1000,
25-
},
26-
[staleProfileId]: {
27-
type: "oauth",
28-
provider: "openai-codex",
29-
access: "stale-access",
30-
refresh: "stale-refresh",
31-
expires: Date.now() + 30 * 60 * 1000,
32-
},
33-
},
34-
order: {
35-
"openai-codex": [staleProfileId],
36-
},
37-
},
38-
agentDir,
39-
);
40-
41-
const updated = await promoteAuthProfileInOrder({
42-
agentDir,
43-
provider: "openai-codex",
44-
profileId: newProfileId,
45-
});
46-
47-
expect(updated?.order?.["openai-codex"]).toEqual([newProfileId, staleProfileId]);
48-
expect(loadAuthProfileStoreForRuntime(agentDir).order?.["openai-codex"]).toEqual([
49-
newProfileId,
50-
staleProfileId,
51-
]);
52-
} finally {
53-
fs.rmSync(agentDir, { recursive: true, force: true });
54-
}
1+
import { beforeEach, describe, expect, it, vi } from "vitest";
2+
import { markAuthProfileSuccess } from "./profiles.js";
3+
import type { AuthProfileStore } from "./types.js";
4+
5+
const storeMocks = vi.hoisted(() => ({
6+
saveAuthProfileStore: vi.fn(),
7+
updateAuthProfileStoreWithLock: vi.fn().mockResolvedValue(null),
8+
}));
9+
10+
vi.mock("./store.js", () => ({
11+
ensureAuthProfileStoreForLocalUpdate: vi.fn(() => ({ version: 1, profiles: {} })),
12+
saveAuthProfileStore: storeMocks.saveAuthProfileStore,
13+
updateAuthProfileStoreWithLock: storeMocks.updateAuthProfileStoreWithLock,
14+
}));
15+
16+
beforeEach(() => {
17+
vi.clearAllMocks();
18+
storeMocks.updateAuthProfileStoreWithLock.mockResolvedValue(null);
19+
});
20+
21+
function makeStore(usageStats: AuthProfileStore["usageStats"]): AuthProfileStore {
22+
return {
23+
version: 1,
24+
profiles: {
25+
"anthropic:default": {
26+
type: "api_key",
27+
provider: "anthropic",
28+
key: "sk-test",
29+
},
30+
},
31+
usageStats,
32+
};
33+
}
34+
35+
describe("markAuthProfileSuccess", () => {
36+
it("updates last-good and usage stats through the fallback save path when lock update misses", async () => {
37+
const store = makeStore({
38+
"anthropic:default": {
39+
errorCount: 3,
40+
cooldownUntil: Date.now() + 60_000,
41+
cooldownReason: "rate_limit",
42+
},
43+
});
44+
45+
storeMocks.updateAuthProfileStoreWithLock.mockResolvedValue(null);
46+
47+
const beforeUsed = Date.now();
48+
await markAuthProfileSuccess({
49+
store,
50+
provider: "anthropic",
51+
profileId: "anthropic:default",
52+
agentDir: "/tmp/openclaw-auth-profiles-success",
53+
});
54+
55+
expect(storeMocks.saveAuthProfileStore).toHaveBeenCalledWith(
56+
store,
57+
"/tmp/openclaw-auth-profiles-success",
58+
);
59+
expect(store.lastGood).toEqual({ anthropic: "anthropic:default" });
60+
expect(store.usageStats?.["anthropic:default"]).toMatchObject({
61+
errorCount: 0,
62+
cooldownUntil: undefined,
63+
cooldownReason: undefined,
64+
});
65+
expect(store.usageStats?.["anthropic:default"]?.lastUsed).toBeGreaterThanOrEqual(beforeUsed);
66+
});
67+
68+
it("adopts locked store last-good and usage stats without saving locally when lock update succeeds", async () => {
69+
const store = makeStore({
70+
"anthropic:default": {
71+
errorCount: 3,
72+
cooldownUntil: Date.now() + 60_000,
73+
},
74+
});
75+
const lockedStore = makeStore(undefined);
76+
77+
storeMocks.updateAuthProfileStoreWithLock.mockImplementationOnce(async ({ updater }) => {
78+
updater(lockedStore);
79+
return lockedStore;
80+
});
81+
82+
await markAuthProfileSuccess({
83+
store,
84+
provider: "anthropic",
85+
profileId: "anthropic:default",
86+
agentDir: "/tmp/openclaw-auth-profiles-success",
87+
});
88+
89+
expect(storeMocks.saveAuthProfileStore).not.toHaveBeenCalled();
90+
expect(store.lastGood).toEqual({ anthropic: "anthropic:default" });
91+
expect(store.usageStats).toEqual(lockedStore.usageStats);
92+
expect(store.usageStats?.["anthropic:default"]).toMatchObject({
93+
errorCount: 0,
94+
cooldownUntil: undefined,
95+
});
96+
expect(store.usageStats?.["anthropic:default"]?.lastUsed).toEqual(expect.any(Number));
5597
});
5698
});

src/agents/auth-profiles/profiles.ts

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,35 @@ import {
88
saveAuthProfileStore,
99
updateAuthProfileStoreWithLock,
1010
} from "./store.js";
11-
import type { AuthProfileCredential, AuthProfileStore } from "./types.js";
11+
import type { AuthProfileCredential, AuthProfileStore, ProfileUsageStats } from "./types.js";
1212
export { dedupeProfileIds, listProfilesForProvider } from "./profile-list.js";
1313

14+
function resetSuccessfulUsageStats(
15+
existing: ProfileUsageStats | undefined,
16+
lastUsed: number,
17+
): ProfileUsageStats {
18+
return {
19+
...existing,
20+
errorCount: 0,
21+
cooldownUntil: undefined,
22+
cooldownReason: undefined,
23+
cooldownModel: undefined,
24+
disabledUntil: undefined,
25+
disabledReason: undefined,
26+
failureCounts: undefined,
27+
lastUsed,
28+
};
29+
}
30+
31+
function updateSuccessfulUsageStatsEntry(
32+
store: AuthProfileStore,
33+
profileId: string,
34+
lastUsed: number,
35+
): void {
36+
store.usageStats = store.usageStats ?? {};
37+
store.usageStats[profileId] = resetSuccessfulUsageStats(store.usageStats[profileId], lastUsed);
38+
}
39+
1440
export async function setAuthProfileOrder(params: {
1541
agentDir?: string;
1642
provider: string;
@@ -157,14 +183,15 @@ export async function removeProviderAuthProfilesWithLock(params: {
157183
});
158184
}
159185

160-
export async function markAuthProfileGood(params: {
186+
export async function markAuthProfileSuccess(params: {
161187
store: AuthProfileStore;
162188
provider: string;
163189
profileId: string;
164190
agentDir?: string;
165191
}): Promise<void> {
166192
const { store, provider, profileId, agentDir } = params;
167193
const providerKey = resolveProviderIdForAuth(provider);
194+
const lastUsed = Date.now();
168195
const updated = await updateAuthProfileStoreWithLock({
169196
agentDir,
170197
updater: (freshStore) => {
@@ -173,17 +200,20 @@ export async function markAuthProfileGood(params: {
173200
return false;
174201
}
175202
freshStore.lastGood = { ...freshStore.lastGood, [providerKey]: profileId };
203+
updateSuccessfulUsageStatsEntry(freshStore, profileId, lastUsed);
176204
return true;
177205
},
178206
});
179207
if (updated) {
180208
store.lastGood = updated.lastGood;
209+
store.usageStats = updated.usageStats;
181210
return;
182211
}
183212
const profile = store.profiles[profileId];
184213
if (!profile || resolveProviderIdForAuth(profile.provider) !== providerKey) {
185214
return;
186215
}
187216
store.lastGood = { ...store.lastGood, [providerKey]: profileId };
217+
updateSuccessfulUsageStatsEntry(store, profileId, lastUsed);
188218
saveAuthProfileStore(store, agentDir);
189219
}

src/agents/auth-profiles/usage-state.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ export function getSoonestCooldownExpiry(
125125
* has both and only one has expired, only that field is cleared.
126126
*
127127
* Mutates the in-memory store; disk persistence happens lazily on the next
128-
* store write (e.g. `markAuthProfileUsed` / `markAuthProfileFailure`), which
128+
* store write (e.g. `markAuthProfileSuccess` / `markAuthProfileFailure`), which
129129
* matches the existing save pattern throughout the auth-profiles module.
130130
*
131131
* @returns `true` if any profile was modified.

src/agents/auth-profiles/usage.test.ts

Lines changed: 0 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import {
66
clearExpiredCooldowns,
77
isProfileInCooldown,
88
markAuthProfileFailure,
9-
markAuthProfileUsed,
109
resolveProfilesUnavailableReason,
1110
resolveProfileUnusableUntil,
1211
resolveProfileUnusableUntilForDisplay,
@@ -600,60 +599,6 @@ describe("clearAuthProfileCooldown", () => {
600599
});
601600
});
602601

603-
describe("markAuthProfileUsed", () => {
604-
it("updates usage stats and persists through the fallback save path when lock update misses", async () => {
605-
const store = makeStore({
606-
"anthropic:default": {
607-
errorCount: 3,
608-
cooldownUntil: Date.now() + 60_000,
609-
},
610-
});
611-
612-
storeMocks.updateAuthProfileStoreWithLock.mockResolvedValue(null);
613-
614-
const beforeUsed = Date.now();
615-
await markAuthProfileUsed({
616-
store,
617-
profileId: "anthropic:default",
618-
agentDir: "/tmp/openclaw-auth-profiles-used",
619-
});
620-
621-
expect(storeMocks.saveAuthProfileStore).toHaveBeenCalledWith(
622-
store,
623-
"/tmp/openclaw-auth-profiles-used",
624-
);
625-
expect(store.usageStats?.["anthropic:default"]?.errorCount).toBe(0);
626-
expect(store.usageStats?.["anthropic:default"]?.cooldownUntil).toBeUndefined();
627-
expect(store.usageStats?.["anthropic:default"]?.lastUsed).toBeGreaterThanOrEqual(beforeUsed);
628-
});
629-
630-
it("adopts locked store usage stats without saving locally when lock update succeeds", async () => {
631-
const store = makeStore({
632-
"anthropic:default": {
633-
errorCount: 3,
634-
cooldownUntil: Date.now() + 60_000,
635-
},
636-
});
637-
const lockedStore = makeStore({
638-
"anthropic:default": {
639-
lastUsed: 123_456,
640-
errorCount: 0,
641-
},
642-
});
643-
644-
storeMocks.updateAuthProfileStoreWithLock.mockResolvedValue(lockedStore);
645-
646-
await markAuthProfileUsed({
647-
store,
648-
profileId: "anthropic:default",
649-
agentDir: "/tmp/openclaw-auth-profiles-used",
650-
});
651-
652-
expect(storeMocks.saveAuthProfileStore).not.toHaveBeenCalled();
653-
expect(store.usageStats).toEqual(lockedStore.usageStats);
654-
});
655-
});
656-
657602
describe("markAuthProfileFailure — active windows do not extend on retry", () => {
658603
// Regression for https://github.com/openclaw/openclaw/issues/23516
659604
// When all providers are at saturation backoff (60 min) and retries fire every 30 min,

0 commit comments

Comments
 (0)