perf(agents): memoize transcript policy safely

This commit is contained in:
Peter Steinberger
2026-05-02 11:30:19 +01:00
parent ae339872a1
commit f8cbd356e1
3 changed files with 126 additions and 11 deletions

View File

@@ -6,6 +6,7 @@ Docs: https://docs.openclaw.ai
### Changes
- Agents/runtime: memoize transcript replay-policy resolution for stable config and process-env runs while preserving custom-env provider hook behavior. Thanks @DmitryPogodaev.
- Infra/path-guards: add a fast path for canonical absolute POSIX containment checks, avoiding repeated `path.resolve` and `path.relative` work in hot filesystem walkers. Refs #75895, #75575, and #68782. Thanks @Enderfga.
- Tools: add a platform-level tool descriptor planner for descriptor-first visibility, generic availability checks, and executor references. Thanks @shakkernerd.
- Docs/Codex: clarify that ChatGPT/Codex subscription setups should use `openai/gpt-*` with `agentRuntime.id: "codex"` for native Codex runtime, while `openai-codex/*` remains the PI OAuth route. Thanks @pashpashpash.

View File

@@ -1,4 +1,6 @@
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import { resolveProviderRuntimePlugin } from "../plugins/provider-hook-runtime.js";
vi.mock("../plugins/provider-hook-runtime.js", async () => {
const replayHelpers = await vi.importActual<
@@ -13,6 +15,7 @@ vi.mock("../plugins/provider-hook-runtime.js", async () => {
"anthropic",
"google",
"github-copilot",
"env-sensitive",
"kilocode",
"kimi",
"kimi-code",
@@ -38,9 +41,20 @@ vi.mock("../plugins/provider-hook-runtime.js", async () => {
return {};
}
return {
buildReplayPolicy: (context?: { modelId?: string; modelApi?: string }) => {
buildReplayPolicy: (context?: {
modelId?: string;
modelApi?: string;
env?: NodeJS.ProcessEnv;
}) => {
const modelId = context?.modelId?.toLowerCase() ?? "";
switch (provider) {
case "env-sensitive":
return {
sanitizeToolCallIds: context?.env?.OPENCLAW_TEST_TRANSCRIPT_POLICY === "strict",
...(context?.env?.OPENCLAW_TEST_TRANSCRIPT_POLICY === "strict"
? { toolCallIdMode: "strict" as const }
: {}),
};
case "amazon-bedrock":
case "anthropic":
return {
@@ -190,6 +204,7 @@ vi.mock("../plugins/provider-hook-runtime.js", async () => {
let resolveTranscriptPolicy: typeof import("./transcript-policy.js").resolveTranscriptPolicy;
let shouldAllowProviderOwnedThinkingReplay: typeof import("./transcript-policy.js").shouldAllowProviderOwnedThinkingReplay;
const mockResolveProviderRuntimePlugin = vi.mocked(resolveProviderRuntimePlugin);
describe("resolveTranscriptPolicy", () => {
beforeAll(async () => {
@@ -225,6 +240,56 @@ describe("resolveTranscriptPolicy", () => {
expect(policy.toolCallIdMode).toBe("strict");
});
it("memoizes replay policy resolution for the same config and process env", () => {
const config = {} as OpenClawConfig;
resolveTranscriptPolicy({
provider: "mistral",
modelId: "mistral-large-latest",
config,
env: process.env,
});
resolveTranscriptPolicy({
provider: "mistral",
modelId: "mistral-large-latest",
config,
env: process.env,
});
expect(mockResolveProviderRuntimePlugin).toHaveBeenCalledTimes(1);
});
it("does not reuse cached replay policies across custom env objects", () => {
const config = {} as OpenClawConfig;
const strictEnv = {
...process.env,
OPENCLAW_TEST_TRANSCRIPT_POLICY: "strict",
};
const looseEnv = {
...process.env,
OPENCLAW_TEST_TRANSCRIPT_POLICY: "loose",
};
const strictPolicy = resolveTranscriptPolicy({
provider: "env-sensitive",
modelId: "env-demo",
config,
env: strictEnv,
});
const loosePolicy = resolveTranscriptPolicy({
provider: "env-sensitive",
modelId: "env-demo",
config,
env: looseEnv,
});
expect(strictPolicy.sanitizeToolCallIds).toBe(true);
expect(strictPolicy.toolCallIdMode).toBe("strict");
expect(loosePolicy.sanitizeToolCallIds).toBe(false);
expect(loosePolicy.toolCallIdMode).toBeUndefined();
expect(mockResolveProviderRuntimePlugin).toHaveBeenCalledTimes(2);
});
it("enables sanitizeToolCallIds for Google provider", () => {
const policy = resolveTranscriptPolicy({
provider: "google",

View File

@@ -1,4 +1,5 @@
import type { OpenClawConfig } from "../config/types.openclaw.js";
import { resolvePluginControlPlaneFingerprint } from "../plugins/plugin-control-plane-context.js";
import { resolveProviderRuntimePlugin } from "../plugins/provider-hook-runtime.js";
import { shouldPreserveThinkingBlocks } from "../plugins/provider-replay-helpers.js";
import type { ProviderRuntimeModel } from "../plugins/provider-runtime-model.types.js";
@@ -177,6 +178,39 @@ function mergeTranscriptPolicy(
};
}
const transcriptPolicyCache = new WeakMap<OpenClawConfig, Map<string, TranscriptPolicy>>();
function canCacheTranscriptPolicy(params: {
config?: OpenClawConfig;
env?: NodeJS.ProcessEnv;
}): params is { config: OpenClawConfig; env?: NodeJS.ProcessEnv } {
if (!params.config) {
return false;
}
return !params.env || params.env === process.env;
}
function resolveTranscriptPolicyCacheKey(params: {
modelApi?: string | null;
provider: string;
modelId?: string | null;
config: OpenClawConfig;
workspaceDir?: string;
env?: NodeJS.ProcessEnv;
}): string {
return JSON.stringify({
provider: params.provider,
modelApi: params.modelApi ?? "",
modelId: params.modelId ?? "",
workspaceDir: params.workspaceDir ?? "",
pluginControlPlane: resolvePluginControlPlaneFingerprint({
config: params.config,
workspaceDir: params.workspaceDir,
env: params.env,
}),
});
}
export function resolveTranscriptPolicy(params: {
modelApi?: string | null;
provider?: string | null;
@@ -187,6 +221,15 @@ export function resolveTranscriptPolicy(params: {
model?: ProviderRuntimeModel;
}): TranscriptPolicy {
const provider = normalizeProviderId(params.provider ?? "");
const cacheKey = canCacheTranscriptPolicy(params)
? resolveTranscriptPolicyCacheKey({ ...params, provider, config: params.config })
: undefined;
if (cacheKey) {
const cached = transcriptPolicyCache.get(params.config)?.get(cacheKey);
if (cached) {
return cached;
}
}
const runtimePlugin = provider
? resolveProviderRuntimePlugin({
provider,
@@ -208,15 +251,21 @@ export function resolveTranscriptPolicy(params: {
// Once a provider adopts the replay-policy hook, replay policy should come
// from the plugin, not from transport-family defaults in core.
const buildReplayPolicy = runtimePlugin?.buildReplayPolicy;
if (buildReplayPolicy) {
const pluginPolicy = buildReplayPolicy(context);
return mergeTranscriptPolicy(pluginPolicy ?? undefined);
const policy = buildReplayPolicy
? mergeTranscriptPolicy(buildReplayPolicy(context) ?? undefined)
: mergeTranscriptPolicy(
buildUnownedProviderTransportReplayFallback({
modelApi: params.modelApi,
modelId: params.modelId,
}),
);
if (cacheKey) {
let configCache = transcriptPolicyCache.get(params.config);
if (!configCache) {
configCache = new Map();
transcriptPolicyCache.set(params.config, configCache);
}
configCache.set(cacheKey, policy);
}
return mergeTranscriptPolicy(
buildUnownedProviderTransportReplayFallback({
modelApi: params.modelApi,
modelId: params.modelId,
}),
);
return policy;
}