mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-28 20:46:57 +02:00
perf(cron): keep auth profile runtime cold
This commit is contained in:
58
src/agents/auth-profiles/runtime-snapshots.ts
Normal file
58
src/agents/auth-profiles/runtime-snapshots.ts
Normal 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));
|
||||
}
|
||||
27
src/agents/auth-profiles/source-check.ts
Normal file
27
src/agents/auth-profiles/source-check.ts
Normal 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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
1
src/cron/isolated-agent/run-auth-profile.runtime.ts
Normal file
1
src/cron/isolated-agent/run-auth-profile.runtime.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { resolveSessionAuthProfileOverride } from "../../agents/auth-profiles/session-override.js";
|
||||
57
src/cron/isolated-agent/run.auth-profile-cold-path.test.ts
Normal file
57
src/cron/isolated-agent/run.auth-profile-cold-path.test.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
@@ -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";
|
||||
|
||||
@@ -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(),
|
||||
}));
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user