refactor: dedupe gateway trimmed readers

This commit is contained in:
Peter Steinberger
2026-04-07 23:53:43 +01:00
parent 669b352d36
commit b3ecabbbb7
6 changed files with 68 additions and 62 deletions

View File

@@ -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<string, unknown>;
type ToolNameRegistry = Map<string, string>;
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)

View File

@@ -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<string, unknown>):
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" };
}

View File

@@ -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"

View File

@@ -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<string>([nodeSession.nodeId]);
if (instanceId) {
nodeIdsForPairing.add(instanceId);

View File

@@ -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<string>();
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<string, { entry: SessionEntry; key: string }>();
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<string>();
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 =

View File

@@ -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");
}