refactor: dedupe optional string readers

This commit is contained in:
Peter Steinberger
2026-04-07 05:17:04 +01:00
parent 7a2abb1c50
commit 326b36794f
7 changed files with 38 additions and 54 deletions

View File

@@ -11,7 +11,7 @@ import {
} from "../../infra/restart-sentinel.js";
import { scheduleGatewaySigusr1Restart } from "../../infra/restart.js";
import { createSubsystemLogger } from "../../logging/subsystem.js";
import { readStringValue } from "../../shared/string-coerce.js";
import { normalizeOptionalString, readStringValue } from "../../shared/string-coerce.js";
import { stringEnum } from "../schema/typebox.js";
import { type AnyAgentTool, jsonResult, readStringParam } from "./common.js";
import { callGatewayTool, readGatewayCallOptions } from "./gateway.js";
@@ -163,19 +163,14 @@ export function createGatewayTool(opts?: {
throw new Error("Gateway restart is disabled (commands.restart=false).");
}
const sessionKey =
typeof params.sessionKey === "string" && params.sessionKey.trim()
? params.sessionKey.trim()
: opts?.agentSessionKey?.trim() || undefined;
normalizeOptionalString(params.sessionKey) ??
normalizeOptionalString(opts?.agentSessionKey);
const delayMs =
typeof params.delayMs === "number" && Number.isFinite(params.delayMs)
? Math.floor(params.delayMs)
: undefined;
const reason =
typeof params.reason === "string" && params.reason.trim()
? params.reason.trim().slice(0, 200)
: undefined;
const note =
typeof params.note === "string" && params.note.trim() ? params.note.trim() : undefined;
const reason = normalizeOptionalString(params.reason)?.slice(0, 200);
const note = normalizeOptionalString(params.note);
// Extract channel + threadId for routing after restart.
// Uses generic :thread: parsing plus plugin-owned session grammars.
const { deliveryContext, threadId } = extractDeliveryInfo(sessionKey);
@@ -216,11 +211,9 @@ export function createGatewayTool(opts?: {
restartDelayMs: number | undefined;
} => {
const sessionKey =
typeof params.sessionKey === "string" && params.sessionKey.trim()
? params.sessionKey.trim()
: opts?.agentSessionKey?.trim() || undefined;
const note =
typeof params.note === "string" && params.note.trim() ? params.note.trim() : undefined;
normalizeOptionalString(params.sessionKey) ??
normalizeOptionalString(opts?.agentSessionKey);
const note = normalizeOptionalString(params.note);
const restartDelayMs =
typeof params.restartDelayMs === "number" && Number.isFinite(params.restartDelayMs)
? Math.floor(params.restartDelayMs)

View File

@@ -2,6 +2,7 @@ import { type Api, type Model } from "@mariozechner/pi-ai";
import type { OpenClawConfig } from "../../config/config.js";
import type { AgentModelConfig } from "../../config/types.agents-shared.js";
import { getDefaultLocalRoots } from "../../media/web-media.js";
import { normalizeOptionalString } from "../../shared/string-coerce.js";
import { normalizeProviderId } from "../provider-id.js";
import type { ImageModelConfig } from "./image-tool.helpers.js";
import {
@@ -214,10 +215,8 @@ export function resolvePromptAndModelOverride(
prompt: string;
modelOverride?: string;
} {
const prompt =
typeof args.prompt === "string" && args.prompt.trim() ? args.prompt.trim() : defaultPrompt;
const modelOverride =
typeof args.model === "string" && args.model.trim() ? args.model.trim() : undefined;
const prompt = normalizeOptionalString(args.prompt) ?? defaultPrompt;
const modelOverride = normalizeOptionalString(args.model);
return { prompt, modelOverride };
}

View File

@@ -3,6 +3,7 @@ import { Type } from "@sinclair/typebox";
import type { OpenClawConfig } from "../../config/config.js";
import { extractPdfContent, type PdfExtractedContent } from "../../media/pdf-extract.js";
import { loadWebMediaRaw } from "../../media/web-media.js";
import { normalizeOptionalString } from "../../shared/string-coerce.js";
import { resolveUserPath } from "../../utils.js";
import { type ImageModelConfig } from "./image-tool.helpers.js";
import { resolvePdfModelConfigForTool } from "./pdf-tool.model-config.js";
@@ -323,8 +324,7 @@ export function createPdfTool(options?: {
const maxBytes = Math.floor(maxBytesMb * 1024 * 1024);
// Parse page range
const pagesRaw =
typeof record.pages === "string" && record.pages.trim() ? record.pages.trim() : undefined;
const pagesRaw = normalizeOptionalString(record.pages);
const sandboxConfig: SandboxedBridgeMediaPathConfig | null =
options?.sandbox && options.sandbox.root.trim()

View File

@@ -7,6 +7,7 @@ import type {
import { resolveSecretInputRef } from "../config/types.secrets.js";
import { readTailscaleWhoisIdentity, type TailscaleWhoisIdentity } from "../infra/tailscale.js";
import { safeEqualSecret } from "../security/secret-equal.js";
import { normalizeOptionalString } from "../shared/string-coerce.js";
import {
AUTH_RATE_LIMIT_SCOPE_SHARED_SECRET,
type AuthRateLimiter,
@@ -157,17 +158,17 @@ function getTailscaleUser(req?: IncomingMessage): TailscaleUser | null {
if (!req) {
return null;
}
const login = req.headers["tailscale-user-login"];
if (typeof login !== "string" || !login.trim()) {
const login = normalizeOptionalString(req.headers["tailscale-user-login"]);
if (!login) {
return null;
}
const nameRaw = req.headers["tailscale-user-name"];
const profilePic = req.headers["tailscale-user-profile-pic"];
const name = typeof nameRaw === "string" && nameRaw.trim() ? nameRaw.trim() : login.trim();
const name = normalizeOptionalString(nameRaw) ?? login;
return {
login: login.trim(),
login,
name,
profilePic: typeof profilePic === "string" && profilePic.trim() ? profilePic.trim() : undefined,
profilePic: normalizeOptionalString(profilePic),
};
}

View File

@@ -1,6 +1,7 @@
import { randomUUID } from "node:crypto";
import { formatErrorMessage } from "../infra/errors.js";
import type { PromptImageOrderEntry } from "../media/prompt-image-order.js";
import { normalizeOptionalString } from "../shared/string-coerce.js";
import type { NodeEvent, NodeEventContext } from "./server-node-events-types.js";
import {
agentCommandFromIngress,
@@ -37,28 +38,20 @@ const MAX_RECENT_VOICE_TRANSCRIPTS = 200;
const recentVoiceTranscripts = new Map<string, { fingerprint: string; ts: number }>();
function normalizeNonEmptyString(value: unknown): string | null {
if (typeof value !== "string") {
return null;
}
const trimmed = value.trim();
return trimmed.length > 0 ? trimmed : null;
}
function normalizeFiniteInteger(value: unknown): number | null {
return typeof value === "number" && Number.isFinite(value) ? Math.trunc(value) : null;
}
function resolveVoiceTranscriptFingerprint(obj: Record<string, unknown>, text: string): string {
const eventId =
normalizeNonEmptyString(obj.eventId) ??
normalizeNonEmptyString(obj.providerEventId) ??
normalizeNonEmptyString(obj.transcriptId);
normalizeOptionalString(obj.eventId) ??
normalizeOptionalString(obj.providerEventId) ??
normalizeOptionalString(obj.transcriptId);
if (eventId) {
return `event:${eventId}`;
}
const callId = normalizeNonEmptyString(obj.providerCallId) ?? normalizeNonEmptyString(obj.callId);
const callId = normalizeOptionalString(obj.providerCallId) ?? normalizeOptionalString(obj.callId);
const sequence = normalizeFiniteInteger(obj.sequence) ?? normalizeFiniteInteger(obj.seq);
if (callId && sequence !== null) {
return `call-seq:${callId}:${sequence}`;
@@ -428,12 +421,12 @@ export const handleNodeEvent = async (ctx: NodeEventContext, nodeId: string, evt
const channelRaw = typeof link?.channel === "string" ? link.channel.trim() : "";
let channel = normalizeChannelId(channelRaw) ?? undefined;
let to = typeof link?.to === "string" && link.to.trim() ? link.to.trim() : undefined;
let to = normalizeOptionalString(link?.to);
const deliverRequested = Boolean(link?.deliver);
const wantsReceipt = Boolean(link?.receipt);
const receiptTextRaw = typeof link?.receiptText === "string" ? link.receiptText.trim() : "";
const receiptText =
receiptTextRaw || "Just received your iOS share + request, working on it.";
normalizeOptionalString(link?.receiptText) ||
"Just received your iOS share + request, working on it.";
const now = Date.now();
const sessionId = entry?.sessionId ?? randomUUID();
@@ -508,24 +501,24 @@ export const handleNodeEvent = async (ctx: NodeEventContext, nodeId: string, evt
if (!obj) {
return;
}
const change = normalizeNonEmptyString(obj.change)?.toLowerCase();
const change = normalizeOptionalString(obj.change)?.toLowerCase();
if (change !== "posted" && change !== "removed") {
return;
}
const keyRaw = normalizeNonEmptyString(obj.key);
const keyRaw = normalizeOptionalString(obj.key);
if (!keyRaw) {
return;
}
const key = sanitizeInboundSystemTags(keyRaw);
const sessionKeyRaw = normalizeNonEmptyString(obj.sessionKey) ?? `node-${nodeId}`;
const sessionKeyRaw = normalizeOptionalString(obj.sessionKey) ?? `node-${nodeId}`;
const { canonicalKey: sessionKey } = loadSessionEntry(sessionKeyRaw);
const packageNameRaw = normalizeNonEmptyString(obj.packageName);
const packageNameRaw = normalizeOptionalString(obj.packageName);
const packageName = packageNameRaw ? sanitizeInboundSystemTags(packageNameRaw) : null;
const title = compactNotificationEventText(
sanitizeInboundSystemTags(normalizeNonEmptyString(obj.title) ?? ""),
sanitizeInboundSystemTags(normalizeOptionalString(obj.title) ?? ""),
);
const text = compactNotificationEventText(
sanitizeInboundSystemTags(normalizeNonEmptyString(obj.text) ?? ""),
sanitizeInboundSystemTags(normalizeOptionalString(obj.text) ?? ""),
);
let summary = `Notification ${change} (node=${nodeId} key=${key}`;

View File

@@ -1,3 +1,4 @@
import { normalizeOptionalString } from "../../shared/string-coerce.js";
import {
INTERNAL_MESSAGE_CHANNEL,
isDeliverableMessageChannel,
@@ -23,16 +24,13 @@ export function resolveExternalBestEffortDeliveryTarget(params: {
normalizedChannel && isDeliverableMessageChannel(normalizedChannel)
? normalizedChannel
: undefined;
const to = typeof params.to === "string" && params.to.trim() ? params.to.trim() : undefined;
const to = normalizeOptionalString(params.to);
const deliver = Boolean(channel && to);
return {
deliver,
channel: deliver ? channel : undefined,
to: deliver ? to : undefined,
accountId:
deliver && typeof params.accountId === "string" && params.accountId.trim()
? params.accountId.trim()
: undefined,
accountId: deliver ? normalizeOptionalString(params.accountId) : undefined,
threadId:
deliver && params.threadId != null && params.threadId !== ""
? String(params.threadId)

View File

@@ -19,6 +19,7 @@ import {
import type { SessionEntry } from "../config/sessions/types.js";
import { stripEnvelope, stripMessageIdHints } from "../shared/chat-envelope.js";
import { asFiniteNumber } from "../shared/number-coercion.js";
import { normalizeOptionalString } from "../shared/string-coerce.js";
import { countToolResults, extractToolCallNames } from "../utils/transcript-tools.js";
import { estimateUsageCost, resolveModelCostConfig } from "../utils/usage-format.js";
import type {
@@ -945,8 +946,7 @@ export async function loadSessionLogs(params: {
const contentParts: string[] = [];
const rawToolName = message.toolName ?? message.tool_name ?? message.name ?? message.tool;
const toolName =
typeof rawToolName === "string" && rawToolName.trim() ? rawToolName.trim() : undefined;
const toolName = normalizeOptionalString(rawToolName);
if (role === "tool" || role === "toolResult") {
contentParts.push(`[Tool: ${toolName ?? "tool"}]`);
contentParts.push("[Tool Result]");