refactor: dedupe agent lowercase helpers

This commit is contained in:
Peter Steinberger
2026-04-07 12:41:44 +01:00
parent 0cbf99ab42
commit 8e4eaec394
19 changed files with 85 additions and 42 deletions

View File

@@ -45,7 +45,10 @@ import {
normalizeAgentId,
parseAgentSessionKey,
} from "../routing/session-key.js";
import { normalizeOptionalString } from "../shared/string-coerce.js";
import {
normalizeOptionalLowercaseString,
normalizeOptionalString,
} from "../shared/string-coerce.js";
import { createRunningTaskRun } from "../tasks/task-executor.js";
import {
deliveryContextFromSession,
@@ -491,7 +494,7 @@ function resolveConversationIdForThreadBinding(params: {
threadId?: string | number;
groupId?: string;
}): string | undefined {
const channel = params.channel?.trim().toLowerCase();
const channel = normalizeOptionalLowercaseString(params.channel);
const normalizedChannelId = channel ? normalizeChannelId(channel) : null;
const channelKey = normalizedChannelId ?? channel ?? null;
const pluginResolvedConversationId = normalizedChannelId
@@ -532,7 +535,7 @@ function resolveAcpSpawnChannelAccountId(params: {
channel?: string;
accountId?: string;
}): string | undefined {
const channel = params.channel?.trim().toLowerCase();
const channel = normalizeOptionalLowercaseString(params.channel);
const explicitAccountId = params.accountId?.trim();
if (explicitAccountId) {
return explicitAccountId;
@@ -555,7 +558,7 @@ function prepareAcpThreadBinding(params: {
threadId?: string | number;
groupId?: string;
}): { ok: true; binding: PreparedAcpThreadBinding } | { ok: false; error: string } {
const channel = params.channel?.trim().toLowerCase();
const channel = normalizeOptionalLowercaseString(params.channel);
if (!channel) {
return {
ok: false,

View File

@@ -3,6 +3,7 @@ import type { CliBackendConfig } from "../config/types.js";
import { resolveRuntimeCliBackends } from "../plugins/cli-backends.runtime.js";
import { resolvePluginSetupCliBackend } from "../plugins/setup-registry.js";
import type { CliBundleMcpMode } from "../plugins/types.js";
import { normalizeOptionalLowercaseString } from "../shared/string-coerce.js";
import { normalizeProviderId } from "./model-selection.js";
export type ResolvedCliBackend = {
@@ -77,7 +78,9 @@ function pickBackendConfig(
config: Record<string, CliBackendConfig>,
normalizedId: string,
): CliBackendConfig | undefined {
const directKey = Object.keys(config).find((key) => key.trim().toLowerCase() === normalizedId);
const directKey = Object.keys(config).find(
(key) => normalizeOptionalLowercaseString(key) === normalizedId,
);
if (directKey) {
return config[directKey];
}

View File

@@ -11,6 +11,7 @@ import type { CliBackendConfig } from "../../config/types.js";
import { resolvePreferredOpenClawTmpDir } from "../../infra/tmp-openclaw-dir.js";
import { MAX_IMAGE_BYTES } from "../../media/constants.js";
import { extensionForMime } from "../../media/mime.js";
import { normalizeOptionalLowercaseString } from "../../shared/string-coerce.js";
import { buildTtsSystemPromptHint } from "../../tts/tts.js";
import { buildModelAliasLines } from "../model-alias-lines.js";
import { resolveDefaultModelForAgent } from "../model-selection.js";
@@ -28,7 +29,7 @@ export { buildCliSupervisorScopeKey, resolveCliNoOutputTimeoutMs } from "./relia
const CLI_RUN_QUEUE = new KeyedAsyncQueue();
function isClaudeCliProvider(providerId: string): boolean {
return providerId.trim().toLowerCase() === "claude-cli";
return normalizeOptionalLowercaseString(providerId) === "claude-cli";
}
export function enqueueCliRun<T>(key: string, task: () => Promise<T>): Promise<T> {

View File

@@ -1,5 +1,6 @@
import path from "node:path";
import type { CliBackendConfig } from "../../config/types.js";
import { normalizeLowercaseStringOrEmpty } from "../../shared/string-coerce.js";
import {
CLI_FRESH_WATCHDOG_DEFAULTS,
CLI_RESUME_WATCHDOG_DEFAULTS,
@@ -75,11 +76,8 @@ export function buildCliSupervisorScopeKey(params: {
backendId: string;
cliSessionId?: string;
}): string | undefined {
const commandToken = path
.basename(params.backend.command ?? "")
.trim()
.toLowerCase();
const backendToken = params.backendId.trim().toLowerCase();
const commandToken = normalizeLowercaseStringOrEmpty(path.basename(params.backend.command ?? ""));
const backendToken = normalizeLowercaseStringOrEmpty(params.backendId);
const sessionToken = params.cliSessionId?.trim();
if (!sessionToken) {
return undefined;

View File

@@ -1,5 +1,6 @@
import type { OpenClawConfig } from "../config/config.js";
import { resolveOwningPluginIdsForProvider } from "../plugins/providers.js";
import { normalizeOptionalLowercaseString } from "../shared/string-coerce.js";
import { normalizeProviderId } from "./provider-id.js";
type ModelTarget = {
@@ -124,10 +125,13 @@ export function createLiveTargetMatcher(params: {
return true;
}
const normalizedProvider = normalizeProviderId(provider);
const normalizedModelId = modelId.trim().toLowerCase();
const normalizedModelId = normalizeOptionalLowercaseString(modelId);
if (!normalizedModelId) {
return false;
}
const directRef = `${normalizedProvider}/${normalizedModelId}`;
for (const target of modelTargets) {
if (target.raw.toLowerCase() === directRef) {
if (normalizeOptionalLowercaseString(target.raw) === directRef) {
return true;
}
if (target.modelId !== normalizedModelId) {

View File

@@ -8,7 +8,10 @@ import {
import { createSubsystemLogger } from "../logging/subsystem.js";
import { resolveRuntimeCliBackends } from "../plugins/cli-backends.runtime.js";
import { resolvePluginSetupCliBackendRuntime } from "../plugins/setup-registry.runtime.js";
import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js";
import {
normalizeLowercaseStringOrEmpty,
normalizeOptionalLowercaseString,
} from "../shared/string-coerce.js";
import { sanitizeForLog, stripAnsi } from "../terminal/ansi.js";
import {
resolveAgentConfig,
@@ -722,14 +725,13 @@ export function resolveThinkingDefault(params: {
const canonicalKey = modelKey(params.provider, params.model);
const legacyKey = legacyModelKey(params.provider, params.model);
const primarySelection = normalizeModelSelection(params.cfg.agents?.defaults?.model);
const normalizedPrimarySelection =
typeof primarySelection === "string" ? primarySelection.trim().toLowerCase() : undefined;
const normalizedPrimarySelection = normalizeOptionalLowercaseString(primarySelection);
const explicitModelConfigured =
(configuredModels ? canonicalKey in configuredModels : false) ||
Boolean(legacyKey && configuredModels && legacyKey in configuredModels) ||
normalizedPrimarySelection === canonicalKey.toLowerCase() ||
Boolean(legacyKey && normalizedPrimarySelection === legacyKey.toLowerCase()) ||
normalizedPrimarySelection === params.model.trim().toLowerCase();
normalizedPrimarySelection === normalizeLowercaseStringOrEmpty(params.model);
const perModelThinking =
configuredModels?.[canonicalKey]?.params?.thinking ??
(legacyKey ? configuredModels?.[legacyKey]?.params?.thinking : undefined);

View File

@@ -2,6 +2,7 @@ import { spawn, type ChildProcess } from "node:child_process";
import type { AgentToolResult } from "@mariozechner/pi-agent-core";
import type { OpenClawConfig } from "../config/config.js";
import { logDebug, logWarn } from "../logger.js";
import { normalizeOptionalLowercaseString } from "../shared/string-coerce.js";
import { loadEmbeddedPiLspConfig } from "./embedded-pi-lsp.js";
import {
resolveStdioMcpServerLaunchConfig,
@@ -308,7 +309,9 @@ export async function createBundleLspToolRuntime(params: {
}
const reservedNames = new Set(
Array.from(params.reservedToolNames ?? [], (name) => name.trim().toLowerCase()).filter(Boolean),
Array.from(params.reservedToolNames ?? [], (name) =>
normalizeOptionalLowercaseString(name),
).filter(Boolean),
);
const sessions: LspSession[] = [];
const tools: AnyAgentTool[] = [];
@@ -354,7 +357,10 @@ export async function createBundleLspToolRuntime(params: {
const serverTools = buildLspTools(session);
for (const tool of serverTools) {
const normalizedName = tool.name.trim().toLowerCase();
const normalizedName = normalizeOptionalLowercaseString(tool.name);
if (!normalizedName) {
continue;
}
if (reservedNames.has(normalizedName)) {
logWarn(
`bundle-lsp: skipped tool "${tool.name}" from server "${serverName}" because the name already exists.`,

View File

@@ -1,3 +1,5 @@
import { normalizeOptionalLowercaseString } from "../shared/string-coerce.js";
const TOOL_NAME_SAFE_RE = /[^A-Za-z0-9_-]/g;
export const TOOL_NAME_SEPARATOR = "__";
const TOOL_NAME_MAX_PREFIX = 30;
@@ -30,7 +32,9 @@ export function sanitizeToolName(raw: string): string {
}
export function normalizeReservedToolNames(names?: Iterable<string>): Set<string> {
return new Set(Array.from(names ?? [], (name) => name.trim().toLowerCase()).filter(Boolean));
return new Set(
Array.from(names ?? [], (name) => normalizeOptionalLowercaseString(name)).filter(Boolean),
);
}
export function buildSafeToolName(params: {

View File

@@ -8,6 +8,7 @@ import {
parseApiErrorInfo,
parseApiErrorPayload,
} from "../../shared/assistant-error-format.js";
import { normalizeOptionalLowercaseString } from "../../shared/string-coerce.js";
export {
extractLeadingHttpStatus,
formatRawAssistantErrorForUi,
@@ -483,7 +484,7 @@ function hasRetryable402TransientSignal(text: string): boolean {
}
function normalize402Message(raw: string): string {
return raw.trim().toLowerCase().replace(LEADING_402_WRAPPER_RE, "").trim();
return normalizeOptionalLowercaseString(raw)?.replace(LEADING_402_WRAPPER_RE, "").trim() ?? "";
}
function classify402Message(message: string): PaymentRequiredFailoverReason {
@@ -664,7 +665,7 @@ function classifyFailoverReasonFromCode(raw: string | undefined): FailoverReason
}
function isProvider(provider: string | undefined, match: string): boolean {
const normalized = provider?.trim().toLowerCase();
const normalized = normalizeOptionalLowercaseString(provider);
return Boolean(normalized && normalized.includes(match));
}
@@ -894,7 +895,7 @@ export function isRawApiErrorPayload(raw?: string): boolean {
}
function isLikelyProviderErrorType(type?: string): boolean {
const normalized = type?.trim().toLowerCase();
const normalized = normalizeOptionalLowercaseString(type);
if (!normalized) {
return false;
}

View File

@@ -33,6 +33,7 @@ import {
import type { ProviderRuntimeModel } from "../../plugins/types.js";
import { type enqueueCommand, enqueueCommandInLane } from "../../process/command-queue.js";
import { isCronSessionKey, isSubagentSessionKey } from "../../routing/session-key.js";
import { normalizeOptionalLowercaseString } from "../../shared/string-coerce.js";
import { buildTtsSystemPromptHint } from "../../tts/tts.js";
import { resolveUserPath } from "../../utils.js";
import { normalizeMessageChannel } from "../../utils/message-channel.js";
@@ -637,10 +638,12 @@ export async function compactEmbeddedPiSessionDirect(
if (promptCapabilities.length > 0) {
runtimeCapabilities ??= [];
const seenCapabilities = new Set(
runtimeCapabilities.map((cap) => String(cap).trim().toLowerCase()),
runtimeCapabilities
.map((cap) => normalizeOptionalLowercaseString(String(cap)))
.filter(Boolean),
);
for (const capability of promptCapabilities) {
const normalizedCapability = capability.trim().toLowerCase();
const normalizedCapability = normalizeOptionalLowercaseString(capability);
if (!normalizedCapability || seenCapabilities.has(normalizedCapability)) {
continue;
}

View File

@@ -1,6 +1,7 @@
import type { StreamFn } from "@mariozechner/pi-agent-core";
import { streamSimple } from "@mariozechner/pi-ai";
import type { ThinkLevel } from "../../auto-reply/thinking.js";
import { normalizeOptionalLowercaseString } from "../../shared/string-coerce.js";
import { streamWithPayloadPatch } from "./stream-payload-utils.js";
type MoonshotThinkingType = "enabled" | "disabled";
@@ -10,7 +11,10 @@ function normalizeMoonshotThinkingType(value: unknown): MoonshotThinkingType | u
return value ? "enabled" : "disabled";
}
if (typeof value === "string") {
const normalized = value.trim().toLowerCase();
const normalized = normalizeOptionalLowercaseString(value);
if (!normalized) {
return undefined;
}
if (["enabled", "enable", "on", "true"].includes(normalized)) {
return "enabled";
}

View File

@@ -2,7 +2,7 @@ import type { StreamFn } from "@mariozechner/pi-agent-core";
import type { SimpleStreamOptions } from "@mariozechner/pi-ai";
import { streamSimple } from "@mariozechner/pi-ai";
import type { OpenClawConfig } from "../../config/config.js";
import { readStringValue } from "../../shared/string-coerce.js";
import { normalizeOptionalLowercaseString, readStringValue } from "../../shared/string-coerce.js";
import {
patchCodexNativeWebSearchPayload,
resolveCodexNativeSearchActivation,
@@ -121,10 +121,10 @@ function normalizeOpenAIFastMode(value: unknown): boolean | undefined {
if (typeof value === "boolean") {
return value;
}
if (typeof value !== "string") {
const normalized = normalizeOptionalLowercaseString(value);
if (!normalized) {
return undefined;
}
const normalized = value.trim().toLowerCase();
if (
normalized === "on" ||
normalized === "true" ||

View File

@@ -2,7 +2,7 @@ import type { StreamFn } from "@mariozechner/pi-agent-core";
import { streamSimple } from "@mariozechner/pi-ai";
import type { ThinkLevel } from "../../auto-reply/thinking.js";
import { isProxyReasoningUnsupportedModelHint } from "../../plugin-sdk/provider-model-shared.js";
import { readStringValue } from "../../shared/string-coerce.js";
import { normalizeOptionalLowercaseString, readStringValue } from "../../shared/string-coerce.js";
import { resolveProviderRequestPolicy } from "../provider-attribution.js";
import { resolveProviderRequestPolicyConfig } from "../provider-request-config.js";
import { applyAnthropicEphemeralCacheControlMarkers } from "./anthropic-cache-control-payload.js";
@@ -76,7 +76,7 @@ export function createOpenRouterSystemCacheWrapper(baseStreamFn: StreamFn | unde
!isAnthropicModelRef(modelId) ||
!(
endpointClass === "openrouter" ||
(endpointClass === "default" && provider?.trim().toLowerCase() === "openrouter")
(endpointClass === "default" && normalizeOptionalLowercaseString(provider) === "openrouter")
)
) {
return underlying(model, context, options);

View File

@@ -26,6 +26,7 @@ import { getGlobalHookRunner } from "../../../plugins/hook-runner-global.js";
import { resolveToolCallArgumentsEncoding } from "../../../plugins/provider-model-compat.js";
import { resolveProviderSystemPromptContribution } from "../../../plugins/provider-runtime.js";
import { isSubagentSessionKey } from "../../../routing/session-key.js";
import { normalizeOptionalLowercaseString } from "../../../shared/string-coerce.js";
import { buildTtsSystemPromptHint } from "../../../tts/tts.js";
import { resolveUserPath } from "../../../utils.js";
import { normalizeMessageChannel } from "../../../utils/message-channel.js";
@@ -605,10 +606,12 @@ export async function runEmbeddedAttempt(
if (promptCapabilities.length > 0) {
runtimeCapabilities ??= [];
const seenCapabilities = new Set(
runtimeCapabilities.map((cap) => String(cap).trim().toLowerCase()),
runtimeCapabilities
.map((cap) => normalizeOptionalLowercaseString(String(cap)))
.filter(Boolean),
);
for (const capability of promptCapabilities) {
const normalizedCapability = capability.trim().toLowerCase();
const normalizedCapability = normalizeOptionalLowercaseString(capability);
if (!normalizedCapability || seenCapabilities.has(normalizedCapability)) {
continue;
}

View File

@@ -6,7 +6,10 @@ import { isSilentReplyPayloadText, SILENT_REPLY_TOKEN } from "../../../auto-repl
import { formatToolAggregate } from "../../../auto-reply/tool-meta.js";
import type { OpenClawConfig } from "../../../config/config.js";
import { isCronSessionKey } from "../../../routing/session-key.js";
import { normalizeOptionalString } from "../../../shared/string-coerce.js";
import {
normalizeOptionalLowercaseString,
normalizeOptionalString,
} from "../../../shared/string-coerce.js";
import {
BILLING_ERROR_USER_MESSAGE,
formatAssistantErrorText,
@@ -74,7 +77,7 @@ function resolveToolErrorWarningPolicy(params: {
sessionKey: string;
verboseLevel?: VerboseLevel;
}): ToolErrorWarningPolicy {
const normalizedToolName = params.lastToolError.toolName.trim().toLowerCase();
const normalizedToolName = normalizeOptionalLowercaseString(params.lastToolError.toolName) ?? "";
const includeDetails = shouldIncludeToolErrorDetails(params);
if (params.suppressToolErrorWarnings) {
return { showWarning: false, includeDetails };

View File

@@ -1,3 +1,4 @@
import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js";
import { escapeRegExp } from "../utils.js";
const ESC = "\x1b";
@@ -324,8 +325,8 @@ function hasAnyModifier(mods: Modifiers): boolean {
}
function parseHexByte(raw: string): number | null {
const trimmed = raw.trim().toLowerCase();
const normalized = trimmed.startsWith("0x") ? trimmed.slice(2) : trimmed;
const lower = normalizeLowercaseStringOrEmpty(raw);
const normalized = lower.startsWith("0x") ? lower.slice(2) : lower;
if (!/^[0-9a-f]{1,2}$/.test(normalized)) {
return null;
}

View File

@@ -10,6 +10,7 @@ import { writeFileFromPathWithinRoot } from "../infra/fs-safe.js";
import { assertCanonicalPathWithinBase } from "../infra/install-safe-path.js";
import { fetchWithSsrFGuard } from "../infra/net/fetch-guard.js";
import { isWithinDir } from "../infra/path-safety.js";
import { normalizeOptionalLowercaseString } from "../shared/string-coerce.js";
import { ensureDir, resolveUserPath } from "../utils.js";
import { extractArchive } from "./skills-install-extract.js";
import { formatInstallFailureMessage } from "./skills-install-output.js";
@@ -43,7 +44,7 @@ function resolveDownloadTargetDir(entry: SkillEntry, spec: SkillInstallSpec): st
}
function resolveArchiveType(spec: SkillInstallSpec, filename: string): string | undefined {
const explicit = spec.archive?.trim().toLowerCase();
const explicit = normalizeOptionalLowercaseString(spec.archive);
if (explicit) {
return explicit;
}

View File

@@ -1,3 +1,5 @@
import { normalizeOptionalLowercaseString } from "../shared/string-coerce.js";
export type ToolErrorSummary = {
toolName: string;
meta?: string;
@@ -10,5 +12,5 @@ export type ToolErrorSummary = {
const EXEC_LIKE_TOOL_NAMES = new Set(["exec", "bash"]);
export function isExecLikeToolName(toolName: string): boolean {
return EXEC_LIKE_TOOL_NAMES.has(toolName.trim().toLowerCase());
return EXEC_LIKE_TOOL_NAMES.has(normalizeOptionalLowercaseString(toolName) ?? "");
}

View File

@@ -5,6 +5,10 @@ import type { CronDelivery, CronMessageChannel } from "../../cron/types.js";
import { normalizeHttpWebhookUrl } from "../../cron/webhook-url.js";
import { parseAgentSessionKey } from "../../sessions/session-key-utils.js";
import { extractTextFromChatContent } from "../../shared/chat-content.js";
import {
normalizeLowercaseStringOrEmpty,
normalizeOptionalLowercaseString,
} from "../../shared/string-coerce.js";
import { isRecord, truncateUtf16Safe } from "../../utils.js";
import { resolveSessionAgentId } from "../agent-scope.js";
import { optionalStringEnum, stringEnum } from "../schema/typebox.js";
@@ -379,7 +383,7 @@ function inferDeliveryFromSessionKey(agentSessionKey?: string): CronDelivery | n
if (parts.length === 0) {
return null;
}
const head = parts[0]?.trim().toLowerCase();
const head = normalizeOptionalLowercaseString(parts[0]);
if (!head || head === "main" || head === "subagent" || head === "acp") {
return null;
}
@@ -409,7 +413,7 @@ function inferDeliveryFromSessionKey(agentSessionKey?: string): CronDelivery | n
let channel: CronMessageChannel | undefined;
if (markerIndex >= 1) {
channel = parts[0]?.trim().toLowerCase() as CronMessageChannel;
channel = normalizeOptionalLowercaseString(parts[0]) as CronMessageChannel | undefined;
}
const delivery: CronDelivery = { mode: "announce", to: peerId };
@@ -586,7 +590,7 @@ Use jobId as the canonical identifier; id is accepted for compatibility. Use con
const deliveryValue = (job as { delivery?: unknown }).delivery;
const delivery = isRecord(deliveryValue) ? deliveryValue : undefined;
const modeRaw = typeof delivery?.mode === "string" ? delivery.mode : "";
const mode = modeRaw.trim().toLowerCase();
const mode = normalizeLowercaseStringOrEmpty(modeRaw);
if (mode === "webhook") {
const webhookUrl = normalizeHttpWebhookUrl(delivery?.to);
if (!webhookUrl) {