mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-10 08:12:43 +00:00
Agents: add context metadata warmup retry backoff
This commit is contained in:
@@ -52,6 +52,7 @@ Docs: https://docs.openclaw.ai
|
|||||||
- OpenAI/Responses WebSocket tool-call id hygiene: normalize blank/whitespace streamed tool-call ids before persistence, and block empty `function_call_output.call_id` payloads in the WS conversion path to avoid OpenAI 400 errors (`Invalid 'input[n].call_id': empty string`), with regression coverage for both inbound stream normalization and outbound payload guards.
|
- OpenAI/Responses WebSocket tool-call id hygiene: normalize blank/whitespace streamed tool-call ids before persistence, and block empty `function_call_output.call_id` payloads in the WS conversion path to avoid OpenAI 400 errors (`Invalid 'input[n].call_id': empty string`), with regression coverage for both inbound stream normalization and outbound payload guards.
|
||||||
- Gateway/Control UI basePath webhook passthrough: let non-read methods under configured `controlUiBasePath` fall through to plugin routes (instead of returning Control UI 405), restoring webhook handlers behind basePath mounts. (#32311) Thanks @ademczuk.
|
- Gateway/Control UI basePath webhook passthrough: let non-read methods under configured `controlUiBasePath` fall through to plugin routes (instead of returning Control UI 405), restoring webhook handlers behind basePath mounts. (#32311) Thanks @ademczuk.
|
||||||
- CLI/Config validation and routing hardening: dedupe `openclaw config validate` failures to a single authoritative report, expose allowed-values metadata/hints across core Zod and plugin AJV validation (including `--json` fields), sanitize terminal-rendered validation text, and make command-path parsing root-option-aware across preaction/route/lazy registration (including routed `config get/unset` with split root options). Thanks @gumadeiras.
|
- CLI/Config validation and routing hardening: dedupe `openclaw config validate` failures to a single authoritative report, expose allowed-values metadata/hints across core Zod and plugin AJV validation (including `--json` fields), sanitize terminal-rendered validation text, and make command-path parsing root-option-aware across preaction/route/lazy registration (including routed `config get/unset` with split root options). Thanks @gumadeiras.
|
||||||
|
- Context-window metadata warmup: add exponential config-load retry backoff (1s -> 2s -> 4s, capped at 60s) so transient startup failures recover automatically without hot-loop retries.
|
||||||
- Models/config env propagation: apply `config.env.vars` before implicit provider discovery in models bootstrap so config-scoped credentials are visible to implicit provider resolution paths. (#32295) Thanks @hsiaoa.
|
- Models/config env propagation: apply `config.env.vars` before implicit provider discovery in models bootstrap so config-scoped credentials are visible to implicit provider resolution paths. (#32295) Thanks @hsiaoa.
|
||||||
- Hooks/runtime stability: keep the internal hook handler registry on a `globalThis` singleton so hook registration/dispatch remains consistent when bundling emits duplicate module copies. (#32292) Thanks @Drickon.
|
- Hooks/runtime stability: keep the internal hook handler registry on a `globalThis` singleton so hook registration/dispatch remains consistent when bundling emits duplicate module copies. (#32292) Thanks @Drickon.
|
||||||
- Hooks/plugin context parity: ensure `llm_input` hooks in embedded attempts receive the same `trigger` and `channelId`-aware `hookCtx` used by the other hook phases, preserving channel/trigger-scoped plugin behavior. (#28623) Thanks @davidrudduck and @vincentkoc.
|
- Hooks/plugin context parity: ensure `llm_input` hooks in embedded attempts receive the same `trigger` and `channelId`-aware `hookCtx` used by the other hook phases, preserving channel/trigger-scoped plugin behavior. (#28623) Thanks @davidrudduck and @vincentkoc.
|
||||||
|
|||||||
@@ -61,4 +61,54 @@ describe("lookupContextTokens", () => {
|
|||||||
process.argv = argvSnapshot;
|
process.argv = argvSnapshot;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("retries config loading after backoff when an initial load fails", async () => {
|
||||||
|
vi.useFakeTimers();
|
||||||
|
const loadConfigMock = vi
|
||||||
|
.fn()
|
||||||
|
.mockImplementationOnce(() => {
|
||||||
|
throw new Error("transient");
|
||||||
|
})
|
||||||
|
.mockImplementation(() => ({
|
||||||
|
models: {
|
||||||
|
providers: {
|
||||||
|
openrouter: {
|
||||||
|
models: [{ id: "openrouter/claude-sonnet", contextWindow: 654_321 }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.doMock("../config/config.js", () => ({
|
||||||
|
loadConfig: loadConfigMock,
|
||||||
|
}));
|
||||||
|
vi.doMock("./models-config.js", () => ({
|
||||||
|
ensureOpenClawModelsJson: vi.fn(async () => {}),
|
||||||
|
}));
|
||||||
|
vi.doMock("./agent-paths.js", () => ({
|
||||||
|
resolveOpenClawAgentDir: () => "/tmp/openclaw-agent",
|
||||||
|
}));
|
||||||
|
vi.doMock("./pi-model-discovery.js", () => ({
|
||||||
|
discoverAuthStorage: vi.fn(() => ({})),
|
||||||
|
discoverModels: vi.fn(() => ({
|
||||||
|
getAll: () => [],
|
||||||
|
})),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const argvSnapshot = process.argv;
|
||||||
|
process.argv = ["node", "openclaw", "config", "validate"];
|
||||||
|
try {
|
||||||
|
const { lookupContextTokens } = await import("./context.js");
|
||||||
|
expect(lookupContextTokens("openrouter/claude-sonnet")).toBeUndefined();
|
||||||
|
expect(loadConfigMock).toHaveBeenCalledTimes(1);
|
||||||
|
expect(lookupContextTokens("openrouter/claude-sonnet")).toBeUndefined();
|
||||||
|
expect(loadConfigMock).toHaveBeenCalledTimes(1);
|
||||||
|
await vi.advanceTimersByTimeAsync(1_000);
|
||||||
|
expect(lookupContextTokens("openrouter/claude-sonnet")).toBe(654_321);
|
||||||
|
expect(loadConfigMock).toHaveBeenCalledTimes(2);
|
||||||
|
} finally {
|
||||||
|
process.argv = argvSnapshot;
|
||||||
|
vi.useRealTimers();
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
import { loadConfig } from "../config/config.js";
|
import { loadConfig } from "../config/config.js";
|
||||||
import type { OpenClawConfig } from "../config/config.js";
|
import type { OpenClawConfig } from "../config/config.js";
|
||||||
|
import { computeBackoff, type BackoffPolicy } from "../infra/backoff.js";
|
||||||
import { consumeRootOptionToken, FLAG_TERMINATOR } from "../infra/cli-root-options.js";
|
import { consumeRootOptionToken, FLAG_TERMINATOR } from "../infra/cli-root-options.js";
|
||||||
import { resolveOpenClawAgentDir } from "./agent-paths.js";
|
import { resolveOpenClawAgentDir } from "./agent-paths.js";
|
||||||
import { ensureOpenClawModelsJson } from "./models-config.js";
|
import { ensureOpenClawModelsJson } from "./models-config.js";
|
||||||
@@ -19,6 +20,12 @@ type AgentModelEntry = { params?: Record<string, unknown> };
|
|||||||
|
|
||||||
const ANTHROPIC_1M_MODEL_PREFIXES = ["claude-opus-4", "claude-sonnet-4"] as const;
|
const ANTHROPIC_1M_MODEL_PREFIXES = ["claude-opus-4", "claude-sonnet-4"] as const;
|
||||||
export const ANTHROPIC_CONTEXT_1M_TOKENS = 1_048_576;
|
export const ANTHROPIC_CONTEXT_1M_TOKENS = 1_048_576;
|
||||||
|
const CONFIG_LOAD_RETRY_POLICY: BackoffPolicy = {
|
||||||
|
initialMs: 1_000,
|
||||||
|
maxMs: 60_000,
|
||||||
|
factor: 2,
|
||||||
|
jitter: 0,
|
||||||
|
};
|
||||||
|
|
||||||
export function applyDiscoveredContextWindows(params: {
|
export function applyDiscoveredContextWindows(params: {
|
||||||
cache: Map<string, number>;
|
cache: Map<string, number>;
|
||||||
@@ -68,7 +75,9 @@ export function applyConfiguredContextWindows(params: {
|
|||||||
|
|
||||||
const MODEL_CACHE = new Map<string, number>();
|
const MODEL_CACHE = new Map<string, number>();
|
||||||
let loadPromise: Promise<void> | null = null;
|
let loadPromise: Promise<void> | null = null;
|
||||||
let configuredWindowsPrimed = false;
|
let configuredConfig: OpenClawConfig | undefined;
|
||||||
|
let configLoadFailures = 0;
|
||||||
|
let nextConfigLoadAttemptAtMs = 0;
|
||||||
|
|
||||||
function getCommandPathFromArgv(argv: string[]): string[] {
|
function getCommandPathFromArgv(argv: string[]): string[] {
|
||||||
const args = argv.slice(2);
|
const args = argv.slice(2);
|
||||||
@@ -100,33 +109,42 @@ function shouldSkipEagerContextWindowWarmup(argv: string[] = process.argv): bool
|
|||||||
}
|
}
|
||||||
|
|
||||||
function primeConfiguredContextWindows(): OpenClawConfig | undefined {
|
function primeConfiguredContextWindows(): OpenClawConfig | undefined {
|
||||||
if (configuredWindowsPrimed) {
|
if (configuredConfig) {
|
||||||
|
return configuredConfig;
|
||||||
|
}
|
||||||
|
if (Date.now() < nextConfigLoadAttemptAtMs) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
configuredWindowsPrimed = true;
|
|
||||||
try {
|
try {
|
||||||
const cfg = loadConfig();
|
const cfg = loadConfig();
|
||||||
applyConfiguredContextWindows({
|
applyConfiguredContextWindows({
|
||||||
cache: MODEL_CACHE,
|
cache: MODEL_CACHE,
|
||||||
modelsConfig: cfg.models as ModelsConfig | undefined,
|
modelsConfig: cfg.models as ModelsConfig | undefined,
|
||||||
});
|
});
|
||||||
|
configuredConfig = cfg;
|
||||||
|
configLoadFailures = 0;
|
||||||
|
nextConfigLoadAttemptAtMs = 0;
|
||||||
return cfg;
|
return cfg;
|
||||||
} catch {
|
} catch {
|
||||||
// If config can't be loaded, leave cache empty.
|
configLoadFailures += 1;
|
||||||
|
const backoffMs = computeBackoff(CONFIG_LOAD_RETRY_POLICY, configLoadFailures);
|
||||||
|
nextConfigLoadAttemptAtMs = Date.now() + backoffMs;
|
||||||
|
// If config can't be loaded, leave cache empty and retry after backoff.
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function ensureContextWindowCacheLoaded(): Promise<void> {
|
function ensureContextWindowCacheLoaded(): Promise<void> {
|
||||||
const cfg = primeConfiguredContextWindows();
|
|
||||||
if (loadPromise) {
|
if (loadPromise) {
|
||||||
return loadPromise;
|
return loadPromise;
|
||||||
}
|
}
|
||||||
loadPromise = (async () => {
|
|
||||||
if (!cfg) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
const cfg = primeConfiguredContextWindows();
|
||||||
|
if (!cfg) {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
loadPromise = (async () => {
|
||||||
try {
|
try {
|
||||||
await ensureOpenClawModelsJson(cfg);
|
await ensureOpenClawModelsJson(cfg);
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
Reference in New Issue
Block a user