diff --git a/src/gateway/cli-session-history.claude.ts b/src/gateway/cli-session-history.claude.ts index 69ed564dce7..04ca816c154 100644 --- a/src/gateway/cli-session-history.claude.ts +++ b/src/gateway/cli-session-history.claude.ts @@ -8,6 +8,7 @@ import { type ToolContentBlock, } from "../chat/tool-content.js"; import type { SessionEntry } from "../config/sessions.js"; +import { normalizeOptionalString } from "../shared/string-coerce.js"; import { attachOpenClawTranscriptMeta } from "./session-utils.fs.js"; export const CLAUDE_CLI_PROVIDER = "claude-cli"; @@ -38,7 +39,7 @@ type TranscriptLikeMessage = Record; type ToolNameRegistry = Map; function resolveHistoryHomeDir(homeDir?: string): string { - return homeDir?.trim() || process.env.HOME || os.homedir(); + return normalizeOptionalString(homeDir) || process.env.HOME || os.homedir(); } function resolveClaudeProjectsDir(homeDir?: string): string { @@ -48,15 +49,17 @@ function resolveClaudeProjectsDir(homeDir?: string): string { export function resolveClaudeCliBindingSessionId( entry: SessionEntry | undefined, ): string | undefined { - const bindingSessionId = entry?.cliSessionBindings?.[CLAUDE_CLI_PROVIDER]?.sessionId?.trim(); + const bindingSessionId = normalizeOptionalString( + entry?.cliSessionBindings?.[CLAUDE_CLI_PROVIDER]?.sessionId, + ); if (bindingSessionId) { return bindingSessionId; } - const legacyMapSessionId = entry?.cliSessionIds?.[CLAUDE_CLI_PROVIDER]?.trim(); + const legacyMapSessionId = normalizeOptionalString(entry?.cliSessionIds?.[CLAUDE_CLI_PROVIDER]); if (legacyMapSessionId) { return legacyMapSessionId; } - const legacyClaudeSessionId = entry?.claudeCliSessionId?.trim(); + const legacyClaudeSessionId = normalizeOptionalString(entry?.claudeCliSessionId); return legacyClaudeSessionId || undefined; } @@ -117,8 +120,8 @@ function normalizeClaudeCliContent( const block = cloneJsonValue(item as ToolContentBlock); const type = typeof block.type === "string" ? block.type : ""; if (type === "tool_use") { - const id = typeof block.id === "string" ? block.id.trim() : ""; - const name = typeof block.name === "string" ? block.name.trim() : ""; + const id = normalizeOptionalString(block.id) ?? ""; + const name = normalizeOptionalString(block.name) ?? ""; if (id && name) { toolNameRegistry.set(id, name); } @@ -231,7 +234,7 @@ function parseClaudeCliHistoryEntry( const baseMeta = { importedFrom: CLAUDE_CLI_PROVIDER, cliSessionId, - ...(typeof entry.uuid === "string" && entry.uuid.trim() ? { externalId: entry.uuid } : {}), + ...(normalizeOptionalString(entry.uuid) ? { externalId: entry.uuid } : {}), }; const content = @@ -259,10 +262,8 @@ function parseClaudeCliHistoryEntry( content, api: "anthropic-messages", provider: CLAUDE_CLI_PROVIDER, - ...(typeof entry.message.model === "string" && entry.message.model.trim() - ? { model: entry.message.model } - : {}), - ...(typeof entry.message.stop_reason === "string" && entry.message.stop_reason.trim() + ...(normalizeOptionalString(entry.message.model) ? { model: entry.message.model } : {}), + ...(normalizeOptionalString(entry.message.stop_reason) ? { stopReason: entry.message.stop_reason } : {}), ...(resolveClaudeCliUsage(entry.message.usage) diff --git a/src/gateway/hooks.ts b/src/gateway/hooks.ts index 06e5f6f28f9..68a5d8fdd50 100644 --- a/src/gateway/hooks.ts +++ b/src/gateway/hooks.ts @@ -44,11 +44,11 @@ export function resolveHooksConfig(cfg: OpenClawConfig): HooksConfigResolved | n if (cfg.hooks?.enabled !== true) { return null; } - const token = cfg.hooks?.token?.trim(); + const token = normalizeOptionalString(cfg.hooks?.token); if (!token) { throw new Error("hooks.enabled requires hooks.token"); } - const rawPath = cfg.hooks?.path?.trim() || DEFAULT_HOOKS_PATH; + const rawPath = normalizeOptionalString(cfg.hooks?.path) || DEFAULT_HOOKS_PATH; const withSlash = rawPath.startsWith("/") ? rawPath : `/${rawPath}`; const trimmed = withSlash.length > 1 ? withSlash.replace(/\/+$/, "") : withSlash; if (trimmed === "/") { @@ -107,8 +107,7 @@ function resolveKnownAgentIds(cfg: OpenClawConfig, defaultAgentId: string): Set< } function resolveSessionKey(raw: string | undefined): string | undefined { - const value = raw?.trim(); - return value ? value : undefined; + return normalizeOptionalString(raw); } function normalizeSessionKeyPrefix(raw: string): string | undefined { @@ -140,18 +139,14 @@ export function isSessionKeyAllowedByPrefix(sessionKey: string, prefixes: string } export function extractHookToken(req: IncomingMessage): string | undefined { - const auth = - typeof req.headers.authorization === "string" ? req.headers.authorization.trim() : ""; + const auth = normalizeOptionalString(req.headers.authorization) ?? ""; if (normalizeLowercaseStringOrEmpty(auth).startsWith("bearer ")) { const token = auth.slice(7).trim(); if (token) { return token; } } - const headerToken = - typeof req.headers["x-openclaw-token"] === "string" - ? req.headers["x-openclaw-token"].trim() - : ""; + const headerToken = normalizeOptionalString(req.headers["x-openclaw-token"]) ?? ""; if (headerToken) { return headerToken; } @@ -196,12 +191,12 @@ export function normalizeWakePayload( ): | { ok: true; value: { text: string; mode: "now" | "next-heartbeat" } } | { ok: false; error: string } { - const text = typeof payload.text === "string" ? payload.text.trim() : ""; - if (!text) { + const normalizedText = normalizeOptionalString(payload.text) ?? ""; + if (!normalizedText) { return { ok: false, error: "text required" }; } const mode = payload.mode === "next-heartbeat" ? "next-heartbeat" : "now"; - return { ok: true, value: { text, mode } }; + return { ok: true, value: { text: normalizedText, mode } }; } export type HookAgentPayload = { @@ -276,7 +271,7 @@ export function resolveHookTargetAgentId( hooksConfig: HooksConfigResolved, agentId: string | undefined, ): string | undefined { - const raw = agentId?.trim(); + const raw = normalizeOptionalString(agentId); if (!raw) { return undefined; } @@ -292,7 +287,7 @@ export function isHookAgentAllowed( agentId: string | undefined, ): boolean { // Keep backwards compatibility for callers that omit agentId. - const raw = agentId?.trim(); + const raw = normalizeOptionalString(agentId); if (!raw) { return true; } @@ -345,7 +340,7 @@ export function normalizeHookDispatchSessionKey(params: { sessionKey: string; targetAgentId: string | undefined; }): string { - const trimmed = params.sessionKey.trim(); + const trimmed = normalizeOptionalString(params.sessionKey) ?? ""; if (!trimmed || !params.targetAgentId) { return trimmed; } @@ -363,7 +358,7 @@ export function normalizeAgentPayload(payload: Record): value: HookAgentPayload; } | { ok: false; error: string } { - const message = typeof payload.message === "string" ? payload.message.trim() : ""; + const message = normalizeOptionalString(payload.message) ?? ""; if (!message) { return { ok: false, error: "message required" }; } diff --git a/src/gateway/openai-http.ts b/src/gateway/openai-http.ts index 3254a8ff318..f3adad693b5 100644 --- a/src/gateway/openai-http.ts +++ b/src/gateway/openai-http.ts @@ -18,7 +18,10 @@ import { type InputImageSource, } from "../media/input-files.js"; import { defaultRuntime } from "../runtime.js"; -import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js"; +import { + normalizeLowercaseStringOrEmpty, + normalizeOptionalString, +} from "../shared/string-coerce.js"; import { resolveAssistantStreamDeltaText } from "./agent-event-assistant-text.js"; import { buildAgentMessageFromConversationEntries, @@ -243,11 +246,11 @@ type ActiveTurnContext = { function parseImageUrlToSource(url: string): InputImageSource { const dataUriMatch = /^data:([^,]*?),(.*)$/is.exec(url); if (dataUriMatch) { - const metadata = dataUriMatch[1]?.trim() ?? ""; + const metadata = normalizeOptionalString(dataUriMatch[1]) ?? ""; const data = dataUriMatch[2] ?? ""; const metadataParts = metadata .split(";") - .map((part) => part.trim()) + .map((part) => normalizeOptionalString(part) ?? "") .filter(Boolean); const isBase64 = metadataParts.some( (part) => normalizeLowercaseStringOrEmpty(part) === "base64", @@ -255,7 +258,7 @@ function parseImageUrlToSource(url: string): InputImageSource { if (!isBase64) { throw new Error("image_url data URI must be base64 encoded"); } - if (!data.trim()) { + if (!(normalizeOptionalString(data) ?? "")) { throw new Error("image_url data URI is missing payload data"); } const mediaTypeRaw = metadataParts.find((part) => part.includes("/")); @@ -275,7 +278,7 @@ function resolveActiveTurnContext(messagesUnknown: unknown): ActiveTurnContext { if (!msg || typeof msg !== "object") { continue; } - const role = typeof msg.role === "string" ? msg.role.trim() : ""; + const role = normalizeOptionalString(msg.role) ?? ""; const normalizedRole = role === "function" ? "tool" : role; if (normalizedRole !== "user" && normalizedRole !== "tool") { continue; @@ -347,7 +350,7 @@ function buildAgentPrompt( if (!msg || typeof msg !== "object") { continue; } - const role = typeof msg.role === "string" ? msg.role.trim() : ""; + const role = normalizeOptionalString(msg.role) ?? ""; const content = extractTextContent(msg.content).trim(); const hasImage = extractImageUrls(msg.content).length > 0; if (!role) { @@ -375,7 +378,7 @@ function buildAgentPrompt( continue; } - const name = typeof msg.name === "string" ? msg.name.trim() : ""; + const name = normalizeOptionalString(msg.name) ?? ""; const sender = normalizedRole === "assistant" ? "Assistant" diff --git a/src/gateway/server/ws-connection/message-handler.ts b/src/gateway/server/ws-connection/message-handler.ts index ad75fa49d4b..086aaaa50fa 100644 --- a/src/gateway/server/ws-connection/message-handler.ts +++ b/src/gateway/server/ws-connection/message-handler.ts @@ -40,6 +40,7 @@ import { type DeviceBootstrapProfile, } from "../../../shared/device-bootstrap-profile.js"; import { roleScopesAllow } from "../../../shared/operator-scope-compat.js"; +import { normalizeOptionalString } from "../../../shared/string-coerce.js"; import { isBrowserOperatorUiClient, isGatewayCliClient, @@ -659,7 +660,7 @@ export function attachGatewayWsMessageHandler(params: { rejectDeviceAuthInvalid("device-signature-stale", "device signature expired"); return; } - const providedNonce = typeof device.nonce === "string" ? device.nonce.trim() : ""; + const providedNonce = normalizeOptionalString(device.nonce) ?? ""; if (!providedNonce) { rejectDeviceAuthInvalid("device-nonce-missing", "device nonce required"); return; @@ -1251,7 +1252,7 @@ export function attachGatewayWsMessageHandler(params: { remoteIp: reportedClientIp, }); const instanceIdRaw = connectParams.client.instanceId; - const instanceId = typeof instanceIdRaw === "string" ? instanceIdRaw.trim() : ""; + const instanceId = normalizeOptionalString(instanceIdRaw) ?? ""; const nodeIdsForPairing = new Set([nodeSession.nodeId]); if (instanceId) { nodeIdsForPairing.add(instanceId); diff --git a/src/gateway/session-utils.ts b/src/gateway/session-utils.ts index e17b442ec8b..c272be9444c 100644 --- a/src/gateway/session-utils.ts +++ b/src/gateway/session-utils.ts @@ -113,7 +113,7 @@ function resolveIdentityAvatarUrl( if (!avatar) { return undefined; } - const trimmed = avatar.trim(); + const trimmed = normalizeOptionalString(avatar) ?? ""; if (!trimmed) { return undefined; } @@ -183,12 +183,12 @@ export function deriveSessionTitle( return undefined; } - if (entry.displayName?.trim()) { - return entry.displayName.trim(); + if (normalizeOptionalString(entry.displayName)) { + return normalizeOptionalString(entry.displayName); } - if (entry.subject?.trim()) { - return entry.subject.trim(); + if (normalizeOptionalString(entry.subject)) { + return normalizeOptionalString(entry.subject); } if (firstUserMessage?.trim()) { @@ -284,13 +284,14 @@ function resolveChildSessionKeys( ): string[] | undefined { const childSessionKeys = new Set(); for (const entry of listSubagentRunsForController(controllerSessionKey)) { - const childSessionKey = entry.childSessionKey?.trim(); + const childSessionKey = normalizeOptionalString(entry.childSessionKey); if (!childSessionKey) { continue; } const latest = getSessionDisplaySubagentRunByChildSessionKey(childSessionKey); const latestControllerSessionKey = - latest?.controllerSessionKey?.trim() || latest?.requesterSessionKey?.trim(); + normalizeOptionalString(latest?.controllerSessionKey) || + normalizeOptionalString(latest?.requesterSessionKey); if (latestControllerSessionKey !== controllerSessionKey) { continue; } @@ -300,15 +301,16 @@ function resolveChildSessionKeys( if (!entry || key === controllerSessionKey) { continue; } - const spawnedBy = entry.spawnedBy?.trim(); - const parentSessionKey = entry.parentSessionKey?.trim(); + const spawnedBy = normalizeOptionalString(entry.spawnedBy); + const parentSessionKey = normalizeOptionalString(entry.parentSessionKey); if (spawnedBy !== controllerSessionKey && parentSessionKey !== controllerSessionKey) { continue; } const latest = getSessionDisplaySubagentRunByChildSessionKey(key); if (latest) { const latestControllerSessionKey = - latest.controllerSessionKey?.trim() || latest.requesterSessionKey?.trim(); + normalizeOptionalString(latest.controllerSessionKey) || + normalizeOptionalString(latest.requesterSessionKey); if (latestControllerSessionKey !== controllerSessionKey) { continue; } @@ -388,13 +390,13 @@ export function loadSessionEntry(sessionKey: string) { const agentId = resolveSessionStoreAgentId(cfg, canonicalKey); const { storePath, store } = resolveGatewaySessionStoreLookup({ cfg, - key: sessionKey.trim(), + key: normalizeOptionalString(sessionKey) ?? "", canonicalKey, agentId, }); const target = resolveGatewaySessionStoreTarget({ cfg, - key: sessionKey.trim(), + key: normalizeOptionalString(sessionKey) ?? "", store, }); const freshestMatch = resolveFreshestSessionStoreMatchFromStoreKeys(store, target.storeKeys); @@ -434,7 +436,7 @@ function findFreshestStoreMatch( ): { entry: SessionEntry; key: string } | undefined { const matches = new Map(); for (const candidate of candidates) { - const trimmed = candidate.trim(); + const trimmed = normalizeOptionalString(candidate) ?? ""; if (!trimmed) { continue; } @@ -486,7 +488,7 @@ export function pruneLegacyStoreKeys(params: { }) { const keysToDelete = new Set(); for (const candidate of params.candidates) { - const trimmed = String(candidate ?? "").trim(); + const trimmed = normalizeOptionalString(String(candidate ?? "")) ?? ""; if (!trimmed) { continue; } @@ -720,7 +722,7 @@ export function resolveSessionStoreKey(params: { cfg: OpenClawConfig; sessionKey: string; }): string { - const raw = (params.sessionKey ?? "").trim(); + const raw = normalizeOptionalString(params.sessionKey) ?? ""; if (!raw) { return raw; } @@ -769,7 +771,7 @@ export function canonicalizeSpawnedByForAgent( agentId: string, spawnedBy?: string, ): string | undefined { - const raw = spawnedBy?.trim(); + const raw = normalizeOptionalString(spawnedBy) ?? ""; if (!raw) { return undefined; } @@ -895,7 +897,7 @@ export function resolveGatewaySessionStoreTarget(params: { canonicalKey: string; storeKeys: string[]; } { - const key = params.key.trim(); + const key = normalizeOptionalString(params.key) ?? ""; const canonicalKey = resolveSessionStoreKey({ cfg: params.cfg, sessionKey: key, @@ -1411,7 +1413,7 @@ export function listSessionsFromStore(params: { const includeDerivedTitles = opts.includeDerivedTitles === true; const includeLastMessage = opts.includeLastMessage === true; const spawnedBy = typeof opts.spawnedBy === "string" ? opts.spawnedBy : ""; - const label = typeof opts.label === "string" ? opts.label.trim() : ""; + const label = normalizeOptionalString(opts.label) ?? ""; const agentId = typeof opts.agentId === "string" ? normalizeAgentId(opts.agentId) : ""; const search = normalizeLowercaseStringOrEmpty(opts.search); const activeMinutes = diff --git a/src/gateway/sessions-patch.ts b/src/gateway/sessions-patch.ts index 8f24a2bb0a4..30907e4243f 100644 --- a/src/gateway/sessions-patch.ts +++ b/src/gateway/sessions-patch.ts @@ -30,7 +30,10 @@ import { applyVerboseOverride, parseVerboseOverride } from "../sessions/level-ov import { applyModelOverrideToSessionEntry } from "../sessions/model-overrides.js"; import { normalizeSendPolicy } from "../sessions/send-policy.js"; import { parseSessionLabel } from "../sessions/session-label.js"; -import { normalizeOptionalLowercaseString } from "../shared/string-coerce.js"; +import { + normalizeOptionalLowercaseString, + normalizeOptionalString, +} from "../shared/string-coerce.js"; import { ErrorCodes, type ErrorShape, @@ -109,7 +112,7 @@ export async function applySessionsPatchToStore(params: { return invalid("spawnedBy cannot be cleared once set"); } } else if (raw !== undefined) { - const trimmed = String(raw).trim(); + const trimmed = normalizeOptionalString(String(raw)) ?? ""; if (!trimmed) { return invalid("invalid spawnedBy: empty"); } @@ -133,7 +136,7 @@ export async function applySessionsPatchToStore(params: { if (!supportsSpawnLineage(storeKey)) { return invalid("spawnedWorkspaceDir is only supported for subagent:* or acp:* sessions"); } - const trimmed = String(raw).trim(); + const trimmed = normalizeOptionalString(String(raw)) ?? ""; if (!trimmed) { return invalid("invalid spawnedWorkspaceDir: empty"); } @@ -237,8 +240,9 @@ export async function applySessionsPatchToStore(params: { } else if (raw !== undefined) { const normalized = normalizeThinkLevel(String(raw)); if (!normalized) { - const hintProvider = existing?.providerOverride?.trim() || resolvedDefault.provider; - const hintModel = existing?.modelOverride?.trim() || resolvedDefault.model; + const hintProvider = + normalizeOptionalString(existing?.providerOverride) || resolvedDefault.provider; + const hintModel = normalizeOptionalString(existing?.modelOverride) || resolvedDefault.model; return invalid( `invalid thinkingLevel (use ${formatThinkingLevels(hintProvider, hintModel, "|")})`, ); @@ -359,7 +363,7 @@ export async function applySessionsPatchToStore(params: { if (raw === null) { delete next.execNode; } else if (raw !== undefined) { - const trimmed = String(raw).trim(); + const trimmed = normalizeOptionalString(String(raw)) ?? ""; if (!trimmed) { return invalid("invalid execNode: empty"); } @@ -380,7 +384,7 @@ export async function applySessionsPatchToStore(params: { markLiveSwitchPending: true, }); } else if (raw !== undefined) { - const trimmed = String(raw).trim(); + const trimmed = normalizeOptionalString(String(raw)) ?? ""; if (!trimmed) { return invalid("invalid model: empty"); }