perf(cron): keep auth profile runtime cold

This commit is contained in:
Vincent Koc
2026-04-13 16:30:13 +01:00
parent 6a8704cf26
commit 2bc031c357
8 changed files with 209 additions and 76 deletions

View File

@@ -0,0 +1,58 @@
import { resolveAuthStorePath } from "./paths.js";
import type { AuthProfileStore } from "./types.js";
const runtimeAuthStoreSnapshots = new Map<string, AuthProfileStore>();
function resolveRuntimeStoreKey(agentDir?: string): string {
return resolveAuthStorePath(agentDir);
}
function cloneAuthProfileStore(store: AuthProfileStore): AuthProfileStore {
return structuredClone(store);
}
export function getRuntimeAuthProfileStoreSnapshot(
agentDir?: string,
): AuthProfileStore | undefined {
const store = runtimeAuthStoreSnapshots.get(resolveRuntimeStoreKey(agentDir));
return store ? cloneAuthProfileStore(store) : undefined;
}
export function hasRuntimeAuthProfileStoreSnapshot(agentDir?: string): boolean {
return runtimeAuthStoreSnapshots.has(resolveRuntimeStoreKey(agentDir));
}
export function hasAnyRuntimeAuthProfileStoreSource(agentDir?: string): boolean {
const requestedStore = getRuntimeAuthProfileStoreSnapshot(agentDir);
if (requestedStore && Object.keys(requestedStore.profiles).length > 0) {
return true;
}
if (!agentDir) {
return false;
}
const mainStore = getRuntimeAuthProfileStoreSnapshot();
return Boolean(mainStore && Object.keys(mainStore.profiles).length > 0);
}
export function replaceRuntimeAuthProfileStoreSnapshots(
entries: Array<{ agentDir?: string; store: AuthProfileStore }>,
): void {
runtimeAuthStoreSnapshots.clear();
for (const entry of entries) {
runtimeAuthStoreSnapshots.set(
resolveRuntimeStoreKey(entry.agentDir),
cloneAuthProfileStore(entry.store),
);
}
}
export function clearRuntimeAuthProfileStoreSnapshots(): void {
runtimeAuthStoreSnapshots.clear();
}
export function setRuntimeAuthProfileStoreSnapshot(
store: AuthProfileStore,
agentDir?: string,
): void {
runtimeAuthStoreSnapshots.set(resolveRuntimeStoreKey(agentDir), cloneAuthProfileStore(store));
}

View File

@@ -0,0 +1,27 @@
import fs from "node:fs";
import { resolveAuthStatePath, resolveAuthStorePath, resolveLegacyAuthStorePath } from "./paths.js";
import { hasAnyRuntimeAuthProfileStoreSource } from "./runtime-snapshots.js";
function hasStoredAuthProfileFiles(agentDir?: string): boolean {
return (
fs.existsSync(resolveAuthStorePath(agentDir)) ||
fs.existsSync(resolveAuthStatePath(agentDir)) ||
fs.existsSync(resolveLegacyAuthStorePath(agentDir))
);
}
export function hasAnyAuthProfileStoreSource(agentDir?: string): boolean {
if (hasAnyRuntimeAuthProfileStoreSource(agentDir)) {
return true;
}
if (hasStoredAuthProfileFiles(agentDir)) {
return true;
}
const authPath = resolveAuthStorePath(agentDir);
const mainAuthPath = resolveAuthStorePath();
if (agentDir && authPath !== mainAuthPath && hasStoredAuthProfileFiles(undefined)) {
return true;
}
return false;
}

View File

@@ -23,6 +23,13 @@ import {
mergeAuthProfileStores,
mergeOAuthFileIntoStore,
} from "./persisted.js";
import {
clearRuntimeAuthProfileStoreSnapshots as clearRuntimeAuthProfileStoreSnapshotsImpl,
getRuntimeAuthProfileStoreSnapshot,
hasRuntimeAuthProfileStoreSnapshot,
replaceRuntimeAuthProfileStoreSnapshots as replaceRuntimeAuthProfileStoreSnapshotsImpl,
setRuntimeAuthProfileStoreSnapshot,
} from "./runtime-snapshots.js";
import { savePersistedAuthProfileState } from "./state.js";
import type { AuthProfileStore } from "./types.js";
@@ -37,7 +44,6 @@ type SaveAuthProfileStoreOptions = {
syncExternalCli?: boolean;
};
const runtimeAuthStoreSnapshots = new Map<string, AuthProfileStore>();
const loadedAuthStoreCache = new Map<
string,
{
@@ -48,72 +54,36 @@ const loadedAuthStoreCache = new Map<
}
>();
function resolveRuntimeStoreKey(agentDir?: string): string {
return resolveAuthStorePath(agentDir);
}
function cloneAuthProfileStore(store: AuthProfileStore): AuthProfileStore {
return structuredClone(store);
}
function resolveRuntimeAuthProfileStore(agentDir?: string): AuthProfileStore | null {
if (runtimeAuthStoreSnapshots.size === 0) {
return null;
}
const mainKey = resolveRuntimeStoreKey(undefined);
const requestedKey = resolveRuntimeStoreKey(agentDir);
const mainStore = runtimeAuthStoreSnapshots.get(mainKey);
const requestedStore = runtimeAuthStoreSnapshots.get(requestedKey);
const mainKey = resolveAuthStorePath(undefined);
const requestedKey = resolveAuthStorePath(agentDir);
const mainStore = getRuntimeAuthProfileStoreSnapshot(undefined);
const requestedStore = getRuntimeAuthProfileStoreSnapshot(agentDir);
if (!agentDir || requestedKey === mainKey) {
if (!mainStore) {
return null;
}
return cloneAuthProfileStore(mainStore);
return mainStore;
}
if (mainStore && requestedStore) {
return mergeAuthProfileStores(
cloneAuthProfileStore(mainStore),
cloneAuthProfileStore(requestedStore),
);
return mergeAuthProfileStores(mainStore, requestedStore);
}
if (requestedStore) {
return cloneAuthProfileStore(requestedStore);
return requestedStore;
}
if (mainStore) {
return cloneAuthProfileStore(mainStore);
return mainStore;
}
return null;
}
function hasStoredAuthProfileFiles(agentDir?: string): boolean {
return (
fs.existsSync(resolveAuthStorePath(agentDir)) ||
fs.existsSync(resolveAuthStatePath(agentDir)) ||
fs.existsSync(resolveLegacyAuthStorePath(agentDir))
);
}
export function replaceRuntimeAuthProfileStoreSnapshots(
entries: Array<{ agentDir?: string; store: AuthProfileStore }>,
): void {
runtimeAuthStoreSnapshots.clear();
for (const entry of entries) {
runtimeAuthStoreSnapshots.set(
resolveRuntimeStoreKey(entry.agentDir),
cloneAuthProfileStore(entry.store),
);
}
}
export function clearRuntimeAuthProfileStoreSnapshots(): void {
runtimeAuthStoreSnapshots.clear();
loadedAuthStoreCache.clear();
}
function readAuthStoreMtimeMs(authPath: string): number | null {
try {
return fs.statSync(authPath).mtimeMs;
@@ -387,23 +357,17 @@ export function ensureAuthProfileStoreForLocalUpdate(agentDir?: string): AuthPro
return mergeAuthProfileStores(mainStore, store);
}
export function hasAnyAuthProfileStoreSource(agentDir?: string): boolean {
const runtimeStore = resolveRuntimeAuthProfileStore(agentDir);
if (runtimeStore && Object.keys(runtimeStore.profiles).length > 0) {
return true;
}
export { hasAnyAuthProfileStoreSource } from "./source-check.js";
if (hasStoredAuthProfileFiles(agentDir)) {
return true;
}
export function replaceRuntimeAuthProfileStoreSnapshots(
entries: Array<{ agentDir?: string; store: AuthProfileStore }>,
): void {
replaceRuntimeAuthProfileStoreSnapshotsImpl(entries);
}
const authPath = resolveAuthStorePath(agentDir);
const mainAuthPath = resolveAuthStorePath();
if (agentDir && authPath !== mainAuthPath && hasStoredAuthProfileFiles(undefined)) {
return true;
}
return false;
export function clearRuntimeAuthProfileStoreSnapshots(): void {
clearRuntimeAuthProfileStoreSnapshotsImpl();
loadedAuthStoreCache.clear();
}
export function saveAuthProfileStore(
@@ -413,7 +377,6 @@ export function saveAuthProfileStore(
): void {
const authPath = resolveAuthStorePath(agentDir);
const statePath = resolveAuthStatePath(agentDir);
const runtimeKey = resolveRuntimeStoreKey(agentDir);
const payload = buildPersistedAuthProfileSecretsStore(store, ({ profileId, credential }) => {
if (credential.type !== "oauth") {
return true;
@@ -440,7 +403,7 @@ export function saveAuthProfileStore(
stateMtimeMs: readAuthStoreMtimeMs(statePath),
store: runtimeStore,
});
if (runtimeAuthStoreSnapshots.has(runtimeKey)) {
runtimeAuthStoreSnapshots.set(runtimeKey, cloneAuthProfileStore(runtimeStore));
if (hasRuntimeAuthProfileStoreSnapshot(agentDir)) {
setRuntimeAuthProfileStoreSnapshot(runtimeStore, agentDir);
}
}

View File

@@ -0,0 +1 @@
export { resolveSessionAuthProfileOverride } from "../../agents/auth-profiles/session-override.js";

View File

@@ -0,0 +1,57 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
const hasAnyAuthProfileStoreSourceMock = vi.fn(() => false);
vi.mock("../../agents/auth-profiles/source-check.js", () => ({
hasAnyAuthProfileStoreSource: hasAnyAuthProfileStoreSourceMock,
}));
import {
clearFastTestEnv,
loadRunCronIsolatedAgentTurn,
resolveSessionAuthProfileOverrideMock,
resetRunCronIsolatedAgentTurnHarness,
restoreFastTestEnv,
} from "./run.test-harness.js";
const runCronIsolatedAgentTurn = await loadRunCronIsolatedAgentTurn();
function makeParams(overrides?: Record<string, unknown>) {
return {
cfg: {},
deps: {} as never,
job: {
id: "cron-auth-cold-path",
name: "Auth Cold Path",
schedule: { kind: "cron", expr: "0 * * * *", tz: "UTC" },
sessionTarget: "isolated",
payload: { kind: "agentTurn", message: "run task" },
},
message: "run task",
sessionKey: "cron:auth-cold-path",
...overrides,
};
}
describe("runCronIsolatedAgentTurn auth-profile cold path", () => {
let previousFastTestEnv: string | undefined;
beforeEach(() => {
previousFastTestEnv = clearFastTestEnv();
resetRunCronIsolatedAgentTurnHarness();
hasAnyAuthProfileStoreSourceMock.mockReset();
hasAnyAuthProfileStoreSourceMock.mockReturnValue(false);
});
afterEach(() => {
restoreFastTestEnv(previousFastTestEnv);
});
it("skips auth-profile override resolution when no sources exist", async () => {
const result = await runCronIsolatedAgentTurn(makeParams());
expect(result.status).toBe("ok");
expect(hasAnyAuthProfileStoreSourceMock).toHaveBeenCalledTimes(1);
expect(resolveSessionAuthProfileOverrideMock).not.toHaveBeenCalled();
});
});

View File

@@ -6,7 +6,6 @@ export {
resolveDefaultAgentId,
resolveAgentSkillsFilter,
} from "../../agents/agent-scope.js";
export { resolveSessionAuthProfileOverride } from "../../agents/auth-profiles/session-override.js";
export { setCliSessionId } from "../../agents/cli-session.js";
export { lookupContextTokens } from "../../agents/context.js";
export { resolveCronStyleNow } from "../../agents/current-time.js";

View File

@@ -98,7 +98,6 @@ vi.mock("./run.runtime.js", () => ({
resolveAgentWorkspaceDir: vi.fn().mockReturnValue("/tmp/workspace"),
resolveDefaultAgentId: vi.fn().mockReturnValue("default"),
resolveAgentSkillsFilter: resolveAgentSkillsFilterMock,
resolveSessionAuthProfileOverride: resolveSessionAuthProfileOverrideMock,
lookupContextTokens: lookupContextTokensMock,
resolveCronStyleNow: resolveCronStyleNowMock,
DEFAULT_CONTEXT_TOKENS: 128000,
@@ -153,6 +152,10 @@ vi.mock("./run-execution.runtime.js", () => ({
logWarn: (...args: unknown[]) => logWarnMock(...args),
}));
vi.mock("./run-auth-profile.runtime.js", () => ({
resolveSessionAuthProfileOverride: resolveSessionAuthProfileOverrideMock,
}));
vi.mock("../../agents/cli-runner.runtime.js", () => ({
setCliSessionId: vi.fn(),
}));

View File

@@ -1,3 +1,4 @@
import { hasAnyAuthProfileStoreSource } from "../../agents/auth-profiles/source-check.js";
import type { SkillSnapshot } from "../../agents/skills.js";
import type { ThinkLevel } from "../../auto-reply/thinking.js";
import type { CliDeps } from "../../cli/outbound-send-deps.js";
@@ -50,7 +51,6 @@ import {
resolveDefaultAgentId,
resolveHookExternalContentSource,
resolveSessionTranscriptPath,
resolveSessionAuthProfileOverride,
resolveThinkingDefault,
setSessionRuntimeModel,
supportsXHighThinking,
@@ -63,12 +63,27 @@ import { resolveCronSkillsSnapshot } from "./skills-snapshot.js";
let sessionStoreRuntimePromise:
| Promise<typeof import("../../config/sessions/store.runtime.js")>
| undefined;
let cronAuthProfileRuntimePromise:
| Promise<typeof import("./run-auth-profile.runtime.js")>
| undefined;
async function loadSessionStoreRuntime() {
sessionStoreRuntimePromise ??= import("../../config/sessions/store.runtime.js");
return await sessionStoreRuntimePromise;
}
async function loadCronAuthProfileRuntime() {
cronAuthProfileRuntimePromise ??= import("./run-auth-profile.runtime.js");
return await cronAuthProfileRuntimePromise;
}
function hasConfiguredAuthProfiles(cfg: OpenClawConfig): boolean {
return (
Boolean(cfg.auth?.profiles && Object.keys(cfg.auth.profiles).length > 0) ||
Boolean(cfg.auth?.order && Object.keys(cfg.auth.order).length > 0)
);
}
function resolveNonNegativeNumber(value: number | undefined): number | undefined {
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : undefined;
}
@@ -413,16 +428,26 @@ async function prepareCronRunContext(params: {
} catch (err) {
logWarn(`[cron:${input.job.id}] Failed to persist pre-run session entry: ${String(err)}`);
}
const authProfileId = await resolveSessionAuthProfileOverride({
cfg: cfgWithAgentDefaults,
provider,
agentDir,
sessionEntry: cronSession.sessionEntry,
sessionStore: cronSession.store,
sessionKey: agentSessionKey,
storePath: cronSession.storePath,
isNewSession: cronSession.isNewSession && input.job.sessionTarget !== "isolated",
});
const hasSessionAuthProfileOverride = Boolean(
cronSession.sessionEntry.authProfileOverride?.trim(),
);
const authProfileId =
!hasSessionAuthProfileOverride &&
!hasConfiguredAuthProfiles(cfgWithAgentDefaults) &&
!hasAnyAuthProfileStoreSource(agentDir)
? undefined
: await (
await loadCronAuthProfileRuntime()
).resolveSessionAuthProfileOverride({
cfg: cfgWithAgentDefaults,
provider,
agentDir,
sessionEntry: cronSession.sessionEntry,
sessionStore: cronSession.store,
sessionKey: agentSessionKey,
storePath: cronSession.storePath,
isNewSession: cronSession.isNewSession && input.job.sessionTarget !== "isolated",
});
const liveSelection: CronLiveSelection = {
provider,
model,