mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-12 17:51:22 +00:00
refactor: dedupe agent lowercase helpers
This commit is contained in:
@@ -48,7 +48,7 @@ const CLAUDE_CODE_TOOLS = [
|
||||
"WebSearch",
|
||||
] as const;
|
||||
const CLAUDE_CODE_TOOL_LOOKUP = new Map(
|
||||
CLAUDE_CODE_TOOLS.map((tool) => [tool.toLowerCase(), tool]),
|
||||
CLAUDE_CODE_TOOLS.map((tool) => [normalizeLowercaseStringOrEmpty(tool), tool]),
|
||||
);
|
||||
|
||||
type AnthropicTransportModel = Model<"anthropic-messages"> & {
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
refreshProviderOAuthCredentialWithPlugin,
|
||||
} from "../../plugins/provider-runtime.runtime.js";
|
||||
import { resolveSecretRefString, type SecretRefResolveCache } from "../../secrets/resolve.js";
|
||||
import { normalizeLowercaseStringOrEmpty } from "../../shared/string-coerce.js";
|
||||
import { refreshChutesTokens } from "../chutes-oauth.js";
|
||||
import { writeCodexCliCredentials } from "../cli-credentials.js";
|
||||
import { AUTH_STORE_LOCK_OPTIONS, log } from "./constants.js";
|
||||
@@ -124,7 +125,7 @@ function extractErrorMessage(error: unknown): string {
|
||||
}
|
||||
|
||||
function isRefreshTokenReusedError(error: unknown): boolean {
|
||||
const message = extractErrorMessage(error).toLowerCase();
|
||||
const message = normalizeLowercaseStringOrEmpty(extractErrorMessage(error));
|
||||
return (
|
||||
message.includes("refresh_token_reused") ||
|
||||
message.includes("refresh token has already been used") ||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { ExecAsk, ExecSecurity, SystemRunApprovalPlan } from "../infra/exec-approvals.js";
|
||||
import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js";
|
||||
import {
|
||||
DEFAULT_APPROVAL_REQUEST_TIMEOUT_MS,
|
||||
DEFAULT_APPROVAL_TIMEOUT_MS,
|
||||
@@ -120,7 +121,7 @@ export async function waitForExecApprovalDecision(id: string): Promise<string |
|
||||
return parseDecision(decisionResult).value;
|
||||
} catch (err) {
|
||||
// Timeout/cleanup path: treat missing/expired as no decision so askFallback applies.
|
||||
const message = String(err).toLowerCase();
|
||||
const message = normalizeLowercaseStringOrEmpty(String(err));
|
||||
if (message.includes("approval expired or not found")) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -17,7 +17,11 @@ import {
|
||||
} from "../infra/shell-env.js";
|
||||
import { logInfo } from "../logger.js";
|
||||
import { parseAgentSessionKey, resolveAgentIdFromSessionKey } from "../routing/session-key.js";
|
||||
import { normalizeOptionalString } from "../shared/string-coerce.js";
|
||||
import {
|
||||
normalizeLowercaseStringOrEmpty,
|
||||
normalizeOptionalLowercaseString,
|
||||
normalizeOptionalString,
|
||||
} from "../shared/string-coerce.js";
|
||||
import { splitShellArgs } from "../utils/shell-argv.js";
|
||||
import { markBackgrounded } from "./bash-process-registry.js";
|
||||
import { processGatewayAllowlist } from "./bash-tools.exec-host-gateway.js";
|
||||
@@ -109,7 +113,7 @@ function isEnvExecutableToken(token: string | undefined): boolean {
|
||||
if (!token) {
|
||||
return false;
|
||||
}
|
||||
const base = token.split(/[\\/]/u).at(-1)?.toLowerCase() ?? "";
|
||||
const base = normalizeOptionalLowercaseString(token.split(/[\\/]/u).at(-1)) ?? "";
|
||||
const normalizedBase = base.endsWith(".exe") ? base.slice(0, -4) : base;
|
||||
return normalizedBase === "env";
|
||||
}
|
||||
@@ -158,7 +162,7 @@ function findFirstPythonScriptArg(tokens: string[]): string | null {
|
||||
const token = tokens[i];
|
||||
if (token === "--") {
|
||||
const next = tokens[i + 1];
|
||||
return next?.toLowerCase().endsWith(".py") ? next : null;
|
||||
return normalizeLowercaseStringOrEmpty(next).endsWith(".py") ? next : null;
|
||||
}
|
||||
if (token === "-") {
|
||||
return null;
|
||||
@@ -176,7 +180,7 @@ function findFirstPythonScriptArg(tokens: string[]): string | null {
|
||||
if (token.startsWith("-")) {
|
||||
continue;
|
||||
}
|
||||
return token.toLowerCase().endsWith(".py") ? token : null;
|
||||
return normalizeLowercaseStringOrEmpty(token).endsWith(".py") ? token : null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -191,7 +195,7 @@ function findNodeScriptArgs(tokens: string[]): string[] {
|
||||
if (token === "--") {
|
||||
if (!hasInlineEvalOrPrint && !entryScript) {
|
||||
const next = tokens[i + 1];
|
||||
if (next?.toLowerCase().endsWith(".js")) {
|
||||
if (normalizeLowercaseStringOrEmpty(next).endsWith(".js")) {
|
||||
entryScript = next;
|
||||
}
|
||||
}
|
||||
@@ -214,7 +218,7 @@ function findNodeScriptArgs(tokens: string[]): string[] {
|
||||
}
|
||||
if (optionsWithSeparateValue.has(token)) {
|
||||
const next = tokens[i + 1];
|
||||
if (next?.toLowerCase().endsWith(".js")) {
|
||||
if (normalizeLowercaseStringOrEmpty(next).endsWith(".js")) {
|
||||
preloadScripts.push(next);
|
||||
}
|
||||
i += 1;
|
||||
@@ -228,7 +232,7 @@ function findNodeScriptArgs(tokens: string[]): string[] {
|
||||
const inlineValue = token.startsWith("-r")
|
||||
? token.slice(2)
|
||||
: token.slice(token.indexOf("=") + 1);
|
||||
if (inlineValue.toLowerCase().endsWith(".js")) {
|
||||
if (normalizeLowercaseStringOrEmpty(inlineValue).endsWith(".js")) {
|
||||
preloadScripts.push(inlineValue);
|
||||
}
|
||||
continue;
|
||||
@@ -236,7 +240,11 @@ function findNodeScriptArgs(tokens: string[]): string[] {
|
||||
if (token.startsWith("-")) {
|
||||
continue;
|
||||
}
|
||||
if (!hasInlineEvalOrPrint && !entryScript && token.toLowerCase().endsWith(".js")) {
|
||||
if (
|
||||
!hasInlineEvalOrPrint &&
|
||||
!entryScript &&
|
||||
normalizeLowercaseStringOrEmpty(token).endsWith(".js")
|
||||
) {
|
||||
entryScript = token;
|
||||
}
|
||||
break;
|
||||
@@ -258,7 +266,7 @@ function extractInterpreterScriptTargetFromArgv(
|
||||
while (commandIdx < argv.length && /^[A-Za-z_][A-Za-z0-9_]*=.*$/u.test(argv[commandIdx])) {
|
||||
commandIdx += 1;
|
||||
}
|
||||
const executable = argv[commandIdx]?.toLowerCase();
|
||||
const executable = normalizeOptionalLowercaseString(argv[commandIdx]);
|
||||
if (!executable) {
|
||||
return null;
|
||||
}
|
||||
@@ -550,7 +558,8 @@ function hasUnquotedScriptHint(raw: string): boolean {
|
||||
let token = "";
|
||||
|
||||
const flushToken = (): boolean => {
|
||||
if (token.toLowerCase().endsWith(".py") || token.toLowerCase().endsWith(".js")) {
|
||||
const normalizedToken = normalizeLowercaseStringOrEmpty(token);
|
||||
if (normalizedToken.endsWith(".py") || normalizedToken.endsWith(".js")) {
|
||||
return true;
|
||||
}
|
||||
token = "";
|
||||
@@ -627,7 +636,7 @@ function resolveLeadingShellSegmentExecutable(rawSegment: string): string | unde
|
||||
) {
|
||||
commandIdx += 1;
|
||||
}
|
||||
return normalizedArgv[commandIdx]?.toLowerCase();
|
||||
return normalizeOptionalLowercaseString(normalizedArgv[commandIdx]);
|
||||
}
|
||||
|
||||
function analyzeInterpreterHeuristicsFromUnquoted(raw: string): {
|
||||
@@ -666,7 +675,7 @@ function extractShellWrappedCommandPayload(
|
||||
if (!executable) {
|
||||
return null;
|
||||
}
|
||||
const executableBase = executable.split(/[\\/]/u).at(-1)?.toLowerCase() ?? "";
|
||||
const executableBase = normalizeOptionalLowercaseString(executable.split(/[\\/]/u).at(-1)) ?? "";
|
||||
const normalizedExecutable = executableBase.endsWith(".exe")
|
||||
? executableBase.slice(0, -4)
|
||||
: executableBase;
|
||||
@@ -724,7 +733,7 @@ function shouldFailClosedInterpreterPreflight(command: string): {
|
||||
) {
|
||||
commandIdx += 1;
|
||||
}
|
||||
const directExecutable = argv?.[commandIdx]?.toLowerCase();
|
||||
const directExecutable = normalizeOptionalLowercaseString(argv?.[commandIdx]);
|
||||
const args = argv ? argv.slice(commandIdx + 1) : [];
|
||||
|
||||
const isDirectPythonExecutable = Boolean(
|
||||
@@ -770,7 +779,7 @@ function shouldFailClosedInterpreterPreflight(command: string): {
|
||||
) {
|
||||
commandIdx += 1;
|
||||
}
|
||||
const executable = normalizedArgv[commandIdx]?.toLowerCase();
|
||||
const executable = normalizeOptionalLowercaseString(normalizedArgv[commandIdx]);
|
||||
if (!executable) {
|
||||
return false;
|
||||
}
|
||||
@@ -988,9 +997,9 @@ function parseExecApprovalShellCommand(raw: string): ParsedExecApprovalCommand |
|
||||
return {
|
||||
approvalId: match[1],
|
||||
decision:
|
||||
match[2].toLowerCase() === "always"
|
||||
normalizeLowercaseStringOrEmpty(match[2]) === "always"
|
||||
? "allow-always"
|
||||
: (match[2].toLowerCase() as ParsedExecApprovalCommand["decision"]),
|
||||
: (normalizeLowercaseStringOrEmpty(match[2]) as ParsedExecApprovalCommand["decision"]),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { AgentToolResult } from "@mariozechner/pi-agent-core";
|
||||
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { logWarn } from "../logger.js";
|
||||
import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js";
|
||||
import {
|
||||
buildSafeToolName,
|
||||
normalizeReservedToolNames,
|
||||
@@ -95,7 +96,7 @@ export async function materializeBundleMcpToolsForRun(params: {
|
||||
`bundle-mcp: tool "${tool.toolName}" from server "${tool.serverName}" registered as "${safeToolName}" to keep the tool name provider-safe.`,
|
||||
);
|
||||
}
|
||||
reservedNames.add(safeToolName.toLowerCase());
|
||||
reservedNames.add(normalizeLowercaseStringOrEmpty(safeToolName));
|
||||
tools.push({
|
||||
name: safeToolName,
|
||||
label: tool.title ?? tool.toolName,
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { normalizeOptionalLowercaseString } from "../shared/string-coerce.js";
|
||||
import {
|
||||
normalizeLowercaseStringOrEmpty,
|
||||
normalizeOptionalLowercaseString,
|
||||
} from "../shared/string-coerce.js";
|
||||
|
||||
const TOOL_NAME_SAFE_RE = /[^A-Za-z0-9_-]/g;
|
||||
export const TOOL_NAME_SEPARATOR = "__";
|
||||
@@ -18,12 +21,12 @@ export function sanitizeServerName(raw: string, usedNames: Set<string>): string
|
||||
const base = sanitizeToolFragment(raw, "mcp", TOOL_NAME_MAX_PREFIX);
|
||||
let candidate = base;
|
||||
let n = 2;
|
||||
while (usedNames.has(candidate.toLowerCase())) {
|
||||
while (usedNames.has(normalizeLowercaseStringOrEmpty(candidate))) {
|
||||
const suffix = `-${n}`;
|
||||
candidate = `${base.slice(0, Math.max(1, TOOL_NAME_MAX_PREFIX - suffix.length))}${suffix}`;
|
||||
n += 1;
|
||||
}
|
||||
usedNames.add(candidate.toLowerCase());
|
||||
usedNames.add(normalizeLowercaseStringOrEmpty(candidate));
|
||||
return candidate;
|
||||
}
|
||||
|
||||
@@ -53,7 +56,7 @@ export function buildSafeToolName(params: {
|
||||
let candidateToolName = truncatedToolName || "tool";
|
||||
let candidate = `${params.serverName}${TOOL_NAME_SEPARATOR}${candidateToolName}`;
|
||||
let n = 2;
|
||||
while (params.reservedNames.has(candidate.toLowerCase())) {
|
||||
while (params.reservedNames.has(normalizeLowercaseStringOrEmpty(candidate))) {
|
||||
const suffix = `-${n}`;
|
||||
candidateToolName = `${(truncatedToolName || "tool").slice(0, Math.max(1, maxToolChars - suffix.length))}${suffix}`;
|
||||
candidate = `${params.serverName}${TOOL_NAME_SEPARATOR}${candidateToolName}`;
|
||||
|
||||
@@ -8,7 +8,10 @@ import {
|
||||
parseApiErrorInfo,
|
||||
parseApiErrorPayload,
|
||||
} from "../../shared/assistant-error-format.js";
|
||||
import { normalizeOptionalLowercaseString } from "../../shared/string-coerce.js";
|
||||
import {
|
||||
normalizeLowercaseStringOrEmpty,
|
||||
normalizeOptionalLowercaseString,
|
||||
} from "../../shared/string-coerce.js";
|
||||
export {
|
||||
extractLeadingHttpStatus,
|
||||
formatRawAssistantErrorForUi,
|
||||
@@ -121,7 +124,7 @@ function formatTransportErrorCopy(raw: string): string | undefined {
|
||||
if (!raw) {
|
||||
return undefined;
|
||||
}
|
||||
const lower = raw.toLowerCase();
|
||||
const lower = normalizeLowercaseStringOrEmpty(raw);
|
||||
|
||||
if (
|
||||
/\beconnrefused\b/i.test(raw) ||
|
||||
@@ -172,7 +175,7 @@ function formatDiskSpaceErrorCopy(raw: string): string | undefined {
|
||||
if (!raw) {
|
||||
return undefined;
|
||||
}
|
||||
const lower = raw.toLowerCase();
|
||||
const lower = normalizeLowercaseStringOrEmpty(raw);
|
||||
if (
|
||||
/\benospc\b/i.test(raw) ||
|
||||
lower.includes("no space left on device") ||
|
||||
@@ -190,7 +193,7 @@ export function isReasoningConstraintErrorMessage(raw: string): boolean {
|
||||
if (!raw) {
|
||||
return false;
|
||||
}
|
||||
const lower = raw.toLowerCase();
|
||||
const lower = normalizeLowercaseStringOrEmpty(raw);
|
||||
return (
|
||||
lower.includes("reasoning is mandatory") ||
|
||||
lower.includes("reasoning is required") ||
|
||||
@@ -203,7 +206,7 @@ function isInvalidStreamingEventOrderError(raw: string): boolean {
|
||||
if (!raw) {
|
||||
return false;
|
||||
}
|
||||
const lower = raw.toLowerCase();
|
||||
const lower = normalizeLowercaseStringOrEmpty(raw);
|
||||
return (
|
||||
lower.includes("unexpected event order") &&
|
||||
lower.includes("message_start") &&
|
||||
@@ -212,7 +215,7 @@ function isInvalidStreamingEventOrderError(raw: string): boolean {
|
||||
}
|
||||
|
||||
function hasRateLimitTpmHint(raw: string): boolean {
|
||||
const lower = raw.toLowerCase();
|
||||
const lower = normalizeLowercaseStringOrEmpty(raw);
|
||||
return /\btpm\b/i.test(lower) || lower.includes("tokens per minute");
|
||||
}
|
||||
|
||||
@@ -220,7 +223,7 @@ export function isContextOverflowError(errorMessage?: string): boolean {
|
||||
if (!errorMessage) {
|
||||
return false;
|
||||
}
|
||||
const lower = errorMessage.toLowerCase();
|
||||
const lower = normalizeLowercaseStringOrEmpty(errorMessage);
|
||||
|
||||
// Groq uses 413 for TPM (tokens per minute) limits, which is a rate limit, not context overflow.
|
||||
if (hasRateLimitTpmHint(errorMessage)) {
|
||||
@@ -317,7 +320,7 @@ export function isCompactionFailureError(errorMessage?: string): boolean {
|
||||
if (!errorMessage) {
|
||||
return false;
|
||||
}
|
||||
const lower = errorMessage.toLowerCase();
|
||||
const lower = normalizeLowercaseStringOrEmpty(errorMessage);
|
||||
const hasCompactionTerm =
|
||||
lower.includes("summarization failed") ||
|
||||
lower.includes("auto-compaction") ||
|
||||
@@ -865,7 +868,7 @@ function isLikelyHttpErrorText(raw: string): boolean {
|
||||
if (status.code < 400) {
|
||||
return false;
|
||||
}
|
||||
const message = status.rest.toLowerCase();
|
||||
const message = normalizeLowercaseStringOrEmpty(status.rest);
|
||||
return HTTP_ERROR_HINTS.some((hint) => message.includes(hint));
|
||||
}
|
||||
|
||||
@@ -1155,7 +1158,7 @@ function isJsonApiInternalServerError(raw: string): boolean {
|
||||
if (!raw) {
|
||||
return false;
|
||||
}
|
||||
const value = raw.toLowerCase();
|
||||
const value = normalizeLowercaseStringOrEmpty(raw);
|
||||
// Providers wrap transient 5xx errors in JSON payloads like:
|
||||
// {"type":"error","error":{"type":"api_error","message":"Internal server error"}}
|
||||
// Non-standard providers (e.g. MiniMax) may use different message text:
|
||||
@@ -1183,7 +1186,7 @@ export function parseImageDimensionError(raw: string): {
|
||||
if (!raw) {
|
||||
return null;
|
||||
}
|
||||
const lower = raw.toLowerCase();
|
||||
const lower = normalizeLowercaseStringOrEmpty(raw);
|
||||
if (!lower.includes("image dimensions exceed max allowed size")) {
|
||||
return null;
|
||||
}
|
||||
@@ -1208,7 +1211,7 @@ export function parseImageSizeError(raw: string): {
|
||||
if (!raw) {
|
||||
return null;
|
||||
}
|
||||
const lower = raw.toLowerCase();
|
||||
const lower = normalizeLowercaseStringOrEmpty(raw);
|
||||
if (!lower.includes("image exceeds") || !lower.includes("mb")) {
|
||||
return null;
|
||||
}
|
||||
@@ -1241,7 +1244,7 @@ export function isModelNotFoundErrorMessage(raw: string): boolean {
|
||||
if (!raw) {
|
||||
return false;
|
||||
}
|
||||
const lower = raw.toLowerCase();
|
||||
const lower = normalizeLowercaseStringOrEmpty(raw);
|
||||
|
||||
// Direct pattern matches from OpenClaw internals and common providers.
|
||||
if (
|
||||
@@ -1272,7 +1275,7 @@ function isCliSessionExpiredErrorMessage(raw: string): boolean {
|
||||
if (!raw) {
|
||||
return false;
|
||||
}
|
||||
const lower = raw.toLowerCase();
|
||||
const lower = normalizeLowercaseStringOrEmpty(raw);
|
||||
return (
|
||||
lower.includes("session not found") ||
|
||||
lower.includes("session does not exist") ||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { normalizeLowercaseStringOrEmpty } from "../../shared/string-coerce.js";
|
||||
|
||||
type ErrorPattern = RegExp | string;
|
||||
|
||||
const PERIODIC_USAGE_LIMIT_RE =
|
||||
@@ -145,7 +147,7 @@ function matchesErrorPatterns(raw: string, patterns: readonly ErrorPattern[]): b
|
||||
if (!raw) {
|
||||
return false;
|
||||
}
|
||||
const value = raw.toLowerCase();
|
||||
const value = normalizeLowercaseStringOrEmpty(raw);
|
||||
return patterns.some((pattern) =>
|
||||
pattern instanceof RegExp ? pattern.test(value) : value.includes(pattern),
|
||||
);
|
||||
@@ -175,7 +177,7 @@ export function isPeriodicUsageLimitErrorMessage(raw: string): boolean {
|
||||
}
|
||||
|
||||
export function isBillingErrorMessage(raw: string): boolean {
|
||||
const value = raw.toLowerCase();
|
||||
const value = normalizeLowercaseStringOrEmpty(raw);
|
||||
if (!value) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { normalizeLowercaseStringOrEmpty } from "../../shared/string-coerce.js";
|
||||
|
||||
const MIN_DUPLICATE_TEXT_LENGTH = 10;
|
||||
|
||||
/**
|
||||
@@ -8,9 +10,7 @@ const MIN_DUPLICATE_TEXT_LENGTH = 10;
|
||||
* - Collapses multiple spaces to single space
|
||||
*/
|
||||
export function normalizeTextForComparison(text: string): string {
|
||||
return text
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
return normalizeLowercaseStringOrEmpty(text)
|
||||
.replace(/\p{Emoji_Presentation}|\p{Extended_Pictographic}/gu, "")
|
||||
.replace(/\s+/g, " ")
|
||||
.trim();
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { normalizeLowercaseStringOrEmpty } from "../../shared/string-coerce.js";
|
||||
|
||||
/**
|
||||
* Runner abort check. Catches any abort-related message for embedded runners.
|
||||
* More permissive than the core isAbortError since runners need to catch
|
||||
@@ -12,6 +14,8 @@ export function isRunnerAbortError(err: unknown): boolean {
|
||||
return true;
|
||||
}
|
||||
const message =
|
||||
"message" in err && typeof err.message === "string" ? err.message.toLowerCase() : "";
|
||||
"message" in err && typeof err.message === "string"
|
||||
? normalizeLowercaseStringOrEmpty(err.message)
|
||||
: "";
|
||||
return message.includes("aborted");
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { resolveProviderCacheTtlEligibility } from "../../plugins/provider-runtime.js";
|
||||
import { normalizeLowercaseStringOrEmpty } from "../../shared/string-coerce.js";
|
||||
import { isAnthropicFamilyCacheTtlEligible } from "./anthropic-family-cache-semantics.js";
|
||||
import { isGooglePromptCacheEligible } from "./prompt-cache-retention.js";
|
||||
|
||||
@@ -22,8 +23,8 @@ export function isCacheTtlEligibleProvider(
|
||||
modelId: string,
|
||||
modelApi?: string,
|
||||
): boolean {
|
||||
const normalizedProvider = provider.toLowerCase();
|
||||
const normalizedModelId = modelId.toLowerCase();
|
||||
const normalizedProvider = normalizeLowercaseStringOrEmpty(provider);
|
||||
const normalizedModelId = normalizeLowercaseStringOrEmpty(modelId);
|
||||
const pluginEligibility = resolveProviderCacheTtlEligibility({
|
||||
provider: normalizedProvider,
|
||||
context: {
|
||||
|
||||
@@ -7,6 +7,7 @@ import type {
|
||||
PluginHookBeforeModelResolveResult,
|
||||
PluginHookBeforePromptBuildResult,
|
||||
} from "../../plugins/types.js";
|
||||
import { normalizeLowercaseStringOrEmpty } from "../../shared/string-coerce.js";
|
||||
import type { FailoverReason } from "../pi-embedded-helpers/types.js";
|
||||
import type { EmbeddedRunAttemptResult } from "./run/types.js";
|
||||
|
||||
@@ -144,7 +145,7 @@ export const mockedIsCompactionFailureError = vi.fn(() => false);
|
||||
export const mockedIsFailoverAssistantError = vi.fn(() => false);
|
||||
export const mockedIsFailoverErrorMessage = vi.fn(() => false);
|
||||
export const mockedIsLikelyContextOverflowError = vi.fn((msg?: string) => {
|
||||
const lower = (msg ?? "").toLowerCase();
|
||||
const lower = normalizeLowercaseStringOrEmpty(msg ?? "");
|
||||
return (
|
||||
lower.includes("request_too_large") ||
|
||||
lower.includes("context window exceeded") ||
|
||||
@@ -270,7 +271,7 @@ export function resetRunOverflowCompactionHarnessMocks(): void {
|
||||
mockedIsFailoverErrorMessage.mockReturnValue(false);
|
||||
mockedIsLikelyContextOverflowError.mockReset();
|
||||
mockedIsLikelyContextOverflowError.mockImplementation((msg?: string) => {
|
||||
const lower = (msg ?? "").toLowerCase();
|
||||
const lower = normalizeLowercaseStringOrEmpty(msg ?? "");
|
||||
return (
|
||||
lower.includes("request_too_large") ||
|
||||
lower.includes("context window exceeded") ||
|
||||
|
||||
@@ -5,6 +5,7 @@ import { assertNoWindowsNetworkPath, safeFileURLToPath } from "../../../infra/lo
|
||||
import type { PromptImageOrderEntry } from "../../../media/prompt-image-order.js";
|
||||
import { resolveMediaBufferPath, getMediaDir } from "../../../media/store.js";
|
||||
import { loadWebMedia } from "../../../media/web-media.js";
|
||||
import { normalizeLowercaseStringOrEmpty } from "../../../shared/string-coerce.js";
|
||||
import { resolveUserPath } from "../../../utils.js";
|
||||
import type { ImageSanitizationLimits } from "../../image-sanitization.js";
|
||||
import {
|
||||
@@ -83,12 +84,12 @@ export interface DetectedImageRef {
|
||||
* Checks if a file extension indicates an image file.
|
||||
*/
|
||||
function isImageExtension(filePath: string): boolean {
|
||||
const ext = path.extname(filePath).toLowerCase();
|
||||
const ext = normalizeLowercaseStringOrEmpty(path.extname(filePath));
|
||||
return IMAGE_EXTENSIONS.has(ext);
|
||||
}
|
||||
|
||||
function normalizeRefForDedupe(raw: string): string {
|
||||
return process.platform === "win32" ? raw.toLowerCase() : raw;
|
||||
return process.platform === "win32" ? normalizeLowercaseStringOrEmpty(raw) : raw;
|
||||
}
|
||||
|
||||
export function mergePromptAttachmentImages(params: {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { normalizeLowercaseStringOrEmpty } from "../../../shared/string-coerce.js";
|
||||
import { isLikelyMutatingToolName } from "../../tool-mutation.js";
|
||||
import type { EmbeddedRunAttemptResult } from "./types.js";
|
||||
|
||||
@@ -137,13 +138,13 @@ function shouldApplyPlanningOnlyRetryGuard(params: {
|
||||
}
|
||||
|
||||
function normalizeAckPrompt(text: string): string {
|
||||
return text
|
||||
const normalized = text
|
||||
.normalize("NFKC")
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(/[\p{P}\p{S}]+/gu, " ")
|
||||
.replace(/\s+/g, " ")
|
||||
.trim();
|
||||
return normalizeLowercaseStringOrEmpty(normalized);
|
||||
}
|
||||
|
||||
export function isLikelyExecutionAckPrompt(text: string): boolean {
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { TextContent } from "@mariozechner/pi-ai";
|
||||
import { SessionManager } from "@mariozechner/pi-coding-agent";
|
||||
import { formatErrorMessage } from "../../infra/errors.js";
|
||||
import { emitSessionTranscriptUpdate } from "../../sessions/transcript-events.js";
|
||||
import { normalizeLowercaseStringOrEmpty } from "../../shared/string-coerce.js";
|
||||
import { acquireSessionWriteLock } from "../session-write-lock.js";
|
||||
import { log } from "./logger.js";
|
||||
import { formatContextLimitTruncationNotice } from "./tool-result-context-guard.js";
|
||||
@@ -71,7 +72,7 @@ const MIDDLE_OMISSION_MARKER =
|
||||
*/
|
||||
function hasImportantTail(text: string): boolean {
|
||||
// Check last ~2000 chars for error-like patterns
|
||||
const tail = text.slice(-2000).toLowerCase();
|
||||
const tail = normalizeLowercaseStringOrEmpty(text.slice(-2000));
|
||||
return (
|
||||
/\b(error|exception|failed|fatal|traceback|panic|stack trace|errno|exit code)\b/.test(tail) ||
|
||||
// JSON closing — if the output is JSON, the tail has closing structure
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { normalizeLowercaseStringOrEmpty } from "../../../shared/string-coerce.js";
|
||||
import { compileGlobPatterns, matchesAnyGlobPattern } from "../../glob-pattern.js";
|
||||
import type { ContextPruningToolMatch } from "./settings.js";
|
||||
|
||||
function normalizeGlob(value: string) {
|
||||
return String(value ?? "")
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
return normalizeLowercaseStringOrEmpty(String(value ?? ""));
|
||||
}
|
||||
|
||||
export function makeToolPrunablePredicate(
|
||||
|
||||
@@ -124,7 +124,7 @@ export function hasCursorModeSensitiveKeys(request: KeyEncodingRequest): boolean
|
||||
if (hasAnyModifier(parsed.mods)) {
|
||||
return false;
|
||||
}
|
||||
return parsed.base.toLowerCase() in DECCKM_SS3_KEYS;
|
||||
return normalizeLowercaseStringOrEmpty(parsed.base) in DECCKM_SS3_KEYS;
|
||||
}) ?? false
|
||||
);
|
||||
}
|
||||
@@ -186,7 +186,7 @@ function encodeKeyToken(
|
||||
|
||||
const parsed = parseModifiers(token);
|
||||
const base = parsed.base;
|
||||
const baseLower = base.toLowerCase();
|
||||
const baseLower = normalizeLowercaseStringOrEmpty(base);
|
||||
|
||||
if (baseLower === "tab" && parsed.mods.shift) {
|
||||
return `${ESC}[Z`;
|
||||
@@ -240,7 +240,7 @@ function parseModifiers(token: string) {
|
||||
let sawModifiers = false;
|
||||
|
||||
while (rest.length > 2 && rest[1] === "-") {
|
||||
const mod = rest[0].toLowerCase();
|
||||
const mod = normalizeLowercaseStringOrEmpty(rest[0]);
|
||||
if (mod === "c") {
|
||||
mods.ctrl = true;
|
||||
} else if (mod === "m") {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import path from "node:path";
|
||||
import { normalizeAgentId } from "../../routing/session-key.js";
|
||||
import { normalizeLowercaseStringOrEmpty } from "../../shared/string-coerce.js";
|
||||
import { resolveUserPath } from "../../utils.js";
|
||||
import { resolveAgentIdFromSessionKey } from "../agent-scope.js";
|
||||
import { hashTextSha256 } from "./hash.js";
|
||||
@@ -7,8 +8,7 @@ import { hashTextSha256 } from "./hash.js";
|
||||
export function slugifySessionKey(value: string) {
|
||||
const trimmed = value.trim() || "session";
|
||||
const hash = hashTextSha256(trimmed).slice(0, 8);
|
||||
const safe = trimmed
|
||||
.toLowerCase()
|
||||
const safe = normalizeLowercaseStringOrEmpty(trimmed)
|
||||
.replace(/[^a-z0-9._-]+/g, "-")
|
||||
.replace(/^-+|-+$/g, "");
|
||||
const base = safe.slice(0, 32) || "session";
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import path from "node:path";
|
||||
import { normalizeLowercaseStringOrEmpty } from "../../shared/string-coerce.js";
|
||||
import type {
|
||||
CreateSandboxBackendParams,
|
||||
SandboxBackendCommandParams,
|
||||
@@ -291,8 +292,7 @@ function resolveSshRuntimePaths(workspaceRoot: string, scopeKey: string): Resolv
|
||||
|
||||
function buildSshSandboxRuntimeId(scopeKey: string): string {
|
||||
const trimmed = scopeKey.trim() || "session";
|
||||
const safe = trimmed
|
||||
.toLowerCase()
|
||||
const safe = normalizeLowercaseStringOrEmpty(trimmed)
|
||||
.replace(/[^a-z0-9._-]+/g, "-")
|
||||
.replace(/^-+|-+$/g, "")
|
||||
.slice(0, 32);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
import { normalizeLowercaseStringOrEmpty } from "../../shared/string-coerce.js";
|
||||
import { resolveAgentConfig } from "../agent-scope.js";
|
||||
import { compileGlobPatterns, matchesAnyGlobPattern } from "../glob-pattern.js";
|
||||
import { expandToolGroups, normalizeToolName } from "../tool-policy.js";
|
||||
@@ -157,13 +158,15 @@ function filterDefaultDenyForExplicitAllows(params: {
|
||||
function expandResolvedPolicy(policy: SandboxToolPolicy): SandboxToolPolicy {
|
||||
const expandedDeny = expandToolGroups(policy.deny ?? []);
|
||||
let expandedAllow = expandToolGroups(policy.allow ?? []);
|
||||
const expandedDenyLower = expandedDeny.map(normalizeLowercaseStringOrEmpty);
|
||||
const expandedAllowLower = expandedAllow.map(normalizeLowercaseStringOrEmpty);
|
||||
|
||||
// `image` is essential for multimodal workflows; keep the existing sandbox
|
||||
// behavior that auto-includes it for explicit allowlists unless it is denied.
|
||||
if (
|
||||
expandedAllow.length > 0 &&
|
||||
!expandedDeny.map((value) => value.toLowerCase()).includes("image") &&
|
||||
!expandedAllow.map((value) => value.toLowerCase()).includes("image")
|
||||
!expandedDenyLower.includes("image") &&
|
||||
!expandedAllowLower.includes("image")
|
||||
) {
|
||||
expandedAllow = [...expandedAllow, "image"];
|
||||
}
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
import { createSubsystemLogger } from "../../logging/subsystem.js";
|
||||
import { loadEnabledClaudeBundleCommands } from "../../plugins/bundle-commands.js";
|
||||
import {
|
||||
normalizeLowercaseStringOrEmpty,
|
||||
normalizeOptionalLowercaseString,
|
||||
} from "../../shared/string-coerce.js";
|
||||
import { resolveEffectiveAgentSkillFilter } from "./agent-filter.js";
|
||||
import type { SkillEligibilityContext, SkillCommandSpec, SkillEntry } from "./types.js";
|
||||
import {
|
||||
@@ -27,8 +31,7 @@ function debugSkillCommandOnce(
|
||||
}
|
||||
|
||||
function sanitizeSkillCommandName(raw: string): string {
|
||||
const normalized = raw
|
||||
.toLowerCase()
|
||||
const normalized = normalizeLowercaseStringOrEmpty(raw)
|
||||
.replace(/[^a-z0-9_]+/g, "_")
|
||||
.replace(/_+/g, "_")
|
||||
.replace(/^_+|_+$/g, "");
|
||||
@@ -37,7 +40,7 @@ function sanitizeSkillCommandName(raw: string): string {
|
||||
}
|
||||
|
||||
function resolveUniqueSkillCommandName(base: string, used: Set<string>): string {
|
||||
const normalizedBase = base.toLowerCase();
|
||||
const normalizedBase = normalizeLowercaseStringOrEmpty(base);
|
||||
if (!used.has(normalizedBase)) {
|
||||
return base;
|
||||
}
|
||||
@@ -46,7 +49,7 @@ function resolveUniqueSkillCommandName(base: string, used: Set<string>): string
|
||||
const maxBaseLength = Math.max(1, SKILL_COMMAND_MAX_LENGTH - suffix.length);
|
||||
const trimmedBase = base.slice(0, maxBaseLength);
|
||||
const candidate = `${trimmedBase}${suffix}`;
|
||||
const candidateKey = candidate.toLowerCase();
|
||||
const candidateKey = normalizeLowercaseStringOrEmpty(candidate);
|
||||
if (!used.has(candidateKey)) {
|
||||
return candidate;
|
||||
}
|
||||
@@ -85,7 +88,7 @@ export function buildWorkspaceSkillCommandSpecs(
|
||||
const userInvocable = eligible.filter((entry) => entry.invocation?.userInvocable !== false);
|
||||
const used = new Set<string>();
|
||||
for (const reserved of opts?.reservedNames ?? []) {
|
||||
used.add(reserved.toLowerCase());
|
||||
used.add(normalizeLowercaseStringOrEmpty(reserved));
|
||||
}
|
||||
|
||||
const specs: SkillCommandSpec[] = [];
|
||||
@@ -107,20 +110,16 @@ export function buildWorkspaceSkillCommandSpecs(
|
||||
{ rawName, deduped: `/${unique}` },
|
||||
);
|
||||
}
|
||||
used.add(unique.toLowerCase());
|
||||
used.add(normalizeLowercaseStringOrEmpty(unique));
|
||||
const rawDescription = entry.skill.description?.trim() || rawName;
|
||||
const description =
|
||||
rawDescription.length > SKILL_COMMAND_DESCRIPTION_MAX_LENGTH
|
||||
? rawDescription.slice(0, SKILL_COMMAND_DESCRIPTION_MAX_LENGTH - 1) + "…"
|
||||
: rawDescription;
|
||||
const dispatch = (() => {
|
||||
const kindRaw = (
|
||||
entry.frontmatter?.["command-dispatch"] ??
|
||||
entry.frontmatter?.["command_dispatch"] ??
|
||||
""
|
||||
)
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
const kindRaw = normalizeLowercaseStringOrEmpty(
|
||||
entry.frontmatter?.["command-dispatch"] ?? entry.frontmatter?.["command_dispatch"] ?? "",
|
||||
);
|
||||
if (!kindRaw || kindRaw !== "tool") {
|
||||
return undefined;
|
||||
}
|
||||
@@ -139,13 +138,9 @@ export function buildWorkspaceSkillCommandSpecs(
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const argModeRaw = (
|
||||
entry.frontmatter?.["command-arg-mode"] ??
|
||||
entry.frontmatter?.["command_arg_mode"] ??
|
||||
""
|
||||
)
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
const argModeRaw = normalizeOptionalLowercaseString(
|
||||
entry.frontmatter?.["command-arg-mode"] ?? entry.frontmatter?.["command_arg_mode"] ?? "",
|
||||
);
|
||||
const argMode = !argModeRaw || argModeRaw === "raw" ? "raw" : null;
|
||||
if (!argMode) {
|
||||
debugSkillCommandOnce(
|
||||
@@ -187,7 +182,7 @@ export function buildWorkspaceSkillCommandSpecs(
|
||||
{ rawName: entry.rawName, deduped: `/${unique}` },
|
||||
);
|
||||
}
|
||||
used.add(unique.toLowerCase());
|
||||
used.add(normalizeLowercaseStringOrEmpty(unique));
|
||||
const description =
|
||||
entry.description.length > SKILL_COMMAND_DESCRIPTION_MAX_LENGTH
|
||||
? entry.description.slice(0, SKILL_COMMAND_DESCRIPTION_MAX_LENGTH - 1) + "…"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { redactToolDetail } from "../logging/redact.js";
|
||||
import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js";
|
||||
import { shortenHomeInString } from "../utils.js";
|
||||
import {
|
||||
defaultTitle,
|
||||
@@ -46,7 +47,7 @@ export function resolveToolDisplay(params: {
|
||||
meta?: string;
|
||||
}): ToolDisplay {
|
||||
const name = normalizeToolName(params.name);
|
||||
const key = name.toLowerCase();
|
||||
const key = normalizeLowercaseStringOrEmpty(name);
|
||||
const spec = TOOL_MAP[key];
|
||||
const emoji = spec?.emoji ?? FALLBACK.emoji ?? "🧩";
|
||||
const title = spec?.title ?? defaultTitle(name);
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import type { LookupFn } from "../../infra/net/ssrf.js";
|
||||
import { normalizeLowercaseStringOrEmpty } from "../../shared/string-coerce.js";
|
||||
|
||||
export function makeFetchHeaders(map: Record<string, string>): {
|
||||
get: (key: string) => string | null;
|
||||
} {
|
||||
return {
|
||||
get: (key) => map[key.toLowerCase()] ?? null,
|
||||
get: (key) => map[normalizeLowercaseStringOrEmpty(key)] ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user