From 8e4eaec394f8443ee20e2ca113cf0caccd9d00db Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 7 Apr 2026 12:41:44 +0100 Subject: [PATCH] refactor: dedupe agent lowercase helpers --- src/agents/acp-spawn.ts | 11 +++++++---- src/agents/cli-backends.ts | 5 ++++- src/agents/cli-runner/helpers.ts | 3 ++- src/agents/cli-runner/reliability.ts | 8 +++----- src/agents/live-target-matcher.ts | 8 ++++++-- src/agents/model-selection.ts | 10 ++++++---- src/agents/pi-bundle-lsp-runtime.ts | 10 ++++++++-- src/agents/pi-bundle-mcp-names.ts | 6 +++++- src/agents/pi-embedded-helpers/errors.ts | 7 ++++--- src/agents/pi-embedded-runner/compact.ts | 7 +++++-- .../moonshot-thinking-stream-wrappers.ts | 6 +++++- .../pi-embedded-runner/openai-stream-wrappers.ts | 6 +++--- .../pi-embedded-runner/proxy-stream-wrappers.ts | 4 ++-- src/agents/pi-embedded-runner/run/attempt.ts | 7 +++++-- src/agents/pi-embedded-runner/run/payloads.ts | 7 +++++-- src/agents/pty-keys.ts | 5 +++-- src/agents/skills-install-download.ts | 3 ++- src/agents/tool-error-summary.ts | 4 +++- src/agents/tools/cron-tool.ts | 10 +++++++--- 19 files changed, 85 insertions(+), 42 deletions(-) diff --git a/src/agents/acp-spawn.ts b/src/agents/acp-spawn.ts index dab96848bf6..908dee09c20 100644 --- a/src/agents/acp-spawn.ts +++ b/src/agents/acp-spawn.ts @@ -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, diff --git a/src/agents/cli-backends.ts b/src/agents/cli-backends.ts index ea67cceef0b..8c08057c418 100644 --- a/src/agents/cli-backends.ts +++ b/src/agents/cli-backends.ts @@ -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, 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]; } diff --git a/src/agents/cli-runner/helpers.ts b/src/agents/cli-runner/helpers.ts index 6a08312c6e3..6920e6e1152 100644 --- a/src/agents/cli-runner/helpers.ts +++ b/src/agents/cli-runner/helpers.ts @@ -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(key: string, task: () => Promise): Promise { diff --git a/src/agents/cli-runner/reliability.ts b/src/agents/cli-runner/reliability.ts index cd1fefa9378..c0c4629174d 100644 --- a/src/agents/cli-runner/reliability.ts +++ b/src/agents/cli-runner/reliability.ts @@ -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; diff --git a/src/agents/live-target-matcher.ts b/src/agents/live-target-matcher.ts index 104b5af2eaf..7387dff593b 100644 --- a/src/agents/live-target-matcher.ts +++ b/src/agents/live-target-matcher.ts @@ -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) { diff --git a/src/agents/model-selection.ts b/src/agents/model-selection.ts index 2036a6f6ff4..d78f4ce073c 100644 --- a/src/agents/model-selection.ts +++ b/src/agents/model-selection.ts @@ -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); diff --git a/src/agents/pi-bundle-lsp-runtime.ts b/src/agents/pi-bundle-lsp-runtime.ts index 74a4d6f4554..a468823dc7a 100644 --- a/src/agents/pi-bundle-lsp-runtime.ts +++ b/src/agents/pi-bundle-lsp-runtime.ts @@ -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.`, diff --git a/src/agents/pi-bundle-mcp-names.ts b/src/agents/pi-bundle-mcp-names.ts index f09f2f1d23f..397f2240270 100644 --- a/src/agents/pi-bundle-mcp-names.ts +++ b/src/agents/pi-bundle-mcp-names.ts @@ -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): Set { - 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: { diff --git a/src/agents/pi-embedded-helpers/errors.ts b/src/agents/pi-embedded-helpers/errors.ts index 11beca36264..41fb6352f1a 100644 --- a/src/agents/pi-embedded-helpers/errors.ts +++ b/src/agents/pi-embedded-helpers/errors.ts @@ -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; } diff --git a/src/agents/pi-embedded-runner/compact.ts b/src/agents/pi-embedded-runner/compact.ts index 7c8f31548f8..b89eb0e255d 100644 --- a/src/agents/pi-embedded-runner/compact.ts +++ b/src/agents/pi-embedded-runner/compact.ts @@ -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; } diff --git a/src/agents/pi-embedded-runner/moonshot-thinking-stream-wrappers.ts b/src/agents/pi-embedded-runner/moonshot-thinking-stream-wrappers.ts index d8991c7206d..f7e11cda149 100644 --- a/src/agents/pi-embedded-runner/moonshot-thinking-stream-wrappers.ts +++ b/src/agents/pi-embedded-runner/moonshot-thinking-stream-wrappers.ts @@ -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"; } diff --git a/src/agents/pi-embedded-runner/openai-stream-wrappers.ts b/src/agents/pi-embedded-runner/openai-stream-wrappers.ts index 51ed8b3f9c6..82e737e13c6 100644 --- a/src/agents/pi-embedded-runner/openai-stream-wrappers.ts +++ b/src/agents/pi-embedded-runner/openai-stream-wrappers.ts @@ -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" || diff --git a/src/agents/pi-embedded-runner/proxy-stream-wrappers.ts b/src/agents/pi-embedded-runner/proxy-stream-wrappers.ts index c49531254e3..32f7faf4d0e 100644 --- a/src/agents/pi-embedded-runner/proxy-stream-wrappers.ts +++ b/src/agents/pi-embedded-runner/proxy-stream-wrappers.ts @@ -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); diff --git a/src/agents/pi-embedded-runner/run/attempt.ts b/src/agents/pi-embedded-runner/run/attempt.ts index 935f8898115..1aebf93b46a 100644 --- a/src/agents/pi-embedded-runner/run/attempt.ts +++ b/src/agents/pi-embedded-runner/run/attempt.ts @@ -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; } diff --git a/src/agents/pi-embedded-runner/run/payloads.ts b/src/agents/pi-embedded-runner/run/payloads.ts index a025403aa6b..50967ed371e 100644 --- a/src/agents/pi-embedded-runner/run/payloads.ts +++ b/src/agents/pi-embedded-runner/run/payloads.ts @@ -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 }; diff --git a/src/agents/pty-keys.ts b/src/agents/pty-keys.ts index 08d7c8a4eaf..a3be7f3cca9 100644 --- a/src/agents/pty-keys.ts +++ b/src/agents/pty-keys.ts @@ -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; } diff --git a/src/agents/skills-install-download.ts b/src/agents/skills-install-download.ts index 2b2ee98bb7a..d03d765d1ca 100644 --- a/src/agents/skills-install-download.ts +++ b/src/agents/skills-install-download.ts @@ -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; } diff --git a/src/agents/tool-error-summary.ts b/src/agents/tool-error-summary.ts index d4e06274029..39a9264ae7d 100644 --- a/src/agents/tool-error-summary.ts +++ b/src/agents/tool-error-summary.ts @@ -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) ?? ""); } diff --git a/src/agents/tools/cron-tool.ts b/src/agents/tools/cron-tool.ts index 09b326614b7..513656a7d44 100644 --- a/src/agents/tools/cron-tool.ts +++ b/src/agents/tools/cron-tool.ts @@ -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) {