From 3ec81709d72d01ad50884ef63b18a8feca33a607 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 7 Mar 2026 20:10:54 +0000 Subject: [PATCH] refactor: unify shared utility normalization helpers --- src/agents/tools/sessions-resolution.ts | 7 ++-- src/auto-reply/reply/commands-acp/shared.ts | 2 +- .../reply/commands-subagents/shared.ts | 5 ++- src/memory/embeddings-mistral.ts | 14 ++++---- src/memory/embeddings-model-normalize.test.ts | 34 +++++++++++++++++++ src/memory/embeddings-model-normalize.ts | 16 +++++++++ src/memory/embeddings-ollama.ts | 14 ++++---- src/memory/embeddings-openai.ts | 14 ++++---- src/memory/embeddings-voyage.ts | 14 ++++---- src/pairing/setup-code.ts | 29 +++++++++------- src/sessions/session-id.test.ts | 14 ++++++++ src/sessions/session-id.ts | 5 +++ 12 files changed, 115 insertions(+), 53 deletions(-) create mode 100644 src/memory/embeddings-model-normalize.test.ts create mode 100644 src/memory/embeddings-model-normalize.ts create mode 100644 src/sessions/session-id.test.ts create mode 100644 src/sessions/session-id.ts diff --git a/src/agents/tools/sessions-resolution.ts b/src/agents/tools/sessions-resolution.ts index 7eb730da09c..c2ba83c3001 100644 --- a/src/agents/tools/sessions-resolution.ts +++ b/src/agents/tools/sessions-resolution.ts @@ -1,6 +1,7 @@ import type { OpenClawConfig } from "../../config/config.js"; import { callGateway } from "../../gateway/call.js"; import { isAcpSessionKey, normalizeMainKey } from "../../routing/session-key.js"; +import { looksLikeSessionId } from "../../sessions/session-id.js"; function normalizeKey(value?: string) { const trimmed = value?.trim(); @@ -112,11 +113,7 @@ export async function isResolvedSessionVisibleToRequester(params: { }); } -const SESSION_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; - -export function looksLikeSessionId(value: string): boolean { - return SESSION_ID_RE.test(value.trim()); -} +export { looksLikeSessionId }; export function looksLikeSessionKey(value: string): boolean { const raw = value.trim(); diff --git a/src/auto-reply/reply/commands-acp/shared.ts b/src/auto-reply/reply/commands-acp/shared.ts index 2fe4710ce76..2b0571b332f 100644 --- a/src/auto-reply/reply/commands-acp/shared.ts +++ b/src/auto-reply/reply/commands-acp/shared.ts @@ -31,7 +31,7 @@ export const ACP_INSTALL_USAGE = "Usage: /acp install"; export const ACP_DOCTOR_USAGE = "Usage: /acp doctor"; export const ACP_SESSIONS_USAGE = "Usage: /acp sessions"; export const ACP_STEER_OUTPUT_LIMIT = 800; -export const SESSION_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; +export { SESSION_ID_RE } from "../../../sessions/session-id.js"; export type AcpAction = | "spawn" diff --git a/src/auto-reply/reply/commands-subagents/shared.ts b/src/auto-reply/reply/commands-subagents/shared.ts index 818120edb34..ec96437e645 100644 --- a/src/auto-reply/reply/commands-subagents/shared.ts +++ b/src/auto-reply/reply/commands-subagents/shared.ts @@ -18,6 +18,7 @@ import { parseDiscordTarget } from "../../../discord/targets.js"; import { callGateway } from "../../../gateway/call.js"; import { formatTimeAgo } from "../../../infra/format-time/format-relative.ts"; import { parseAgentSessionKey } from "../../../routing/session-key.js"; +import { looksLikeSessionId } from "../../../sessions/session-id.js"; import { extractTextFromChatContent } from "../../../shared/chat-content.js"; import { formatDurationCompact, @@ -75,8 +76,6 @@ export const RECENT_WINDOW_MINUTES = 30; const SUBAGENT_TASK_PREVIEW_MAX = 110; export const STEER_ABORT_SETTLE_TIMEOUT_MS = 5_000; -const SESSION_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; - function compactLine(value: string) { return value.replace(/\s+/g, " ").trim(); } @@ -345,7 +344,7 @@ export async function resolveFocusTargetSession(params: { const attempts: Array> = []; attempts.push({ key: token }); - if (SESSION_ID_RE.test(token)) { + if (looksLikeSessionId(token)) { attempts.push({ sessionId: token }); } attempts.push({ label: token }); diff --git a/src/memory/embeddings-mistral.ts b/src/memory/embeddings-mistral.ts index 7d9f2bb3dfe..0347c2b017c 100644 --- a/src/memory/embeddings-mistral.ts +++ b/src/memory/embeddings-mistral.ts @@ -1,4 +1,5 @@ import type { SsrFPolicy } from "../infra/net/ssrf.js"; +import { normalizeEmbeddingModelWithPrefixes } from "./embeddings-model-normalize.js"; import { createRemoteEmbeddingProvider, resolveRemoteEmbeddingClient, @@ -16,14 +17,11 @@ export const DEFAULT_MISTRAL_EMBEDDING_MODEL = "mistral-embed"; const DEFAULT_MISTRAL_BASE_URL = "https://api.mistral.ai/v1"; export function normalizeMistralModel(model: string): string { - const trimmed = model.trim(); - if (!trimmed) { - return DEFAULT_MISTRAL_EMBEDDING_MODEL; - } - if (trimmed.startsWith("mistral/")) { - return trimmed.slice("mistral/".length); - } - return trimmed; + return normalizeEmbeddingModelWithPrefixes({ + model, + defaultModel: DEFAULT_MISTRAL_EMBEDDING_MODEL, + prefixes: ["mistral/"], + }); } export async function createMistralEmbeddingProvider( diff --git a/src/memory/embeddings-model-normalize.test.ts b/src/memory/embeddings-model-normalize.test.ts new file mode 100644 index 00000000000..dc0581b82fe --- /dev/null +++ b/src/memory/embeddings-model-normalize.test.ts @@ -0,0 +1,34 @@ +import { describe, expect, it } from "vitest"; +import { normalizeEmbeddingModelWithPrefixes } from "./embeddings-model-normalize.js"; + +describe("normalizeEmbeddingModelWithPrefixes", () => { + it("returns default model when input is blank", () => { + expect( + normalizeEmbeddingModelWithPrefixes({ + model: " ", + defaultModel: "fallback-model", + prefixes: ["openai/"], + }), + ).toBe("fallback-model"); + }); + + it("strips the first matching prefix", () => { + expect( + normalizeEmbeddingModelWithPrefixes({ + model: "openai/text-embedding-3-small", + defaultModel: "fallback-model", + prefixes: ["openai/"], + }), + ).toBe("text-embedding-3-small"); + }); + + it("keeps explicit model names when no prefix matches", () => { + expect( + normalizeEmbeddingModelWithPrefixes({ + model: "voyage-4-large", + defaultModel: "fallback-model", + prefixes: ["voyage/"], + }), + ).toBe("voyage-4-large"); + }); +}); diff --git a/src/memory/embeddings-model-normalize.ts b/src/memory/embeddings-model-normalize.ts new file mode 100644 index 00000000000..85fcf5b16ce --- /dev/null +++ b/src/memory/embeddings-model-normalize.ts @@ -0,0 +1,16 @@ +export function normalizeEmbeddingModelWithPrefixes(params: { + model: string; + defaultModel: string; + prefixes: string[]; +}): string { + const trimmed = params.model.trim(); + if (!trimmed) { + return params.defaultModel; + } + for (const prefix of params.prefixes) { + if (trimmed.startsWith(prefix)) { + return trimmed.slice(prefix.length); + } + } + return trimmed; +} diff --git a/src/memory/embeddings-ollama.ts b/src/memory/embeddings-ollama.ts index 03e8a4de60b..4c9326df874 100644 --- a/src/memory/embeddings-ollama.ts +++ b/src/memory/embeddings-ollama.ts @@ -2,6 +2,7 @@ import { resolveEnvApiKey } from "../agents/model-auth.js"; import { formatErrorMessage } from "../infra/errors.js"; import type { SsrFPolicy } from "../infra/net/ssrf.js"; import { normalizeOptionalSecretInput } from "../utils/normalize-secret-input.js"; +import { normalizeEmbeddingModelWithPrefixes } from "./embeddings-model-normalize.js"; import type { EmbeddingProvider, EmbeddingProviderOptions } from "./embeddings.js"; import { buildRemoteBaseUrlPolicy, withRemoteHttpResponse } from "./remote-http.js"; import { resolveMemorySecretInputString } from "./secret-input.js"; @@ -28,14 +29,11 @@ function sanitizeAndNormalizeEmbedding(vec: number[]): number[] { } function normalizeOllamaModel(model: string): string { - const trimmed = model.trim(); - if (!trimmed) { - return DEFAULT_OLLAMA_EMBEDDING_MODEL; - } - if (trimmed.startsWith("ollama/")) { - return trimmed.slice("ollama/".length); - } - return trimmed; + return normalizeEmbeddingModelWithPrefixes({ + model, + defaultModel: DEFAULT_OLLAMA_EMBEDDING_MODEL, + prefixes: ["ollama/"], + }); } function resolveOllamaApiBase(configuredBaseUrl?: string): string { diff --git a/src/memory/embeddings-openai.ts b/src/memory/embeddings-openai.ts index af8184f4452..0ea4156c489 100644 --- a/src/memory/embeddings-openai.ts +++ b/src/memory/embeddings-openai.ts @@ -1,4 +1,5 @@ import type { SsrFPolicy } from "../infra/net/ssrf.js"; +import { normalizeEmbeddingModelWithPrefixes } from "./embeddings-model-normalize.js"; import { createRemoteEmbeddingProvider, resolveRemoteEmbeddingClient, @@ -21,14 +22,11 @@ const OPENAI_MAX_INPUT_TOKENS: Record = { }; export function normalizeOpenAiModel(model: string): string { - const trimmed = model.trim(); - if (!trimmed) { - return DEFAULT_OPENAI_EMBEDDING_MODEL; - } - if (trimmed.startsWith("openai/")) { - return trimmed.slice("openai/".length); - } - return trimmed; + return normalizeEmbeddingModelWithPrefixes({ + model, + defaultModel: DEFAULT_OPENAI_EMBEDDING_MODEL, + prefixes: ["openai/"], + }); } export async function createOpenAiEmbeddingProvider( diff --git a/src/memory/embeddings-voyage.ts b/src/memory/embeddings-voyage.ts index faf9fe1c85e..b078ebdb21a 100644 --- a/src/memory/embeddings-voyage.ts +++ b/src/memory/embeddings-voyage.ts @@ -1,4 +1,5 @@ import type { SsrFPolicy } from "../infra/net/ssrf.js"; +import { normalizeEmbeddingModelWithPrefixes } from "./embeddings-model-normalize.js"; import { resolveRemoteEmbeddingBearerClient } from "./embeddings-remote-client.js"; import { fetchRemoteEmbeddingVectors } from "./embeddings-remote-fetch.js"; import type { EmbeddingProvider, EmbeddingProviderOptions } from "./embeddings.js"; @@ -19,14 +20,11 @@ const VOYAGE_MAX_INPUT_TOKENS: Record = { }; export function normalizeVoyageModel(model: string): string { - const trimmed = model.trim(); - if (!trimmed) { - return DEFAULT_VOYAGE_EMBEDDING_MODEL; - } - if (trimmed.startsWith("voyage/")) { - return trimmed.slice("voyage/".length); - } - return trimmed; + return normalizeEmbeddingModelWithPrefixes({ + model, + defaultModel: DEFAULT_VOYAGE_EMBEDDING_MODEL, + prefixes: ["voyage/"], + }); } export async function createVoyageEmbeddingProvider( diff --git a/src/pairing/setup-code.ts b/src/pairing/setup-code.ts index 247abd38cc8..ef9dda897ca 100644 --- a/src/pairing/setup-code.ts +++ b/src/pairing/setup-code.ts @@ -155,6 +155,16 @@ function pickTailnetIPv4( return pickIPv4Matching(networkInterfaces, isTailnetIPv4); } +function resolveGatewayTokenFromEnv(env: NodeJS.ProcessEnv): string | undefined { + return env.OPENCLAW_GATEWAY_TOKEN?.trim() || env.CLAWDBOT_GATEWAY_TOKEN?.trim() || undefined; +} + +function resolveGatewayPasswordFromEnv(env: NodeJS.ProcessEnv): string | undefined { + return ( + env.OPENCLAW_GATEWAY_PASSWORD?.trim() || env.CLAWDBOT_GATEWAY_PASSWORD?.trim() || undefined + ); +} + function resolveAuth(cfg: OpenClawConfig, env: NodeJS.ProcessEnv): ResolveAuthResult { const mode = cfg.gateway?.auth?.mode; const defaults = cfg.secrets?.defaults; @@ -166,13 +176,12 @@ function resolveAuth(cfg: OpenClawConfig, env: NodeJS.ProcessEnv): ResolveAuthRe value: cfg.gateway?.auth?.password, defaults, }).ref; + const envToken = resolveGatewayTokenFromEnv(env); + const envPassword = resolveGatewayPasswordFromEnv(env); const token = - env.OPENCLAW_GATEWAY_TOKEN?.trim() || - env.CLAWDBOT_GATEWAY_TOKEN?.trim() || - (tokenRef ? undefined : normalizeSecretInputString(cfg.gateway?.auth?.token)); + envToken || (tokenRef ? undefined : normalizeSecretInputString(cfg.gateway?.auth?.token)); const password = - env.OPENCLAW_GATEWAY_PASSWORD?.trim() || - env.CLAWDBOT_GATEWAY_PASSWORD?.trim() || + envPassword || (passwordRef ? undefined : normalizeSecretInputString(cfg.gateway?.auth?.password)); if (mode === "password") { @@ -208,9 +217,7 @@ async function resolveGatewayTokenSecretRef( if (!ref) { return cfg; } - const hasTokenEnvCandidate = Boolean( - env.OPENCLAW_GATEWAY_TOKEN?.trim() || env.CLAWDBOT_GATEWAY_TOKEN?.trim(), - ); + const hasTokenEnvCandidate = Boolean(resolveGatewayTokenFromEnv(env)); if (hasTokenEnvCandidate) { return cfg; } @@ -258,9 +265,7 @@ async function resolveGatewayPasswordSecretRef( if (!ref) { return cfg; } - const hasPasswordEnvCandidate = Boolean( - env.OPENCLAW_GATEWAY_PASSWORD?.trim() || env.CLAWDBOT_GATEWAY_PASSWORD?.trim(), - ); + const hasPasswordEnvCandidate = Boolean(resolveGatewayPasswordFromEnv(env)); if (hasPasswordEnvCandidate) { return cfg; } @@ -270,7 +275,7 @@ async function resolveGatewayPasswordSecretRef( } if (mode !== "password") { const hasTokenCandidate = - Boolean(env.OPENCLAW_GATEWAY_TOKEN?.trim() || env.CLAWDBOT_GATEWAY_TOKEN?.trim()) || + Boolean(resolveGatewayTokenFromEnv(env)) || hasConfiguredSecretInput(cfg.gateway?.auth?.token, cfg.secrets?.defaults); if (hasTokenCandidate) { return cfg; diff --git a/src/sessions/session-id.test.ts b/src/sessions/session-id.test.ts new file mode 100644 index 00000000000..1fb3021a242 --- /dev/null +++ b/src/sessions/session-id.test.ts @@ -0,0 +1,14 @@ +import { describe, expect, it } from "vitest"; +import { SESSION_ID_RE, looksLikeSessionId } from "./session-id.js"; + +describe("session-id", () => { + it("matches canonical UUID session ids", () => { + expect(SESSION_ID_RE.test("123e4567-e89b-12d3-a456-426614174000")).toBe(true); + expect(looksLikeSessionId(" 123e4567-e89b-12d3-a456-426614174000 ")).toBe(true); + }); + + it("rejects non-session-id values", () => { + expect(SESSION_ID_RE.test("agent:main:main")).toBe(false); + expect(looksLikeSessionId("session-label")).toBe(false); + }); +}); diff --git a/src/sessions/session-id.ts b/src/sessions/session-id.ts new file mode 100644 index 00000000000..475d017832b --- /dev/null +++ b/src/sessions/session-id.ts @@ -0,0 +1,5 @@ +export const SESSION_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; + +export function looksLikeSessionId(value: string): boolean { + return SESSION_ID_RE.test(value.trim()); +}