perf(test): speed up reply trigger hotspots

This commit is contained in:
Peter Steinberger
2026-04-20 14:14:31 +01:00
parent b7703616f0
commit 510fe8b95d
14 changed files with 288 additions and 68 deletions

View File

@@ -29,6 +29,7 @@ export {
ensureAuthProfileStore,
hasAnyAuthProfileStoreSource,
loadAuthProfileStoreForSecretsRuntime,
loadAuthProfileStoreWithoutExternalProfiles,
loadAuthProfileStoreForRuntime,
replaceRuntimeAuthProfileStoreSnapshots,
loadAuthProfileStore,

View File

@@ -282,6 +282,19 @@ export function loadAuthProfileStoreForSecretsRuntime(agentDir?: string): AuthPr
return loadAuthProfileStoreForRuntime(agentDir, { readOnly: true, allowKeychainPrompt: false });
}
export function loadAuthProfileStoreWithoutExternalProfiles(agentDir?: string): AuthProfileStore {
const options: LoadAuthProfileStoreOptions = { readOnly: true, allowKeychainPrompt: false };
const store = loadAuthProfileStoreForAgent(agentDir, options);
const authPath = resolveAuthStorePath(agentDir);
const mainAuthPath = resolveAuthStorePath();
if (!agentDir || authPath === mainAuthPath) {
return store;
}
const mainStore = loadAuthProfileStoreForAgent(undefined, options);
return mergeAuthProfileStores(mainStore, store);
}
export function ensureAuthProfileStore(
agentDir?: string,
options?: { allowKeychainPrompt?: boolean },
@@ -304,6 +317,25 @@ export function ensureAuthProfileStore(
return overlayExternalAuthProfiles(merged, { agentDir });
}
export function findPersistedAuthProfileCredential(params: {
agentDir?: string;
profileId: string;
}): AuthProfileStore["profiles"][string] | undefined {
const requestedStore = loadPersistedAuthProfileStore(params.agentDir);
const requestedProfile = requestedStore?.profiles[params.profileId];
if (requestedProfile || !params.agentDir) {
return requestedProfile;
}
const requestedPath = resolveAuthStorePath(params.agentDir);
const mainPath = resolveAuthStorePath();
if (requestedPath === mainPath) {
return requestedProfile;
}
return loadPersistedAuthProfileStore()?.profiles[params.profileId];
}
export function ensureAuthProfileStoreForLocalUpdate(agentDir?: string): AuthProfileStore {
const options: LoadAuthProfileStoreOptions = { syncExternalCli: false };
const store = loadAuthProfileStoreForAgent(agentDir, options);

View File

@@ -2,6 +2,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
const mocks = vi.hoisted(() => ({
ensureAuthProfileStore: vi.fn(),
loadAuthProfileStoreWithoutExternalProfiles: vi.fn(),
resolveAuthProfileOrder: vi.fn(),
resolveAuthProfileDisplayLabel: vi.fn(),
resolveUsableCustomProviderApiKey: vi.fn(() => null),
@@ -10,6 +11,7 @@ const mocks = vi.hoisted(() => ({
vi.mock("./auth-profiles.js", () => ({
ensureAuthProfileStore: mocks.ensureAuthProfileStore,
loadAuthProfileStoreWithoutExternalProfiles: mocks.loadAuthProfileStoreWithoutExternalProfiles,
resolveAuthProfileOrder: mocks.resolveAuthProfileOrder,
resolveAuthProfileDisplayLabel: mocks.resolveAuthProfileDisplayLabel,
}));
@@ -27,6 +29,7 @@ describe("resolveModelAuthLabel", () => {
({ resolveModelAuthLabel } = await import("./model-auth-label.js"));
}
mocks.ensureAuthProfileStore.mockReset();
mocks.loadAuthProfileStoreWithoutExternalProfiles.mockReset();
mocks.resolveAuthProfileOrder.mockReset();
mocks.resolveAuthProfileDisplayLabel.mockReset();
mocks.resolveUsableCustomProviderApiKey.mockReset();
@@ -108,4 +111,28 @@ describe("resolveModelAuthLabel", () => {
expect(label).toBe("oauth (anthropic:oauth)");
});
it("can skip external auth profile overlays for status labels", () => {
mocks.loadAuthProfileStoreWithoutExternalProfiles.mockReturnValue({
version: 1,
profiles: {
"anthropic:oauth": {
type: "oauth",
provider: "anthropic",
},
},
} as never);
mocks.resolveAuthProfileOrder.mockReturnValue(["anthropic:oauth"]);
mocks.resolveAuthProfileDisplayLabel.mockReturnValue("anthropic:oauth");
const label = resolveModelAuthLabel({
provider: "anthropic",
cfg: {},
includeExternalProfiles: false,
});
expect(label).toBe("oauth (anthropic:oauth)");
expect(mocks.loadAuthProfileStoreWithoutExternalProfiles).toHaveBeenCalledOnce();
expect(mocks.ensureAuthProfileStore).not.toHaveBeenCalled();
});
});

View File

@@ -2,6 +2,7 @@ import type { SessionEntry } from "../config/sessions.js";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import {
ensureAuthProfileStore,
loadAuthProfileStoreWithoutExternalProfiles,
resolveAuthProfileDisplayLabel,
resolveAuthProfileOrder,
} from "./auth-profiles.js";
@@ -13,6 +14,7 @@ export function resolveModelAuthLabel(params: {
cfg?: OpenClawConfig;
sessionEntry?: Partial<Pick<SessionEntry, "authProfileOverride">>;
agentDir?: string;
includeExternalProfiles?: boolean;
}): string | undefined {
const resolvedProvider = params.provider?.trim();
if (!resolvedProvider) {
@@ -20,9 +22,12 @@ export function resolveModelAuthLabel(params: {
}
const providerKey = normalizeProviderId(resolvedProvider);
const store = ensureAuthProfileStore(params.agentDir, {
allowKeychainPrompt: false,
});
const store =
params.includeExternalProfiles === false
? loadAuthProfileStoreWithoutExternalProfiles(params.agentDir)
: ensureAuthProfileStore(params.agentDir, {
allowKeychainPrompt: false,
});
const profileOverride = params.sessionEntry?.authProfileOverride?.trim();
const order = resolveAuthProfileOrder({
cfg: params.cfg,

View File

@@ -17,7 +17,6 @@ import {
import { loadSessionStore, resolveSessionKey } from "../config/sessions.js";
import { registerGroupIntroPromptCases } from "./reply.triggers.group-intro-prompts.cases.js";
import { registerTriggerHandlingUsageSummaryCases } from "./reply.triggers.trigger-handling.filters-usage-summary-current-model-provider.cases.js";
import { withFullRuntimeReplyConfig } from "./reply/get-reply-fast-path.js";
import { enqueueFollowupRun, getFollowupQueueDepth, type FollowupRun } from "./reply/queue.js";
import { HEARTBEAT_TOKEN } from "./tokens.js";
@@ -672,10 +671,8 @@ describe("trigger handling", () => {
it("applies native model auth profile overrides to the target session", async () => {
await withTempHome(async (home) => {
const cfg = withFullRuntimeReplyConfig({
...makeCfg(home),
session: { store: join(home, "native-model-auth.sessions.json") },
});
const cfg = makeCfg(home);
cfg.session = { ...cfg.session, store: join(home, "native-model-auth.sessions.json") };
const runEmbeddedPiAgentMock = getRunEmbeddedPiAgentMock();
runEmbeddedPiAgentMock.mockReset();
const storePath = cfg.session?.store;

View File

@@ -1,4 +1,7 @@
import { ensureAuthProfileStore } from "../../agents/auth-profiles.js";
import {
ensureAuthProfileStore,
findPersistedAuthProfileCredential,
} from "../../agents/auth-profiles/store.js";
import type { OpenClawConfig } from "../../config/types.openclaw.js";
import { normalizeOptionalString } from "../../shared/string-coerce.js";
@@ -12,6 +15,19 @@ export function resolveProfileOverride(params: {
if (!raw) {
return {};
}
const persistedProfile = findPersistedAuthProfileCredential({
agentDir: params.agentDir,
profileId: raw,
});
if (persistedProfile) {
if (persistedProfile.provider !== params.provider) {
return {
error: `Auth profile "${raw}" is for ${persistedProfile.provider}, not ${params.provider}.`,
};
}
return { profileId: raw };
}
const store = ensureAuthProfileStore(params.agentDir, {
allowKeychainPrompt: false,
});

View File

@@ -45,6 +45,7 @@ export async function persistInlineDirectives(params: {
surface?: string;
gatewayClientScopes?: string[];
senderIsOwner?: boolean;
markLiveSwitchPending?: boolean;
}): Promise<{ provider: string; model: string; contextTokens: number }> {
const {
directives,
@@ -185,6 +186,7 @@ export async function persistInlineDirectives(params: {
entry: sessionEntry,
selection: modelResolution.modelSelection,
profileOverride: modelResolution.profileOverride,
markLiveSwitchPending: params.markLiveSwitchPending,
});
provider = modelResolution.modelSelection.provider;
model = modelResolution.modelSelection.model;

View File

@@ -6,6 +6,7 @@ import type { ElevatedLevel } from "../thinking.js";
import type { ReplyPayload } from "../types.js";
import type { CommandContext } from "./commands-types.js";
import { isDirectiveOnly } from "./directive-handling.directive-only.js";
import { resolveModelSelectionFromDirective } from "./directive-handling.model-selection.js";
import type { ApplyInlineDirectivesFastLaneParams } from "./directive-handling.params.js";
import type { InlineDirectives } from "./directive-handling.parse.js";
import { clearInlineDirectives } from "./get-reply-directives-utils.js";
@@ -49,6 +50,21 @@ function loadDirectivePersist() {
return directivePersistPromise;
}
function hasOnlyModelDirective(directives: InlineDirectives): boolean {
return (
directives.hasModelDirective &&
!directives.hasThinkDirective &&
!directives.hasFastDirective &&
!directives.hasVerboseDirective &&
!directives.hasTraceDirective &&
!directives.hasReasoningDirective &&
!directives.hasElevatedDirective &&
!directives.hasExecDirective &&
!directives.hasQueueDirective &&
!directives.hasStatusDirective
);
}
export type ApplyDirectiveResult =
| { kind: "reply"; reply: ReplyPayload | ReplyPayload[] | undefined }
| {
@@ -211,6 +227,69 @@ export async function applyInlineDirectiveOverrides(params: {
typing.cleanup();
return { kind: "reply", reply: undefined };
}
if (hasOnlyModelDirective(directives) && effectiveModelDirective) {
const modelResolution = resolveModelSelectionFromDirective({
directives: {
...directives,
rawModelDirective: effectiveModelDirective,
},
cfg,
agentDir,
defaultProvider,
defaultModel,
aliasIndex,
allowedModelKeys: modelState.allowedModelKeys,
allowedModelCatalog: modelState.allowedModelCatalog,
provider,
});
if (modelResolution.errorText) {
typing.cleanup();
return { kind: "reply", reply: { text: modelResolution.errorText } };
}
const modelSelection = modelResolution.modelSelection;
if (modelSelection) {
await (
await loadDirectivePersist()
).persistInlineDirectives({
directives,
effectiveModelDirective,
cfg,
agentDir,
sessionEntry,
sessionStore,
sessionKey,
storePath,
elevatedEnabled,
elevatedAllowed,
defaultProvider,
defaultModel,
aliasIndex,
allowedModelKeys: modelState.allowedModelKeys,
provider,
model,
initialModelLabel,
formatModelSwitchEvent,
agentCfg,
messageProvider: ctx.Provider,
surface: ctx.Surface,
gatewayClientScopes: ctx.GatewayClientScopes,
senderIsOwner: command.senderIsOwner,
markLiveSwitchPending: true,
});
const label = `${modelSelection.provider}/${modelSelection.model}`;
const labelWithAlias = modelSelection.alias ? `${modelSelection.alias} (${label})` : label;
const parts = [
modelSelection.isDefault
? `Model reset to default (${labelWithAlias}).`
: `Model set to ${labelWithAlias}.`,
modelResolution.profileOverride
? `Auth profile set to ${modelResolution.profileOverride}.`
: undefined,
].filter(Boolean);
typing.cleanup();
return { kind: "reply", reply: { text: parts.join(" ") } };
}
}
const {
currentThinkLevel: resolvedDefaultThinkLevel,
currentFastMode,

View File

@@ -1,7 +1,7 @@
import { listAgentEntries } from "../../agents/agent-scope.js";
import { DEFAULT_CONTEXT_TOKENS } from "../../agents/defaults.js";
import { resolveFastModeState } from "../../agents/fast-mode.js";
import type { ModelAliasIndex } from "../../agents/model-selection.js";
import { type ModelAliasIndex, resolveModelRefFromString } from "../../agents/model-selection.js";
import { resolveSandboxRuntimeStatus } from "../../agents/sandbox/runtime-status.js";
import type { SkillCommandSpec } from "../../agents/skills.js";
import type { SessionEntry } from "../../config/sessions.js";
@@ -53,6 +53,24 @@ function loadSkillCommands() {
return skillCommandsPromise;
}
function canUseFastExplicitModelDirective(params: {
directives: InlineDirectives;
defaultProvider: string;
aliasIndex: ModelAliasIndex;
}): boolean {
const raw = normalizeOptionalString(params.directives.rawModelDirective);
if (!raw || /^[0-9]+$/.test(raw)) {
return false;
}
return Boolean(
resolveModelRefFromString({
raw,
defaultProvider: params.defaultProvider,
aliasIndex: params.aliasIndex,
}),
);
}
function resolveDirectiveCommandText(params: { ctx: MsgContext; sessionCtx: TemplateContext }) {
const commandSource =
params.sessionCtx.BodyForCommands ??
@@ -202,8 +220,13 @@ export async function resolveReplyDirectives(params: {
commandSource: ctx.CommandSource,
});
const commandTextHasSlash = commandText.includes("/");
const hasConfiguredModelAliases =
commandTextHasSlash &&
Object.values(cfg.agents?.defaults?.models ?? {}).some((entry) =>
Boolean(normalizeOptionalString(entry.alias)),
);
const reservedCommands = new Set<string>();
if (commandTextHasSlash) {
if (hasConfiguredModelAliases) {
const { listChatCommands } = await loadCommandsRegistry();
for (const chatCommand of listChatCommands()) {
for (const alias of chatCommand.textAliases) {
@@ -212,11 +235,13 @@ export async function resolveReplyDirectives(params: {
}
}
const rawAliases = resolveConfiguredDirectiveAliases({
cfg,
commandTextHasSlash,
reservedCommands,
});
const rawAliases = hasConfiguredModelAliases
? resolveConfiguredDirectiveAliases({
cfg,
commandTextHasSlash,
reservedCommands,
})
: [];
// Only load workspace skill commands when we actually need them to filter aliases.
// This avoids scanning skills for messages that only use plain text with no slash syntax.
@@ -428,33 +453,41 @@ export async function resolveReplyDirectives(params: {
isFastTestEnv: process.env.OPENCLAW_TEST_FAST === "1",
});
const modelState =
const useFastModelSelection =
useFastReplyRuntime &&
!directives.hasModelDirective &&
!hasResolvedHeartbeatModelOverride &&
!(agentCfg?.models && Object.keys(agentCfg.models).length > 0) &&
!normalizeOptionalString(targetSessionEntry?.modelOverride) &&
!normalizeOptionalString(targetSessionEntry?.providerOverride)
? createFastTestModelSelectionState({
agentCfg,
provider,
model,
})
: await createModelSelectionState({
cfg,
agentId,
agentCfg,
sessionEntry: targetSessionEntry,
sessionStore,
sessionKey,
parentSessionKey: targetSessionEntry?.parentSessionKey ?? ctx.ParentSessionKey,
storePath,
defaultProvider,
defaultModel,
provider,
model,
hasModelDirective: directives.hasModelDirective,
hasResolvedHeartbeatModelOverride,
});
!normalizeOptionalString(targetSessionEntry?.providerOverride) &&
(!directives.hasModelDirective ||
canUseFastExplicitModelDirective({
directives,
defaultProvider,
aliasIndex: params.aliasIndex,
}));
const modelState = useFastModelSelection
? createFastTestModelSelectionState({
agentCfg,
provider,
model,
})
: await createModelSelectionState({
cfg,
agentId,
agentCfg,
sessionEntry: targetSessionEntry,
sessionStore,
sessionKey,
parentSessionKey: targetSessionEntry?.parentSessionKey ?? ctx.ParentSessionKey,
storePath,
defaultProvider,
defaultModel,
provider,
model,
hasModelDirective: directives.hasModelDirective,
hasResolvedHeartbeatModelOverride,
});
provider = modelState.provider;
model = modelState.model;
const resolvedThinkLevelWithDefault =

View File

@@ -337,14 +337,15 @@ export async function runPreparedReply(
workspaceDir,
isPrimaryRun: !isSubagentSessionKey(sessionKey) && !isAcpSessionKey(sessionKey),
isCanonicalWorkspace: !spawnedWorkspaceOverride,
hasBootstrapFileAccess: resolveBareResetBootstrapFileAccess({
cfg,
agentId,
sessionKey,
workspaceDir,
modelProvider: provider,
modelId: model,
}),
hasBootstrapFileAccess: () =>
resolveBareResetBootstrapFileAccess({
cfg,
agentId,
sessionKey,
workspaceDir,
modelProvider: provider,
modelId: model,
}),
})
: null;
const startupContextPrelude =
@@ -559,7 +560,7 @@ export async function runPreparedReply(
logVerbose(`Interrupting ${sessionLaneKey} (cleared ${cleared}, aborted=${aborted})`);
}
let authProfileId = useFastReplyRuntime
? undefined
? preparedSessionState.sessionEntry?.authProfileOverride
: await resolveSessionAuthProfileOverride({
cfg,
provider,
@@ -612,7 +613,7 @@ export async function runPreparedReply(
refreshPreparedState: async () => {
preparedSessionState = resolvePreparedSessionState();
authProfileId = useFastReplyRuntime
? undefined
? preparedSessionState.sessionEntry?.authProfileOverride
: await resolveSessionAuthProfileOverride({
cfg,
provider,

View File

@@ -323,22 +323,25 @@ export async function getReplyFromConfig(
});
}
const channelModelOverride = resolveChannelModelOverride({
cfg,
channel:
groupResolution?.channel ??
sessionEntry.channel ??
sessionEntry.origin?.provider ??
(typeof finalized.OriginatingChannel === "string"
? finalized.OriginatingChannel
: undefined) ??
finalized.Provider,
groupId: groupResolution?.id ?? sessionEntry.groupId,
groupChatType: sessionEntry.chatType ?? sessionCtx.ChatType ?? finalized.ChatType,
groupChannel: sessionEntry.groupChannel ?? sessionCtx.GroupChannel ?? finalized.GroupChannel,
groupSubject: sessionEntry.subject ?? sessionCtx.GroupSubject ?? finalized.GroupSubject,
parentSessionKey: sessionCtx.ParentSessionKey,
});
const channelModelOverride = cfg.channels?.modelByChannel
? resolveChannelModelOverride({
cfg,
channel:
groupResolution?.channel ??
sessionEntry.channel ??
sessionEntry.origin?.provider ??
(typeof finalized.OriginatingChannel === "string"
? finalized.OriginatingChannel
: undefined) ??
finalized.Provider,
groupId: groupResolution?.id ?? sessionEntry.groupId,
groupChatType: sessionEntry.chatType ?? sessionCtx.ChatType ?? finalized.ChatType,
groupChannel:
sessionEntry.groupChannel ?? sessionCtx.GroupChannel ?? finalized.GroupChannel,
groupSubject: sessionEntry.subject ?? sessionCtx.GroupSubject ?? finalized.GroupSubject,
parentSessionKey: sessionCtx.ParentSessionKey,
})
: null;
const hasSessionModelOverride = Boolean(
normalizeOptionalString(sessionEntry.modelOverride) ||
normalizeOptionalString(sessionEntry.providerOverride),

View File

@@ -84,6 +84,23 @@ describe("buildBareSessionResetPrompt", () => {
expect(complete.prompt).toContain("Execute your Session Startup sequence now");
});
it("does not resolve bootstrap file access when bootstrap is complete", async () => {
const workspaceDir = await makeTempWorkspace("openclaw-reset-bootstrap-complete-");
let resolvedAccess = false;
const complete = await resolveBareSessionResetPromptState({
workspaceDir,
hasBootstrapFileAccess: () => {
resolvedAccess = true;
return false;
},
});
expect(complete.bootstrapMode).toBe("none");
expect(complete.shouldPrependStartupContext).toBe(true);
expect(resolvedAccess).toBe(false);
});
it("suppresses bootstrap mode for non-primary bare reset sessions", async () => {
const workspaceDir = await makeTempWorkspace("openclaw-reset-non-primary-");
await fs.writeFile(path.join(workspaceDir, "BOOTSTRAP.md"), "ritual", "utf8");

View File

@@ -63,7 +63,7 @@ export async function resolveBareSessionResetPromptState(params: {
nowMs?: number;
isPrimaryRun?: boolean;
isCanonicalWorkspace?: boolean;
hasBootstrapFileAccess?: boolean;
hasBootstrapFileAccess?: boolean | (() => boolean);
}): Promise<{
bootstrapMode: BootstrapMode;
prompt: string;
@@ -72,13 +72,18 @@ export async function resolveBareSessionResetPromptState(params: {
const bootstrapPending = params.workspaceDir
? await isWorkspaceBootstrapPending(params.workspaceDir)
: false;
const hasBootstrapFileAccess = bootstrapPending
? typeof params.hasBootstrapFileAccess === "function"
? params.hasBootstrapFileAccess()
: (params.hasBootstrapFileAccess ?? true)
: true;
const bootstrapMode = resolveBootstrapMode({
bootstrapPending,
runKind: "default",
isInteractiveUserFacing: true,
isPrimaryRun: params.isPrimaryRun ?? true,
isCanonicalWorkspace: params.isCanonicalWorkspace ?? true,
hasBootstrapFileAccess: params.hasBootstrapFileAccess ?? true,
hasBootstrapFileAccess,
});
return {
bootstrapMode,

View File

@@ -176,6 +176,7 @@ export async function buildStatusText(params: BuildStatusTextParams): Promise<st
cfg,
sessionEntry,
agentDir: statusAgentDir,
includeExternalProfiles: false,
});
const activeModelAuth = Object.hasOwn(params, "activeModelAuthOverride")
? params.activeModelAuthOverride
@@ -185,6 +186,7 @@ export async function buildStatusText(params: BuildStatusTextParams): Promise<st
cfg,
sessionEntry,
agentDir: statusAgentDir,
includeExternalProfiles: false,
})
: selectedModelAuth;
const currentUsageProvider = (() => {