mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-17 21:10:54 +00:00
fix: detect Ollama "prompt too long" as context overflow error (#34019)
Merged via squash.
Prepared head SHA: 825a402f0f
Co-authored-by: lishuaigit <7495165+lishuaigit@users.noreply.github.com>
Co-authored-by: jalehman <550978+jalehman@users.noreply.github.com>
Reviewed-by: @jalehman
This commit is contained in:
@@ -274,6 +274,10 @@ Docs: https://docs.openclaw.ai
|
||||
- Agents/Anthropic replay: drop replayed assistant thinking blocks for native Anthropic and Bedrock Claude providers so persisted follow-up turns no longer fail on stored thinking blocks. (#44843) Thanks @jmcte.
|
||||
- Docs/Brave pricing: escape literal dollar signs in Brave Search cost text so the docs render the free credit and per-request pricing correctly. (#44989) Thanks @keelanfh.
|
||||
- Feishu/file uploads: preserve literal UTF-8 filenames in `im.file.create` so Chinese and other non-ASCII filenames no longer appear percent-encoded in chat. (#34262) Thanks @fabiaodemianyang and @KangShuaiFu.
|
||||
- Agents/compaction safeguard: trim large kept `toolResult` payloads consistently for budgeting, pruning, and identifier seeding, then restore preserved payloads after prune so oversized safeguard summaries stay stable. (#44133) thanks @SayrWolfridge.
|
||||
- Agents/compaction: compare post-compaction token sanity checks against full-session pre-compaction totals and skip the check when token estimation fails, so sessions with large bootstrap context keep real token counts instead of falling back to unknown. (#28347) thanks @efe-arv.
|
||||
- Discord/gateway startup: treat plain-text and transient `/gateway/bot` metadata fetch failures as transient startup errors so Discord gateway boot no longer crashes on unhandled rejections. (#44397) Thanks @jalehman.
|
||||
- Agents/Ollama overflow: rewrite Ollama `prompt too long` API payloads through the normal context-overflow sanitizer so embedded sessions keep the friendly overflow copy and auto-compaction trigger. (#34019) thanks @lishuaigit.
|
||||
|
||||
## 2026.3.11
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ beforeEach(() => {
|
||||
code: 1,
|
||||
signal: null,
|
||||
killed: false,
|
||||
termination: "exit",
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ beforeEach(() => {
|
||||
warn: warnMock,
|
||||
child: () => logger,
|
||||
};
|
||||
return logger as ReturnType<typeof subsystemModule.createSubsystemLogger>;
|
||||
return logger as unknown as ReturnType<typeof subsystemModule.createSubsystemLogger>;
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -3,12 +3,12 @@ import {
|
||||
createScopedAccountConfigAccessors,
|
||||
formatAllowFromLowercase,
|
||||
} from "openclaw/plugin-sdk/compat";
|
||||
import type { ChannelPlugin } from "openclaw/plugin-sdk/core";
|
||||
import {
|
||||
buildChannelConfigSchema,
|
||||
getChatChannelMeta,
|
||||
normalizeAccountId,
|
||||
TelegramConfigSchema,
|
||||
type ChannelPlugin,
|
||||
type OpenClawConfig,
|
||||
} from "openclaw/plugin-sdk/telegram";
|
||||
import { inspectTelegramAccount } from "./account-inspect.js";
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
resolveThreadSessionKeys,
|
||||
type RoutePeer,
|
||||
} from "openclaw/plugin-sdk/core";
|
||||
import type { ChannelPlugin } from "openclaw/plugin-sdk/core";
|
||||
import {
|
||||
buildChannelConfigSchema,
|
||||
buildTokenChannelStatusSummary,
|
||||
@@ -28,7 +29,6 @@ import {
|
||||
resolveTelegramGroupToolPolicy,
|
||||
TelegramConfigSchema,
|
||||
type ChannelMessageActionAdapter,
|
||||
type ChannelPlugin,
|
||||
type OpenClawConfig,
|
||||
} from "openclaw/plugin-sdk/telegram";
|
||||
import { parseTelegramTopicConversation } from "../../../src/acp/conversation-id.js";
|
||||
|
||||
@@ -230,7 +230,7 @@ export async function probeTlonAccount(account: ConfiguredTlonAccount) {
|
||||
}
|
||||
|
||||
export async function startTlonGatewayAccount(
|
||||
ctx: Parameters<NonNullable<ChannelPlugin["gateway"]>["startAccount"]>[0],
|
||||
ctx: Parameters<NonNullable<NonNullable<ChannelPlugin["gateway"]>["startAccount"]>>[0],
|
||||
) {
|
||||
const account = ctx.account;
|
||||
ctx.setStatus({
|
||||
|
||||
@@ -27,12 +27,12 @@ const tlonSetupWizardProxy = {
|
||||
resolveConfigured: async ({ cfg }) =>
|
||||
await (await loadTlonChannelRuntime()).tlonSetupWizard.status.resolveConfigured({ cfg }),
|
||||
resolveStatusLines: async ({ cfg, configured }) =>
|
||||
await (
|
||||
(await (
|
||||
await loadTlonChannelRuntime()
|
||||
).tlonSetupWizard.status.resolveStatusLines?.({
|
||||
cfg,
|
||||
configured,
|
||||
}),
|
||||
})) ?? [],
|
||||
},
|
||||
introNote: {
|
||||
title: "Tlon setup",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import path from "node:path";
|
||||
import type { DmPolicy } from "openclaw/plugin-sdk/whatsapp";
|
||||
import {
|
||||
DEFAULT_ACCOUNT_ID,
|
||||
type DmPolicy,
|
||||
formatCliCommand,
|
||||
formatDocsLink,
|
||||
normalizeAccountId,
|
||||
@@ -21,7 +21,7 @@ const channel = "whatsapp" as const;
|
||||
|
||||
function mergeWhatsAppConfig(
|
||||
cfg: OpenClawConfig,
|
||||
patch: Partial<NonNullable<OpenClawConfig["channels"]>["whatsapp"]>,
|
||||
patch: Partial<NonNullable<NonNullable<OpenClawConfig["channels"]>["whatsapp"]>>,
|
||||
options?: { unsetOnUndefined?: string[] },
|
||||
): OpenClawConfig {
|
||||
const base = { ...(cfg.channels?.whatsapp ?? {}) } as Record<string, unknown>;
|
||||
|
||||
@@ -35,6 +35,12 @@ describe("formatAssistantErrorText", () => {
|
||||
);
|
||||
expect(formatAssistantErrorText(msg)).toContain("Context overflow");
|
||||
});
|
||||
it("returns context overflow for Ollama 'prompt too long' errors (#34005)", () => {
|
||||
const msg = makeAssistantError(
|
||||
'Ollama API error 400: {"StatusCode":400,"Status":"400 Bad Request","error":"prompt too long; exceeded max context length by 4 tokens"}',
|
||||
);
|
||||
expect(formatAssistantErrorText(msg)).toContain("Context overflow");
|
||||
});
|
||||
it("returns a reasoning-required message for mandatory reasoning endpoint errors", () => {
|
||||
const msg = makeAssistantError(
|
||||
"400 Reasoning is mandatory for this endpoint and cannot be disabled.",
|
||||
|
||||
@@ -42,6 +42,14 @@ describe("sanitizeUserFacingText", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("sanitizes Ollama prompt-too-long payloads through the context-overflow path", () => {
|
||||
const text =
|
||||
'Ollama API error 400: {"StatusCode":400,"Status":"400 Bad Request","error":"prompt too long; exceeded max context length by 4 tokens"}';
|
||||
expect(sanitizeUserFacingText(text, { errorContext: true })).toContain(
|
||||
"Context overflow: prompt too large for the model.",
|
||||
);
|
||||
});
|
||||
|
||||
it.each([
|
||||
"Changelog note: we fixed false positives for `Context overflow: prompt too large for the model. Try /reset (or /new) to start a fresh session, or use a larger-context model.` in 2026.2.9",
|
||||
"nah it failed, hit a context overflow. the prompt was too large for the model. want me to retry it with a different approach?",
|
||||
|
||||
@@ -97,6 +97,7 @@ export function isContextOverflowError(errorMessage?: string): boolean {
|
||||
lower.includes("context length exceeded") ||
|
||||
lower.includes("maximum context length") ||
|
||||
lower.includes("prompt is too long") ||
|
||||
lower.includes("prompt too long") ||
|
||||
lower.includes("exceeds model context window") ||
|
||||
lower.includes("model token limit") ||
|
||||
(hasRequestSizeExceeds && hasContextWindow) ||
|
||||
@@ -211,11 +212,12 @@ export function extractObservedOverflowTokenCount(errorMessage?: string): number
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Allow provider-wrapped API payloads such as "Ollama API error 400: {...}".
|
||||
const ERROR_PAYLOAD_PREFIX_RE =
|
||||
/^(?:error|api\s*error|apierror|openai\s*error|anthropic\s*error|gateway\s*error)[:\s-]+/i;
|
||||
/^(?:error|(?:[a-z][\w-]*\s+)?api\s*error|apierror|openai\s*error|anthropic\s*error|gateway\s*error)(?:\s+\d{3})?[:\s-]+/i;
|
||||
const FINAL_TAG_RE = /<\s*\/?\s*final\s*>/gi;
|
||||
const ERROR_PREFIX_RE =
|
||||
/^(?:error|api\s*error|openai\s*error|anthropic\s*error|gateway\s*error|request failed|failed|exception)[:\s-]+/i;
|
||||
/^(?:error|(?:[a-z][\w-]*\s+)?api\s*error|openai\s*error|anthropic\s*error|gateway\s*error|request failed|failed|exception)(?:\s+\d{3})?[:\s-]+/i;
|
||||
const CONTEXT_OVERFLOW_ERROR_HEAD_RE =
|
||||
/^(?:context overflow:|request_too_large\b|request size exceeds\b|request exceeds the maximum size\b|context length exceeded\b|maximum context length\b|prompt is too long\b|exceeds model context window\b)/i;
|
||||
const HTTP_STATUS_PREFIX_RE = /^(?:http\s*)?(\d{3})\s+(.+)$/i;
|
||||
|
||||
@@ -33,7 +33,6 @@ function sortStrings(values: readonly string[]) {
|
||||
}
|
||||
|
||||
const contractRuntime = createNonExitingRuntime();
|
||||
|
||||
function expectDirectoryEntryShape(entry: ChannelDirectoryEntry) {
|
||||
expect(["user", "group", "channel"]).toContain(entry.kind);
|
||||
expect(typeof entry.id).toBe("string");
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import {
|
||||
dedupeProfileIds,
|
||||
ensureAuthProfileStore,
|
||||
@@ -9,6 +12,7 @@ import { isNonSecretApiKeyMarker } from "../agents/model-auth-markers.js";
|
||||
import { resolveUsableCustomProviderApiKey } from "../agents/model-auth.js";
|
||||
import { normalizeProviderId } from "../agents/model-selection.js";
|
||||
import { loadConfig, type OpenClawConfig } from "../config/config.js";
|
||||
import { resolveRequiredHomeDir } from "../infra/home-dir.js";
|
||||
import { resolveProviderUsageAuthWithPlugin } from "../plugins/provider-runtime.js";
|
||||
import { normalizeSecretInput } from "../utils/normalize-secret-input.js";
|
||||
import type { UsageProviderId } from "./provider-usage.types.js";
|
||||
@@ -28,6 +32,39 @@ type UsageAuthState = {
|
||||
agentDir?: string;
|
||||
};
|
||||
|
||||
function parseGoogleUsageToken(apiKey: string): string {
|
||||
try {
|
||||
const parsed = JSON.parse(apiKey) as { token?: unknown };
|
||||
if (typeof parsed?.token === "string") {
|
||||
return parsed.token;
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
return apiKey;
|
||||
}
|
||||
|
||||
function resolveLegacyZaiUsageToken(env: NodeJS.ProcessEnv): string | undefined {
|
||||
try {
|
||||
const authPath = path.join(
|
||||
resolveRequiredHomeDir(env, os.homedir),
|
||||
".pi",
|
||||
"agent",
|
||||
"auth.json",
|
||||
);
|
||||
if (!fs.existsSync(authPath)) {
|
||||
return undefined;
|
||||
}
|
||||
const parsed = JSON.parse(fs.readFileSync(authPath, "utf8")) as Record<
|
||||
string,
|
||||
{ access?: string }
|
||||
>;
|
||||
return parsed["z-ai"]?.access || parsed.zai?.access;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function resolveProviderApiKeyFromConfigAndStore(params: {
|
||||
state: UsageAuthState;
|
||||
providerIds: string[];
|
||||
@@ -166,6 +203,52 @@ async function resolveProviderUsageAuthViaPlugin(params: {
|
||||
};
|
||||
}
|
||||
|
||||
async function resolveProviderUsageAuthFallback(params: {
|
||||
state: UsageAuthState;
|
||||
provider: UsageProviderId;
|
||||
}): Promise<ProviderAuth | null> {
|
||||
switch (params.provider) {
|
||||
case "anthropic":
|
||||
case "github-copilot":
|
||||
case "openai-codex":
|
||||
return await resolveOAuthToken(params);
|
||||
case "google-gemini-cli": {
|
||||
const auth = await resolveOAuthToken(params);
|
||||
return auth ? { ...auth, token: parseGoogleUsageToken(auth.token) } : null;
|
||||
}
|
||||
case "zai": {
|
||||
const apiKey = resolveProviderApiKeyFromConfigAndStore({
|
||||
state: params.state,
|
||||
providerIds: ["zai", "z-ai"],
|
||||
envDirect: [params.state.env.ZAI_API_KEY, params.state.env.Z_AI_API_KEY],
|
||||
});
|
||||
if (apiKey) {
|
||||
return { provider: "zai", token: apiKey };
|
||||
}
|
||||
const legacyToken = resolveLegacyZaiUsageToken(params.state.env);
|
||||
return legacyToken ? { provider: "zai", token: legacyToken } : null;
|
||||
}
|
||||
case "minimax": {
|
||||
const apiKey = resolveProviderApiKeyFromConfigAndStore({
|
||||
state: params.state,
|
||||
providerIds: ["minimax"],
|
||||
envDirect: [params.state.env.MINIMAX_CODE_PLAN_KEY, params.state.env.MINIMAX_API_KEY],
|
||||
});
|
||||
return apiKey ? { provider: "minimax", token: apiKey } : null;
|
||||
}
|
||||
case "xiaomi": {
|
||||
const apiKey = resolveProviderApiKeyFromConfigAndStore({
|
||||
state: params.state,
|
||||
providerIds: ["xiaomi"],
|
||||
envDirect: [params.state.env.XIAOMI_API_KEY],
|
||||
});
|
||||
return apiKey ? { provider: "xiaomi", token: apiKey } : null;
|
||||
}
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function resolveProviderAuths(params: {
|
||||
providers: UsageProviderId[];
|
||||
auth?: ProviderAuth[];
|
||||
@@ -192,6 +275,14 @@ export async function resolveProviderAuths(params: {
|
||||
});
|
||||
if (pluginAuth) {
|
||||
auths.push(pluginAuth);
|
||||
continue;
|
||||
}
|
||||
const fallbackAuth = await resolveProviderUsageAuthFallback({
|
||||
state,
|
||||
provider,
|
||||
});
|
||||
if (fallbackAuth) {
|
||||
auths.push(fallbackAuth);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,13 @@ import { loadConfig, type OpenClawConfig } from "../config/config.js";
|
||||
import { resolveProviderUsageSnapshotWithPlugin } from "../plugins/provider-runtime.js";
|
||||
import { resolveFetch } from "./fetch.js";
|
||||
import { type ProviderAuth, resolveProviderAuths } from "./provider-usage.auth.js";
|
||||
import {
|
||||
fetchClaudeUsage,
|
||||
fetchCodexUsage,
|
||||
fetchGeminiUsage,
|
||||
fetchMinimaxUsage,
|
||||
fetchZaiUsage,
|
||||
} from "./provider-usage.fetch.js";
|
||||
import {
|
||||
DEFAULT_TIMEOUT_MS,
|
||||
ignoredErrors,
|
||||
@@ -15,6 +22,99 @@ import type {
|
||||
UsageSummary,
|
||||
} from "./provider-usage.types.js";
|
||||
|
||||
async function fetchCopilotUsageFallback(
|
||||
token: string,
|
||||
timeoutMs: number,
|
||||
fetchFn: typeof fetch,
|
||||
): Promise<ProviderUsageSnapshot> {
|
||||
const res = await fetchFn("https://api.github.com/copilot_internal/user", {
|
||||
headers: {
|
||||
Authorization: `token ${token}`,
|
||||
"Editor-Version": "vscode/1.96.2",
|
||||
"User-Agent": "GitHubCopilotChat/0.26.7",
|
||||
"X-Github-Api-Version": "2025-04-01",
|
||||
},
|
||||
signal: AbortSignal.timeout(timeoutMs),
|
||||
});
|
||||
if (!res.ok) {
|
||||
return {
|
||||
provider: "github-copilot",
|
||||
displayName: PROVIDER_LABELS["github-copilot"],
|
||||
windows: [],
|
||||
error: `HTTP ${res.status}`,
|
||||
};
|
||||
}
|
||||
const data = (await res.json()) as {
|
||||
quota_snapshots?: {
|
||||
premium_interactions?: { percent_remaining?: number | null };
|
||||
chat?: { percent_remaining?: number | null };
|
||||
};
|
||||
copilot_plan?: string;
|
||||
};
|
||||
const windows = [];
|
||||
const premiumRemaining = data.quota_snapshots?.premium_interactions?.percent_remaining;
|
||||
if (premiumRemaining !== undefined && premiumRemaining !== null) {
|
||||
windows.push({
|
||||
label: "Premium",
|
||||
usedPercent: Math.max(0, Math.min(100, 100 - premiumRemaining)),
|
||||
});
|
||||
}
|
||||
const chatRemaining = data.quota_snapshots?.chat?.percent_remaining;
|
||||
if (chatRemaining !== undefined && chatRemaining !== null) {
|
||||
windows.push({ label: "Chat", usedPercent: Math.max(0, Math.min(100, 100 - chatRemaining)) });
|
||||
}
|
||||
return {
|
||||
provider: "github-copilot",
|
||||
displayName: PROVIDER_LABELS["github-copilot"],
|
||||
windows,
|
||||
plan: data.copilot_plan,
|
||||
};
|
||||
}
|
||||
|
||||
async function fetchProviderUsageSnapshotFallback(params: {
|
||||
auth: ProviderAuth;
|
||||
timeoutMs: number;
|
||||
fetchFn: typeof fetch;
|
||||
}): Promise<ProviderUsageSnapshot> {
|
||||
switch (params.auth.provider) {
|
||||
case "anthropic":
|
||||
return await fetchClaudeUsage(params.auth.token, params.timeoutMs, params.fetchFn);
|
||||
case "github-copilot":
|
||||
return await fetchCopilotUsageFallback(params.auth.token, params.timeoutMs, params.fetchFn);
|
||||
case "google-gemini-cli":
|
||||
return await fetchGeminiUsage(
|
||||
params.auth.token,
|
||||
params.timeoutMs,
|
||||
params.fetchFn,
|
||||
"google-gemini-cli",
|
||||
);
|
||||
case "openai-codex":
|
||||
return await fetchCodexUsage(
|
||||
params.auth.token,
|
||||
params.auth.accountId,
|
||||
params.timeoutMs,
|
||||
params.fetchFn,
|
||||
);
|
||||
case "zai":
|
||||
return await fetchZaiUsage(params.auth.token, params.timeoutMs, params.fetchFn);
|
||||
case "minimax":
|
||||
return await fetchMinimaxUsage(params.auth.token, params.timeoutMs, params.fetchFn);
|
||||
case "xiaomi":
|
||||
return {
|
||||
provider: "xiaomi",
|
||||
displayName: PROVIDER_LABELS.xiaomi,
|
||||
windows: [],
|
||||
};
|
||||
default:
|
||||
return {
|
||||
provider: params.auth.provider,
|
||||
displayName: PROVIDER_LABELS[params.auth.provider],
|
||||
windows: [],
|
||||
error: "Unsupported provider",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
type UsageSummaryOptions = {
|
||||
now?: number;
|
||||
timeoutMs?: number;
|
||||
@@ -56,12 +156,11 @@ async function fetchProviderUsageSnapshot(params: {
|
||||
if (pluginSnapshot) {
|
||||
return pluginSnapshot;
|
||||
}
|
||||
return {
|
||||
provider: params.auth.provider,
|
||||
displayName: PROVIDER_LABELS[params.auth.provider],
|
||||
windows: [],
|
||||
error: "Unsupported provider",
|
||||
};
|
||||
return await fetchProviderUsageSnapshotFallback({
|
||||
auth: params.auth,
|
||||
timeoutMs: params.timeoutMs,
|
||||
fetchFn: params.fetchFn,
|
||||
});
|
||||
}
|
||||
|
||||
export async function loadProviderUsageSummary(
|
||||
|
||||
@@ -22,6 +22,10 @@ export type SecretFileReadResult =
|
||||
error?: unknown;
|
||||
};
|
||||
|
||||
function normalizeSecretReadError(error: unknown): Error {
|
||||
return error instanceof Error ? error : new Error(String(error));
|
||||
}
|
||||
|
||||
export function loadSecretFileSync(
|
||||
filePath: string,
|
||||
label: string,
|
||||
@@ -39,11 +43,12 @@ export function loadSecretFileSync(
|
||||
try {
|
||||
previewStat = fs.lstatSync(resolvedPath);
|
||||
} catch (error) {
|
||||
const normalized = normalizeSecretReadError(error);
|
||||
return {
|
||||
ok: false,
|
||||
resolvedPath,
|
||||
error,
|
||||
message: `Failed to inspect ${label} file at ${resolvedPath}: ${String(error)}`,
|
||||
error: normalized,
|
||||
message: `Failed to inspect ${label} file at ${resolvedPath}: ${String(normalized)}`,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -75,8 +80,9 @@ export function loadSecretFileSync(
|
||||
maxBytes,
|
||||
});
|
||||
if (!opened.ok) {
|
||||
const error =
|
||||
opened.reason === "validation" ? new Error("security validation failed") : opened.error;
|
||||
const error = normalizeSecretReadError(
|
||||
opened.reason === "validation" ? new Error("security validation failed") : opened.error,
|
||||
);
|
||||
return {
|
||||
ok: false,
|
||||
resolvedPath,
|
||||
@@ -97,11 +103,12 @@ export function loadSecretFileSync(
|
||||
}
|
||||
return { ok: true, secret, resolvedPath };
|
||||
} catch (error) {
|
||||
const normalized = normalizeSecretReadError(error);
|
||||
return {
|
||||
ok: false,
|
||||
resolvedPath,
|
||||
error,
|
||||
message: `Failed to read ${label} file at ${resolvedPath}: ${String(error)}`,
|
||||
error: normalized,
|
||||
message: `Failed to read ${label} file at ${resolvedPath}: ${String(normalized)}`,
|
||||
};
|
||||
} finally {
|
||||
fs.closeSync(opened.fd);
|
||||
|
||||
@@ -137,10 +137,7 @@ describe("warning filter", () => {
|
||||
seenWarnings.find((warning) => warning.code === "OPENCLAW_TEST_WARNING"),
|
||||
).toBeDefined();
|
||||
expect(
|
||||
seenWarnings.find(
|
||||
(warning) =>
|
||||
warning.code === "DEP0040" && warning.message === "The punycode module is deprecated.",
|
||||
),
|
||||
seenWarnings.find((warning) => warning.message === "The punycode module is deprecated."),
|
||||
).toBeDefined();
|
||||
expect(stderrWrites.join("")).toContain("Visible warning");
|
||||
} finally {
|
||||
|
||||
@@ -75,6 +75,20 @@ export function installProcessWarningFilter(): void {
|
||||
if (shouldIgnoreWarning(normalizeWarningArgs(args))) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
args[0] instanceof Error &&
|
||||
args[1] &&
|
||||
typeof args[1] === "object" &&
|
||||
!Array.isArray(args[1])
|
||||
) {
|
||||
const warning = args[0];
|
||||
const emitted = Object.assign(new Error(warning.message), {
|
||||
name: warning.name,
|
||||
code: (warning as Error & { code?: string }).code,
|
||||
});
|
||||
process.emit("warning", emitted);
|
||||
return;
|
||||
}
|
||||
return Reflect.apply(originalEmitWarning, process, args);
|
||||
}) as typeof process.emitWarning;
|
||||
|
||||
|
||||
@@ -2,10 +2,8 @@ import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { build } from "tsdown";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
buildPluginSdkEntrySources,
|
||||
buildPluginSdkPackageExports,
|
||||
buildPluginSdkSpecifiers,
|
||||
pluginSdkEntrypoints,
|
||||
@@ -119,24 +117,16 @@ describe("plugin-sdk exports", () => {
|
||||
});
|
||||
|
||||
it("emits importable bundled subpath entries", { timeout: 240_000 }, async () => {
|
||||
const outDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-plugin-sdk-build-"));
|
||||
const fixtureDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-plugin-sdk-consumer-"));
|
||||
const repoDistDir = path.join(process.cwd(), "dist");
|
||||
|
||||
try {
|
||||
await build({
|
||||
clean: true,
|
||||
config: false,
|
||||
dts: false,
|
||||
entry: buildPluginSdkEntrySources(),
|
||||
env: { NODE_ENV: "production" },
|
||||
fixedExtension: false,
|
||||
logLevel: "error",
|
||||
outDir,
|
||||
platform: "node",
|
||||
});
|
||||
await expect(fs.access(path.join(repoDistDir, "plugin-sdk"))).resolves.toBeUndefined();
|
||||
|
||||
for (const entry of pluginSdkEntrypoints) {
|
||||
const module = await import(pathToFileURL(path.join(outDir, `${entry}.js`)).href);
|
||||
const module = await import(
|
||||
pathToFileURL(path.join(repoDistDir, "plugin-sdk", `${entry}.js`)).href
|
||||
);
|
||||
expect(module).toBeTypeOf("object");
|
||||
}
|
||||
|
||||
@@ -144,8 +134,8 @@ describe("plugin-sdk exports", () => {
|
||||
const consumerDir = path.join(fixtureDir, "consumer");
|
||||
const consumerEntry = path.join(consumerDir, "import-plugin-sdk.mjs");
|
||||
|
||||
await fs.mkdir(path.join(packageDir, "dist"), { recursive: true });
|
||||
await fs.symlink(outDir, path.join(packageDir, "dist", "plugin-sdk"), "dir");
|
||||
await fs.mkdir(packageDir, { recursive: true });
|
||||
await fs.symlink(repoDistDir, path.join(packageDir, "dist"), "dir");
|
||||
await fs.writeFile(
|
||||
path.join(packageDir, "package.json"),
|
||||
JSON.stringify(
|
||||
@@ -178,7 +168,6 @@ describe("plugin-sdk exports", () => {
|
||||
Object.fromEntries(pluginSdkSpecifiers.map((specifier: string) => [specifier, "object"])),
|
||||
);
|
||||
} finally {
|
||||
await fs.rm(outDir, { recursive: true, force: true });
|
||||
await fs.rm(fixtureDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
resolveOwningPluginIdsForProvider,
|
||||
resolvePluginProviders,
|
||||
} from "./providers.js";
|
||||
import { resolvePluginCacheInputs } from "./roots.js";
|
||||
import type {
|
||||
ProviderAuthDoctorHintContext,
|
||||
ProviderAugmentModelCatalogContext,
|
||||
@@ -76,8 +77,16 @@ function resolveHookProviderCacheBucket(params: {
|
||||
return bucket;
|
||||
}
|
||||
|
||||
function buildHookProviderCacheKey(params: { workspaceDir?: string; onlyPluginIds?: string[] }) {
|
||||
return `${params.workspaceDir ?? ""}::${JSON.stringify(params.onlyPluginIds ?? [])}`;
|
||||
function buildHookProviderCacheKey(params: {
|
||||
workspaceDir?: string;
|
||||
onlyPluginIds?: string[];
|
||||
env?: NodeJS.ProcessEnv;
|
||||
}) {
|
||||
const { roots } = resolvePluginCacheInputs({
|
||||
workspaceDir: params.workspaceDir,
|
||||
env: params.env,
|
||||
});
|
||||
return `${roots.workspace ?? ""}::${roots.global}::${roots.stock ?? ""}::${JSON.stringify(params.onlyPluginIds ?? [])}`;
|
||||
}
|
||||
|
||||
export function resetProviderRuntimeHookCacheForTest(): void {
|
||||
@@ -105,6 +114,7 @@ function resolveProviderPluginsForHooks(params: {
|
||||
const cacheKey = buildHookProviderCacheKey({
|
||||
workspaceDir: params.workspaceDir,
|
||||
onlyPluginIds: params.onlyPluginIds,
|
||||
env,
|
||||
});
|
||||
const cached = cacheBucket.get(cacheKey);
|
||||
if (cached) {
|
||||
|
||||
@@ -153,9 +153,7 @@ describe("stageBundledPluginRuntime", () => {
|
||||
description: string;
|
||||
acceptsArgs: boolean;
|
||||
}>;
|
||||
matchPluginCommand: (
|
||||
commandBody: string,
|
||||
) => {
|
||||
matchPluginCommand: (commandBody: string) => {
|
||||
command: { handler: ({ args }: { args?: string }) => Promise<{ text: string }> };
|
||||
args?: string;
|
||||
} | null;
|
||||
|
||||
Reference in New Issue
Block a user