mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 07:20:43 +00:00
fix(agents): keep reply tool snapshots aligned
This commit is contained in:
@@ -69,6 +69,7 @@ Docs: https://docs.openclaw.ai
|
||||
### Fixes
|
||||
|
||||
- Config/recovery: skip whole-file last-known-good rollback when invalidity is scoped to `plugins.entries.*`, preserving unrelated user settings during plugin schema or host-version skew. Fixes #71289. Thanks @jalehman.
|
||||
- Agents/tools: keep resolved reply-run configs from being overwritten by stale runtime snapshots, and let empty web runtime metadata fall back to configured provider auto-detection so standard and queued turns expose the same tool set. Fixes #71355. Thanks @c-g14.
|
||||
- Compaction: honor explicit `agents.defaults.compaction.keepRecentTokens` for manual `/compact`, re-distill safeguard summaries instead of snowballing previous summaries, and enable safeguard summary quality checks by default. Fixes #71357. Thanks @WhiteGiverMa.
|
||||
- Sessions: honor configured `session.maintenance` settings during load-time maintenance instead of falling back to default entry caps. Fixes #71356. Thanks @comolago.
|
||||
- Browser/sandbox: pass the resolved `browser.ssrfPolicy` into sandbox browser bridges and refresh cached bridges when the effective policy changes, so sandboxed browser navigation honors private-network opt-ins. Fixes #45153 and #57055. Thanks @jzakirov, @zuoanCo, and @kybrcore.
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { selectApplicableRuntimeConfig } from "../config/config.js";
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import { resolvePluginTools } from "../plugins/tools.js";
|
||||
import { getActiveSecretsRuntimeSnapshot } from "../secrets/runtime.js";
|
||||
@@ -45,7 +46,11 @@ export function resolveOpenClawPluginToolsForOptions(params: {
|
||||
...resolveOpenClawPluginToolInputs({
|
||||
options: params.options,
|
||||
resolvedConfig: params.resolvedConfig,
|
||||
runtimeConfig: runtimeSnapshot?.config,
|
||||
runtimeConfig: selectApplicableRuntimeConfig({
|
||||
inputConfig: params.resolvedConfig ?? params.options?.config,
|
||||
runtimeConfig: runtimeSnapshot?.config,
|
||||
runtimeSourceConfig: runtimeSnapshot?.sourceConfig,
|
||||
}),
|
||||
}),
|
||||
existingToolNames: params.existingToolNames ?? new Set<string>(),
|
||||
toolAllowlist: params.options?.pluginToolAllowlist,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { activateSecretsRuntimeSnapshot, clearSecretsRuntimeSnapshot } from "../secrets/runtime.js";
|
||||
import { resolveOpenClawPluginToolsForOptions } from "./openclaw-plugin-tools.js";
|
||||
|
||||
const hoisted = vi.hoisted(() => ({
|
||||
@@ -13,6 +14,7 @@ vi.mock("../plugins/tools.js", () => ({
|
||||
describe("createOpenClawTools browser plugin integration", () => {
|
||||
afterEach(() => {
|
||||
hoisted.resolvePluginTools.mockReset();
|
||||
clearSecretsRuntimeSnapshot();
|
||||
});
|
||||
|
||||
it("keeps the browser tool returned by plugin resolution", () => {
|
||||
@@ -117,4 +119,57 @@ describe("createOpenClawTools browser plugin integration", () => {
|
||||
const details = (result.details ?? {}) as { workspaceOnly?: boolean | null };
|
||||
expect(details.workspaceOnly).toBe(true);
|
||||
});
|
||||
|
||||
it("does not pass a stale active snapshot as plugin runtime config for a resolved run config", () => {
|
||||
const staleSourceConfig = {
|
||||
plugins: {
|
||||
allow: ["old-plugin"],
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
const staleRuntimeConfig = {
|
||||
plugins: {
|
||||
allow: ["old-plugin"],
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
const resolvedRunConfig = {
|
||||
plugins: {
|
||||
allow: ["browser"],
|
||||
},
|
||||
tools: {
|
||||
experimental: {
|
||||
planTool: true,
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
let capturedRuntimeConfig: OpenClawConfig | undefined;
|
||||
hoisted.resolvePluginTools.mockImplementation((params: unknown) => {
|
||||
capturedRuntimeConfig = (params as { context?: { runtimeConfig?: OpenClawConfig } }).context
|
||||
?.runtimeConfig;
|
||||
return [];
|
||||
});
|
||||
activateSecretsRuntimeSnapshot({
|
||||
sourceConfig: staleSourceConfig,
|
||||
config: staleRuntimeConfig,
|
||||
authStores: [],
|
||||
warnings: [],
|
||||
webTools: {
|
||||
search: {
|
||||
providerSource: "none",
|
||||
diagnostics: [],
|
||||
},
|
||||
fetch: {
|
||||
providerSource: "none",
|
||||
diagnostics: [],
|
||||
},
|
||||
diagnostics: [],
|
||||
},
|
||||
});
|
||||
|
||||
resolveOpenClawPluginToolsForOptions({
|
||||
options: { config: resolvedRunConfig },
|
||||
resolvedConfig: resolvedRunConfig,
|
||||
});
|
||||
|
||||
expect(capturedRuntimeConfig).toBe(resolvedRunConfig);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -17,7 +17,8 @@ vi.mock("../../cli/command-secret-targets.js", () => ({
|
||||
hoisted.getScopedChannelsCommandSecretTargetsMock(...args),
|
||||
}));
|
||||
|
||||
const { resolveQueuedReplyExecutionConfig } = await import("./agent-runner-utils.js");
|
||||
const { resolveQueuedReplyExecutionConfig, resolveQueuedReplyRuntimeConfig } =
|
||||
await import("./agent-runner-utils.js");
|
||||
const { clearRuntimeConfigSnapshot, setRuntimeConfigSnapshot } =
|
||||
await import("../../config/config.js");
|
||||
|
||||
@@ -145,4 +146,46 @@ describe("resolveQueuedReplyExecutionConfig channel scope", () => {
|
||||
accountId: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it("does not replace an already resolved run config with a stale runtime snapshot", () => {
|
||||
const sourceConfig = {
|
||||
models: {
|
||||
providers: {
|
||||
openai: {
|
||||
apiKey: { source: "env", provider: "default", id: "OPENAI_API_KEY" },
|
||||
models: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
} as unknown as OpenClawConfig;
|
||||
const staleRuntimeConfig = {
|
||||
models: {
|
||||
providers: {
|
||||
openai: {
|
||||
apiKey: "stale-runtime-key",
|
||||
models: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
} as unknown as OpenClawConfig;
|
||||
const scopedResolvedConfig = {
|
||||
models: {
|
||||
providers: {
|
||||
openai: {
|
||||
apiKey: "fresh-scoped-key",
|
||||
models: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
tools: {
|
||||
experimental: {
|
||||
planTool: true,
|
||||
},
|
||||
},
|
||||
} as unknown as OpenClawConfig;
|
||||
setRuntimeConfigSnapshot(staleRuntimeConfig, sourceConfig);
|
||||
|
||||
expect(resolveQueuedReplyRuntimeConfig(structuredClone(sourceConfig))).toBe(staleRuntimeConfig);
|
||||
expect(resolveQueuedReplyRuntimeConfig(scopedResolvedConfig)).toBe(scopedResolvedConfig);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -11,7 +11,12 @@ import {
|
||||
getScopedChannelsCommandSecretTargets,
|
||||
} from "../../cli/command-secret-targets.js";
|
||||
import { resolveMessageSecretScope } from "../../cli/message-secret-scope.js";
|
||||
import { getRuntimeConfigSnapshot, type OpenClawConfig } from "../../config/config.js";
|
||||
import {
|
||||
getRuntimeConfigSnapshot,
|
||||
getRuntimeConfigSourceSnapshot,
|
||||
selectApplicableRuntimeConfig,
|
||||
type OpenClawConfig,
|
||||
} from "../../config/config.js";
|
||||
import {
|
||||
normalizeOptionalLowercaseString,
|
||||
normalizeOptionalString,
|
||||
@@ -29,8 +34,16 @@ import type { FollowupRun } from "./queue.js";
|
||||
const BUN_FETCH_SOCKET_ERROR_RE = /socket connection was closed unexpectedly/i;
|
||||
|
||||
export function resolveQueuedReplyRuntimeConfig(config: OpenClawConfig): OpenClawConfig {
|
||||
const runtimeConfig =
|
||||
typeof getRuntimeConfigSnapshot === "function" ? getRuntimeConfigSnapshot() : null;
|
||||
const runtimeSourceConfig =
|
||||
typeof getRuntimeConfigSourceSnapshot === "function" ? getRuntimeConfigSourceSnapshot() : null;
|
||||
return (
|
||||
(typeof getRuntimeConfigSnapshot === "function" ? getRuntimeConfigSnapshot() : null) ?? config
|
||||
selectApplicableRuntimeConfig({
|
||||
inputConfig: config,
|
||||
runtimeConfig,
|
||||
runtimeSourceConfig,
|
||||
}) ?? config
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ export {
|
||||
recoverConfigFromJsonRootSuffix,
|
||||
resetConfigRuntimeState,
|
||||
resolveConfigSnapshotHash,
|
||||
selectApplicableRuntimeConfig,
|
||||
setRuntimeConfigSnapshotRefreshHandler,
|
||||
setRuntimeConfigSnapshot,
|
||||
writeConfigFile,
|
||||
|
||||
@@ -80,6 +80,7 @@ import {
|
||||
notifyRuntimeConfigWriteListeners,
|
||||
registerRuntimeConfigWriteListener,
|
||||
resetConfigRuntimeState as resetConfigRuntimeStateState,
|
||||
selectApplicableRuntimeConfig,
|
||||
setRuntimeConfigSnapshot as setRuntimeConfigSnapshotState,
|
||||
getRuntimeConfigSnapshotRefreshHandler as getRuntimeConfigSnapshotRefreshHandlerState,
|
||||
setRuntimeConfigSnapshotRefreshHandler as setRuntimeConfigSnapshotRefreshHandlerState,
|
||||
@@ -98,6 +99,7 @@ export {
|
||||
getRuntimeConfigSnapshotState as getRuntimeConfigSnapshot,
|
||||
getRuntimeConfigSourceSnapshotState as getRuntimeConfigSourceSnapshot,
|
||||
resetConfigRuntimeStateState as resetConfigRuntimeState,
|
||||
selectApplicableRuntimeConfig,
|
||||
setRuntimeConfigSnapshotState as setRuntimeConfigSnapshot,
|
||||
setRuntimeConfigSnapshotRefreshHandlerState as setRuntimeConfigSnapshotRefreshHandler,
|
||||
};
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
notifyRuntimeConfigWriteListeners,
|
||||
registerRuntimeConfigWriteListener,
|
||||
resetConfigRuntimeState,
|
||||
selectApplicableRuntimeConfig,
|
||||
setRuntimeConfigSnapshot,
|
||||
setRuntimeConfigSnapshotRefreshHandler,
|
||||
} from "./runtime-snapshot.js";
|
||||
@@ -70,6 +71,54 @@ describe("runtime snapshot state", () => {
|
||||
expect(getRuntimeConfigSourceSnapshot()).toEqual(sourceConfig);
|
||||
});
|
||||
|
||||
it("selects runtime config only when input still matches the runtime source", () => {
|
||||
const sourceConfig: OpenClawConfig = {
|
||||
models: {
|
||||
providers: {
|
||||
openai: {
|
||||
baseUrl: "https://api.openai.com/v1",
|
||||
apiKey: { source: "env", provider: "default", id: "OPENAI_API_KEY" },
|
||||
models: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const runtimeConfig: OpenClawConfig = {
|
||||
models: {
|
||||
providers: {
|
||||
openai: {
|
||||
baseUrl: "https://api.openai.com/v1",
|
||||
apiKey: "sk-runtime-resolved",
|
||||
models: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const scopedResolvedConfig: OpenClawConfig = {
|
||||
...runtimeConfig,
|
||||
tools: {
|
||||
experimental: {
|
||||
planTool: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(
|
||||
selectApplicableRuntimeConfig({
|
||||
inputConfig: structuredClone(sourceConfig),
|
||||
runtimeConfig,
|
||||
runtimeSourceConfig: sourceConfig,
|
||||
}),
|
||||
).toBe(runtimeConfig);
|
||||
expect(
|
||||
selectApplicableRuntimeConfig({
|
||||
inputConfig: scopedResolvedConfig,
|
||||
runtimeConfig,
|
||||
runtimeSourceConfig: sourceConfig,
|
||||
}),
|
||||
).toBe(scopedResolvedConfig);
|
||||
});
|
||||
|
||||
it("clears runtime source snapshot when runtime snapshot is cleared", () => {
|
||||
setRuntimeConfigSnapshot({ gateway: { port: 18789 } }, { gateway: { port: 18789 } });
|
||||
resetRuntimeConfigState();
|
||||
|
||||
@@ -22,6 +22,31 @@ let runtimeConfigSourceSnapshot: OpenClawConfig | null = null;
|
||||
let runtimeConfigSnapshotRefreshHandler: RuntimeConfigSnapshotRefreshHandler | null = null;
|
||||
const runtimeConfigWriteListeners = new Set<(event: RuntimeConfigWriteNotification) => void>();
|
||||
|
||||
function stableConfigStringify(value: unknown): string {
|
||||
if (value === null || typeof value !== "object") {
|
||||
return JSON.stringify(value) ?? "null";
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
return `[${value.map((entry) => stableConfigStringify(entry)).join(",")}]`;
|
||||
}
|
||||
const record = value as Record<string, unknown>;
|
||||
const keys = Object.keys(record).toSorted();
|
||||
return `{${keys
|
||||
.map((key) => `${JSON.stringify(key)}:${stableConfigStringify(record[key])}`)
|
||||
.join(",")}}`;
|
||||
}
|
||||
|
||||
function configSnapshotsMatch(left: OpenClawConfig, right: OpenClawConfig): boolean {
|
||||
if (left === right) {
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
return stableConfigStringify(left) === stableConfigStringify(right);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function setRuntimeConfigSnapshot(
|
||||
config: OpenClawConfig,
|
||||
sourceConfig?: OpenClawConfig,
|
||||
@@ -47,6 +72,32 @@ export function getRuntimeConfigSourceSnapshot(): OpenClawConfig | null {
|
||||
return runtimeConfigSourceSnapshot;
|
||||
}
|
||||
|
||||
export function selectApplicableRuntimeConfig(params: {
|
||||
inputConfig?: OpenClawConfig;
|
||||
runtimeConfig?: OpenClawConfig | null;
|
||||
runtimeSourceConfig?: OpenClawConfig | null;
|
||||
}): OpenClawConfig | undefined {
|
||||
const runtimeConfig = params.runtimeConfig ?? null;
|
||||
if (!runtimeConfig) {
|
||||
return params.inputConfig;
|
||||
}
|
||||
const inputConfig = params.inputConfig;
|
||||
if (!inputConfig) {
|
||||
return runtimeConfig;
|
||||
}
|
||||
if (inputConfig === runtimeConfig) {
|
||||
return inputConfig;
|
||||
}
|
||||
const runtimeSourceConfig = params.runtimeSourceConfig ?? null;
|
||||
if (!runtimeSourceConfig) {
|
||||
return runtimeConfig;
|
||||
}
|
||||
if (configSnapshotsMatch(inputConfig, runtimeSourceConfig)) {
|
||||
return runtimeConfig;
|
||||
}
|
||||
return inputConfig;
|
||||
}
|
||||
|
||||
export function setRuntimeConfigSnapshotRefreshHandler(
|
||||
refreshHandler: RuntimeConfigSnapshotRefreshHandler | null,
|
||||
): void {
|
||||
|
||||
31
src/web/provider-runtime-shared.test.ts
Normal file
31
src/web/provider-runtime-shared.test.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { resolveWebProviderDefinition } from "./provider-runtime-shared.js";
|
||||
|
||||
describe("resolveWebProviderDefinition", () => {
|
||||
it("falls back to auto-detect when runtime metadata has no selected provider", () => {
|
||||
const resolved = resolveWebProviderDefinition({
|
||||
config: {},
|
||||
toolConfig: { enabled: true },
|
||||
runtimeMetadata: {},
|
||||
providers: [
|
||||
{
|
||||
id: "custom",
|
||||
},
|
||||
],
|
||||
resolveEnabled: () => true,
|
||||
resolveAutoProviderId: () => "custom",
|
||||
createTool: ({ provider }) => ({
|
||||
name: provider.id,
|
||||
}),
|
||||
});
|
||||
|
||||
expect(resolved).toEqual({
|
||||
provider: {
|
||||
id: "custom",
|
||||
},
|
||||
definition: {
|
||||
name: "custom",
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -133,8 +133,7 @@ export function resolveWebProviderDefinition<
|
||||
providers,
|
||||
});
|
||||
const providerId =
|
||||
params.providerId ??
|
||||
(params.runtimeMetadata ? params.runtimeMetadata.selectedProvider : autoProviderId);
|
||||
params.providerId ?? params.runtimeMetadata?.selectedProvider ?? autoProviderId;
|
||||
if (!providerId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user