diff --git a/extensions/codex/src/app-server/config-runtime.ts b/extensions/codex/src/app-server/config-runtime.ts index f5b9cc0c2dc6..3c3670d5a76c 100644 --- a/extensions/codex/src/app-server/config-runtime.ts +++ b/extensions/codex/src/app-server/config-runtime.ts @@ -1,3 +1,4 @@ +import { normalizeTrimmedStringList } from "openclaw/plugin-sdk/string-coerce-runtime"; import type { CodexAppServerApprovalPolicySource, CodexAppServerCommandSource, @@ -53,7 +54,6 @@ import { normalizeCodexServiceTier, normalizeHeaders, normalizePositiveNumber, - normalizeStringList, readBooleanEnv, readNonEmptyString, readNumberEnv, @@ -120,7 +120,7 @@ export function resolveCodexAppServerRuntimeOptions( } const args = resolveArgs(config.args, env.OPENCLAW_CODEX_APP_SERVER_ARGS); const headers = normalizeHeaders(config.headers); - const clearEnv = normalizeStringList(config.clearEnv); + const clearEnv = normalizeTrimmedStringList(config.clearEnv); const authToken = normalizeCodexAppServerSecretInput({ value: config.authToken, path: "plugins.entries.codex.config.appServer.authToken", diff --git a/extensions/codex/src/app-server/config-utils.ts b/extensions/codex/src/app-server/config-utils.ts index dcf581b670eb..b3c0a71248b9 100644 --- a/extensions/codex/src/app-server/config-utils.ts +++ b/extensions/codex/src/app-server/config-utils.ts @@ -4,7 +4,6 @@ import { normalizeResolvedSecretInputString } from "openclaw/plugin-sdk/secret-i import { asOptionalRecord as readRecord, normalizeOptionalString as readNonEmptyString, - normalizeTrimmedStringList, parseBooleanValue, } from "openclaw/plugin-sdk/string-coerce-runtime"; import type { OpenClawExecAsk, OpenClawExecSecurity } from "./config-contracts.js"; @@ -69,10 +68,6 @@ export function normalizeCodexAppServerSecretInput(params: { return normalizeResolvedSecretInputString(params); } -export function normalizeStringList(value: unknown): string[] { - return normalizeTrimmedStringList(value); -} - export function readBooleanEnv(value: string | undefined): boolean | undefined { return parseBooleanValue(value); } diff --git a/packages/model-catalog-core/src/model-catalog-normalize.ts b/packages/model-catalog-core/src/model-catalog-normalize.ts index 581bf7f3f0f8..ebf6105961a9 100644 --- a/packages/model-catalog-core/src/model-catalog-normalize.ts +++ b/packages/model-catalog-core/src/model-catalog-normalize.ts @@ -10,11 +10,7 @@ import { normalizeOptionalTrimmedStringList, normalizeTrimmedStringList, } from "@openclaw/normalization-core/string-normalization"; -import { - buildModelCatalogMergeKey, - buildModelCatalogRef, - normalizeModelCatalogProviderId, -} from "./model-catalog-refs.js"; +import { buildModelCatalogMergeKey, buildModelCatalogRef } from "./model-catalog-refs.js"; import { MODEL_CATALOG_APIS, MODEL_CATALOG_THINKING_LEVELS, @@ -39,6 +35,7 @@ import { type ModelCatalogVercelGatewayRouting, type NormalizedModelCatalogRow, } from "./model-catalog-types.js"; +import { normalizeProviderId } from "./provider-id.js"; // Normalizes raw provider model catalogs into stable rows for lookup and merging. @@ -83,7 +80,7 @@ function normalizeSafeRecordKey(value: unknown): string { function normalizeOwnedProviderSet(providers: ReadonlySet): ReadonlySet { const normalized = new Set(); for (const provider of providers) { - const providerId = normalizeModelCatalogProviderId(provider); + const providerId = normalizeProviderId(provider); if (providerId) { normalized.add(providerId); } @@ -552,7 +549,7 @@ function normalizeModelCatalogProviders( } const providers: Record = {}; for (const [rawProviderId, rawProvider] of Object.entries(value)) { - const providerId = normalizeModelCatalogProviderId(rawProviderId); + const providerId = normalizeProviderId(rawProviderId); if (!providerId || !ownedProviders.has(providerId)) { continue; } @@ -573,13 +570,11 @@ function normalizeModelCatalogAliases( } const aliases: Record = {}; for (const [rawAlias, rawTarget] of Object.entries(value)) { - const alias = normalizeModelCatalogProviderId(rawAlias); + const alias = normalizeProviderId(rawAlias); if (!alias || !isRecord(rawTarget)) { continue; } - const provider = normalizeModelCatalogProviderId( - normalizeOptionalString(rawTarget.provider) ?? "", - ); + const provider = normalizeProviderId(normalizeOptionalString(rawTarget.provider) ?? ""); if (!provider || !ownedProviders.has(provider)) { continue; } @@ -603,7 +598,7 @@ function normalizeModelCatalogSuppressions(value: unknown): ModelCatalogSuppress if (!isRecord(entry)) { continue; } - const provider = normalizeModelCatalogProviderId(normalizeOptionalString(entry.provider) ?? ""); + const provider = normalizeProviderId(normalizeOptionalString(entry.provider) ?? ""); const model = normalizeOptionalString(entry.model) ?? ""; if (!provider || !model) { continue; @@ -642,7 +637,7 @@ function normalizeModelCatalogDiscovery( } const discovery: Record = {}; for (const [rawProviderId, rawMode] of Object.entries(value)) { - const providerId = normalizeModelCatalogProviderId(rawProviderId); + const providerId = normalizeProviderId(rawProviderId); const mode = normalizeOptionalString(rawMode) ?? ""; if (providerId && ownedProviders.has(providerId) && MODEL_CATALOG_DISCOVERY_MODES.has(mode)) { discovery[providerId] = mode as ModelCatalogDiscovery; @@ -681,7 +676,7 @@ export function normalizeModelCatalogProviderRows(params: { providerCatalog: ModelCatalogProvider; source: ModelCatalogSource; }): NormalizedModelCatalogRow[] { - const provider = normalizeModelCatalogProviderId(params.provider); + const provider = normalizeProviderId(params.provider); if (!provider || !Array.isArray(params.providerCatalog.models)) { return []; } diff --git a/packages/model-catalog-core/src/model-catalog-refs.ts b/packages/model-catalog-core/src/model-catalog-refs.ts index 2e109276d614..2fd5a940f689 100644 --- a/packages/model-catalog-core/src/model-catalog-refs.ts +++ b/packages/model-catalog-core/src/model-catalog-refs.ts @@ -1,5 +1,8 @@ // Model Catalog Core module implements model catalog refs behavior. import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce"; +import { normalizeProviderId } from "./provider-id.js"; + +export { normalizeProviderId as normalizeModelCatalogProviderId } from "./provider-id.js"; // Stable model catalog ref and merge-key builders. @@ -43,14 +46,9 @@ export function isCloudModelRef(modelRef: string | undefined): boolean { return source?.source === "cloud" && parseModelSourceSuffix(source.base) === undefined; } -/** Normalize provider ids for catalog refs. */ -export function normalizeModelCatalogProviderId(provider: string): string { - return normalizeLowercaseStringOrEmpty(provider); -} - /** Build a provider/model catalog reference. */ export function buildModelCatalogRef(provider: string, modelId: string): string { - return `${normalizeModelCatalogProviderId(provider)}/${modelId}`; + return `${normalizeProviderId(provider)}/${modelId}`; } /** Parse a strict provider/model reference without normalizing either segment. */ @@ -72,12 +70,12 @@ export function parseModelCatalogRef(value: string): ModelCatalogRef | null { return null; } return { - provider: normalizeModelCatalogProviderId(parsed.provider), + provider: normalizeProviderId(parsed.provider), modelId: parsed.model, }; } /** Build a case-insensitive merge key for provider/model rows. */ export function buildModelCatalogMergeKey(provider: string, modelId: string): string { - return `${normalizeModelCatalogProviderId(provider)}::${normalizeLowercaseStringOrEmpty(modelId)}`; + return `${normalizeProviderId(provider)}::${normalizeLowercaseStringOrEmpty(modelId)}`; } diff --git a/packages/normalization-core/src/string-normalization.test.ts b/packages/normalization-core/src/string-normalization.test.ts index d26103e2e4d0..7ee8e391f87f 100644 --- a/packages/normalization-core/src/string-normalization.test.ts +++ b/packages/normalization-core/src/string-normalization.test.ts @@ -3,6 +3,7 @@ import { describe, expect, it } from "vitest"; import { filterStringEntries, normalizeAtHashSlug, + normalizeCsvOrLooseStringList, normalizeHyphenSlug, normalizeOptionalTrimmedStringList, normalizeSortedUniqueStringEntries, @@ -82,6 +83,15 @@ describe("normalization-core/string-normalization", () => { expect(normalizeOptionalTrimmedStringList(["", 42])).toBeUndefined(); }); + it.each([ + { value: " first, second, , first ", expected: ["first", "second", "first"] }, + { value: [" first ", 42, "", " ", 7], expected: ["first", "42", "7"] }, + { value: null, expected: [] }, + { value: { value: "first" }, expected: [] }, + ])("normalizes CSV or loose string-list input", ({ value, expected }) => { + expect(normalizeCsvOrLooseStringList(value)).toEqual(expected); + }); + it("normalizes sorted unique trimmed string lists", () => { expect(normalizeSortedUniqueTrimmedStringList([" b ", "a", "b", "", "a"])).toEqual(["a", "b"]); expect(normalizeSortedUniqueTrimmedStringList(["z", 1, " a "] as unknown[])).toEqual([ diff --git a/src/agents/embedded-agent-message-tool-source-reply.ts b/src/agents/embedded-agent-message-tool-source-reply.ts index 84a79c5ec9ad..0e1e6928c636 100644 --- a/src/agents/embedded-agent-message-tool-source-reply.ts +++ b/src/agents/embedded-agent-message-tool-source-reply.ts @@ -99,10 +99,6 @@ function isBareSentDeliveryStatus(value: unknown): boolean { return normalizeStatus(value) === SENT_DELIVERY_STATUS; } -function parseJsonRecord(value: string): Record | undefined { - return safeParseJsonRecord(value); -} - function recordHasDeliveredMessageId(record: Record): boolean { const hasDeliveredId = (value: unknown) => { const normalized = normalizeStatus(value); @@ -151,7 +147,7 @@ function deliveryEnvelopeHasCreatedConversationId(value: unknown, depth = 0): bo } } if (typeof record.text === "string") { - const parsed = parseJsonRecord(record.text); + const parsed = safeParseJsonRecord(record.text); if (parsed && deliveryEnvelopeHasCreatedConversationId(parsed, depth + 1)) { return true; } @@ -183,7 +179,7 @@ function deliveryEnvelopeIndicatesOk(value: unknown, depth = 0): boolean { return true; } if (typeof record.text === "string") { - const parsed = parseJsonRecord(record.text); + const parsed = safeParseJsonRecord(record.text); if (parsed && deliveryEnvelopeIndicatesOk(parsed, depth + 1)) { return true; } @@ -218,7 +214,7 @@ function deliveryEnvelopeIndicatesNonDelivery(value: unknown, depth = 0): boolea return true; } if (typeof record.text === "string") { - const parsed = parseJsonRecord(record.text); + const parsed = safeParseJsonRecord(record.text); if (parsed && deliveryEnvelopeIndicatesNonDelivery(parsed, depth + 1)) { return true; } @@ -263,7 +259,7 @@ function deliveryEnvelopeIndicatesNoOp(value: unknown, depth = 0): boolean { return true; } if (typeof record.text === "string") { - const parsed = parseJsonRecord(record.text); + const parsed = safeParseJsonRecord(record.text); if (parsed && deliveryEnvelopeIndicatesNoOp(parsed, depth + 1)) { return true; } @@ -314,7 +310,7 @@ function deliveryEnvelopeIndicatesSuccessfulBroadcast(value: unknown, depth = 0) return true; } if (typeof record.text === "string") { - const parsed = parseJsonRecord(record.text); + const parsed = safeParseJsonRecord(record.text); if (parsed && deliveryEnvelopeIndicatesSuccessfulBroadcast(parsed, depth + 1)) { return true; } @@ -348,7 +344,7 @@ function deliveryEnvelopeIndicatesDryRun(value: unknown, depth = 0): boolean { return true; } if (typeof record.text === "string") { - const parsed = parseJsonRecord(record.text); + const parsed = safeParseJsonRecord(record.text); if (parsed && deliveryEnvelopeIndicatesDryRun(parsed, depth + 1)) { return true; } @@ -363,7 +359,7 @@ function deliveryEnvelopeIndicatesDryRun(value: unknown, depth = 0): boolean { if (item && typeof item === "object" && !Array.isArray(item)) { const text = (item as Record).text; if (typeof text === "string") { - const parsed = parseJsonRecord(text); + const parsed = safeParseJsonRecord(text); if (parsed && deliveryEnvelopeIndicatesDryRun(parsed, depth + 1)) { return true; } @@ -403,7 +399,7 @@ function deliveryEnvelopeIndicatesDelivered( return true; } if (typeof record.text === "string") { - const parsed = parseJsonRecord(record.text); + const parsed = safeParseJsonRecord(record.text); if (parsed && deliveryEnvelopeIndicatesDelivered(parsed, depth + 1, requireReceipt)) { return true; } @@ -421,7 +417,7 @@ function deliveryEnvelopeIndicatesDelivered( if (item && typeof item === "object" && !Array.isArray(item)) { const text = (item as Record).text; if (typeof text === "string") { - const parsed = parseJsonRecord(text); + const parsed = safeParseJsonRecord(text); if (parsed && deliveryEnvelopeIndicatesDelivered(parsed, depth + 1, requireReceipt)) { return true; } @@ -455,7 +451,7 @@ function deliveryEnvelopeIndicatesSessionsSendAccepted(value: unknown, depth = 0 return true; } if (typeof record.text === "string") { - const parsed = parseJsonRecord(record.text); + const parsed = safeParseJsonRecord(record.text); if (parsed && deliveryEnvelopeIndicatesSessionsSendAccepted(parsed, depth + 1)) { return true; } diff --git a/src/agents/timeout.ts b/src/agents/timeout.ts index e569ceda1aab..c407f1377a96 100644 --- a/src/agents/timeout.ts +++ b/src/agents/timeout.ts @@ -6,6 +6,7 @@ import { clampTimerTimeoutMs, MAX_TIMER_TIMEOUT_MS, + resolveOptionalIntegerOption, } from "@openclaw/normalization-core/number-coercion"; import type { OpenClawConfig } from "../config/types.openclaw.js"; @@ -14,11 +15,8 @@ export const DEFAULT_AGENT_TIMEOUT_MS = DEFAULT_AGENT_TIMEOUT_SECONDS * 1000; const NO_TIMEOUT_MS = MAX_TIMER_TIMEOUT_MS; const NO_TIMEOUT_SECONDS = Math.floor(NO_TIMEOUT_MS / 1000); -const normalizeNumber = (value: unknown): number | undefined => - typeof value === "number" && Number.isFinite(value) ? Math.floor(value) : undefined; - function resolveAgentTimeoutSeconds(cfg?: OpenClawConfig): number { - const raw = normalizeNumber(cfg?.agents?.defaults?.timeoutSeconds); + const raw = resolveOptionalIntegerOption(cfg?.agents?.defaults?.timeoutSeconds); // Config 0 uses the same unlimited-run sentinel as per-run overrides. The // LLM idle watchdog still enforces liveness under that sentinel. if (raw === 0) { @@ -34,10 +32,10 @@ export function resolveAgentTimeoutMs(opts: { overrideSeconds?: number | null; minMs?: number; }): number { - const minMs = Math.max(normalizeNumber(opts.minMs) ?? 1, 1); + const minMs = Math.max(resolveOptionalIntegerOption(opts.minMs) ?? 1, 1); const clampTimeoutMs = (valueMs: number) => clampTimerTimeoutMs(valueMs, minMs) ?? minMs; const defaultMs = clampTimeoutMs(resolveAgentTimeoutSeconds(opts.cfg) * 1000); - const overrideMs = normalizeNumber(opts.overrideMs); + const overrideMs = resolveOptionalIntegerOption(opts.overrideMs); if (overrideMs !== undefined) { if (overrideMs === 0) { return NO_TIMEOUT_MS; @@ -47,7 +45,7 @@ export function resolveAgentTimeoutMs(opts: { } return clampTimeoutMs(overrideMs); } - const overrideSeconds = normalizeNumber(opts.overrideSeconds); + const overrideSeconds = resolveOptionalIntegerOption(opts.overrideSeconds); if (overrideSeconds !== undefined) { if (overrideSeconds === 0) { return NO_TIMEOUT_MS; diff --git a/src/gateway/chat-display-projection.message-tool.ts b/src/gateway/chat-display-projection.message-tool.ts index 189ca4ec38b1..0f8145ea75a4 100644 --- a/src/gateway/chat-display-projection.message-tool.ts +++ b/src/gateway/chat-display-projection.message-tool.ts @@ -30,13 +30,9 @@ function normalizeToolHistoryType(value: unknown): string | undefined { return normalized ? normalized.replace(/_/g, "") : undefined; } -function parseJsonRecord(value: string): Record | undefined { - return safeParseJsonRecord(value); -} - function readMaybeJsonRecord(value: unknown): Record | undefined { if (typeof value === "string") { - return parseJsonRecord(value); + return safeParseJsonRecord(value); } return readRecord(value); } diff --git a/src/secrets/configure.ts b/src/secrets/configure.ts index 1cda511215ab..51728a65e1cc 100644 --- a/src/secrets/configure.ts +++ b/src/secrets/configure.ts @@ -8,6 +8,7 @@ import { normalizeOptionalString, normalizeStringifiedOptionalString, } from "@openclaw/normalization-core/string-coerce"; +import { normalizeCsvOrLooseStringList } from "@openclaw/normalization-core/string-normalization"; import { listAgentIds, resolveAgentDir, resolveDefaultAgentId } from "../agents/agent-scope.js"; import { AUTH_STORE_VERSION } from "../agents/auth-profiles/constants.js"; import { loadPersistedAuthProfileStore } from "../agents/auth-profiles/persisted.js"; @@ -66,13 +67,6 @@ function isAbsolutePathValue(value: string): boolean { ); } -function parseCsv(value: string): string[] { - return value - .split(",") - .map((entry) => entry.trim()) - .filter((entry) => entry.length > 0); -} - function parseOptionalPositiveInt(value: string, max: number): number | undefined { const trimmed = value.trim(); if (!trimmed) { @@ -216,7 +210,7 @@ function assertNoCancel(value: T | symbol, message: string): T { const AUTH_PROFILE_ID_PATTERN = /^[A-Za-z0-9:_-]{1,128}$/; function validateEnvNameCsv(value: string): string | undefined { - const entries = parseCsv(value); + const entries = normalizeCsvOrLooseStringList(value); for (const entry of entries) { if (!isValidEnvSecretRefId(entry)) { return `Invalid env name: ${entry}`; @@ -237,7 +231,7 @@ async function promptEnvNameCsv(params: { }), "Secrets configure cancelled.", ); - return parseCsv(raw ?? ""); + return normalizeCsvOrLooseStringList(raw ?? ""); } async function promptOptionalPositiveInt(params: { @@ -599,7 +593,7 @@ async function promptExecProvider( message: "Trusted dirs (comma-separated absolute paths, blank for none)", initialValue: base?.trustedDirs?.join(",") ?? "", validate: (value) => { - const entries = parseCsv(value ?? ""); + const entries = normalizeCsvOrLooseStringList(value ?? ""); for (const entry of entries) { if (!isAbsolutePathValue(entry)) { return `Trusted dir must be absolute: ${entry}`; @@ -612,7 +606,7 @@ async function promptExecProvider( ); const args = await parseArgsInput(normalizeStringifiedOptionalString(argsRaw) ?? ""); - const trustedDirs = parseCsv(trustedDirsRaw ?? ""); + const trustedDirs = normalizeCsvOrLooseStringList(trustedDirsRaw ?? ""); return { source: "exec", diff --git a/src/tasks/task-flow-registry.store.sqlite.ts b/src/tasks/task-flow-registry.store.sqlite.ts index dbce72b94edb..f07e4168a87c 100644 --- a/src/tasks/task-flow-registry.store.sqlite.ts +++ b/src/tasks/task-flow-registry.store.sqlite.ts @@ -1,6 +1,5 @@ // Persists managed task-flow records through the OpenClaw SQLite state database. import type { DatabaseSync } from "node:sqlite"; -import { safeParseJson } from "@openclaw/normalization-core"; import type { Insertable, Selectable } from "kysely"; import { executeSqliteQuerySync, getNodeSqliteKysely } from "../infra/kysely-sync.js"; import { normalizeSqliteNumber } from "../infra/sqlite-number.js"; @@ -18,7 +17,7 @@ import { type TaskFlowRecord, type TaskFlowSyncMode, } from "./task-flow-registry.types.js"; -import { parseDeliveryContextJson } from "./task-registry.sqlite.shared.js"; +import { parseDeliveryContextJson, parseSqliteJsonValue } from "./task-registry.sqlite.shared.js"; import { parseTaskNotifyPolicy } from "./task-registry.types.js"; type FlowRunsTable = OpenClawStateKyselyDatabase["flow_runs"]; @@ -42,13 +41,6 @@ function serializeJson(value: unknown): string | null { return value === undefined ? null : JSON.stringify(value); } -function parseJsonValue(raw: string | null): JsonValue | undefined { - if (!raw?.trim()) { - return undefined; - } - return safeParseJson(raw) as JsonValue | undefined; -} - function rowToSyncMode(row: FlowRegistryRow): TaskFlowSyncMode { // Older single_task rows did not persist sync_mode; preserve their mirrored semantics. const syncMode = parseOptionalTaskFlowSyncMode(row.sync_mode); @@ -62,8 +54,8 @@ function rowToFlowRecord(row: FlowRegistryRow): TaskFlowRecord { const endedAt = normalizeSqliteNumber(row.ended_at); const cancelRequestedAt = normalizeSqliteNumber(row.cancel_requested_at); const requesterOrigin = parseDeliveryContextJson(row.requester_origin_json); - const stateJson = parseJsonValue(row.state_json); - const waitJson = parseJsonValue(row.wait_json); + const stateJson = parseSqliteJsonValue(row.state_json); + const waitJson = parseSqliteJsonValue(row.wait_json); return { flowId: row.flow_id, syncMode: rowToSyncMode(row), diff --git a/src/tasks/task-registry.sqlite.shared.ts b/src/tasks/task-registry.sqlite.shared.ts index 72a41f18dd7b..192dd5869778 100644 --- a/src/tasks/task-registry.sqlite.shared.ts +++ b/src/tasks/task-registry.sqlite.shared.ts @@ -5,7 +5,7 @@ import { normalizeDeliveryContext } from "../utils/delivery-context.shared.js"; import type { DeliveryContext } from "../utils/delivery-context.types.js"; // oxlint-disable-next-line typescript/no-unnecessary-type-parameters -- Persisted JSON columns are typed by the receiving field. -function parseSqliteJsonValue(raw: string | null): T | undefined { +export function parseSqliteJsonValue(raw: string | null): T | undefined { if (!raw?.trim()) { return undefined; } diff --git a/src/tasks/task-registry.store.sqlite.ts b/src/tasks/task-registry.store.sqlite.ts index 174e8e90917b..57b36a3d35d5 100644 --- a/src/tasks/task-registry.store.sqlite.ts +++ b/src/tasks/task-registry.store.sqlite.ts @@ -1,6 +1,5 @@ // Persists task registry records and events through the OpenClaw SQLite state database. import type { DatabaseSync } from "node:sqlite"; -import { safeParseJson } from "@openclaw/normalization-core"; import type { Insertable, Selectable } from "kysely"; import { executeSqliteQuerySync, getNodeSqliteKysely } from "../infra/kysely-sync.js"; import { assertSqliteTableIntegrity } from "../infra/sqlite-integrity.js"; @@ -13,7 +12,7 @@ import { runOpenClawStateWriteTransaction, type OpenClawStateDatabase, } from "../state/openclaw-state-db.js"; -import { parseDeliveryContextJson } from "./task-registry.sqlite.shared.js"; +import { parseDeliveryContextJson, parseSqliteJsonValue } from "./task-registry.sqlite.shared.js"; import type { TaskRegistryStoreSnapshot } from "./task-registry.store.types.js"; import { parseOptionalTaskTerminalOutcome, @@ -91,13 +90,6 @@ function serializeJson(value: unknown): string | null { return value === undefined ? null : (JSON.stringify(value) ?? null); } -function parseJsonValue(raw: string | null): JsonValue | undefined { - if (!raw?.trim()) { - return undefined; - } - return safeParseJson(raw) as JsonValue | undefined; -} - function rowToTaskRecord(row: TaskRegistryRow): TaskRecord { const startedAt = normalizeSqliteNumber(row.started_at); const endedAt = normalizeSqliteNumber(row.ended_at); @@ -106,7 +98,7 @@ function rowToTaskRecord(row: TaskRegistryRow): TaskRecord { const toolUseCount = normalizeSqliteNumber(row.tool_use_count); const scopeKind = parseTaskScopeKind(row.scope_kind); const terminalOutcome = parseOptionalTaskTerminalOutcome(row.terminal_outcome); - const detail = parseJsonValue(row.detail_json); + const detail = parseSqliteJsonValue(row.detail_json); // System tasks intentionally have no requester session; ownerKey is the lookup anchor. const requesterSessionKey = scopeKind === "system" ? "" : row.requester_session_key?.trim() || row.owner_key; diff --git a/ui/src/lib/agents/display.ts b/ui/src/lib/agents/display.ts index 524eb9fa288d..05a589c7c8e0 100644 --- a/ui/src/lib/agents/display.ts +++ b/ui/src/lib/agents/display.ts @@ -535,13 +535,6 @@ export function resolveEffectiveModelFallbacks( return resolveModelPrimary(entryModel) ? [] : resolveModelFallbacks(defaultModel); } -export function parseFallbackList(value: string): string[] { - return value - .split(",") - .map((entry) => entry.trim()) - .filter(Boolean); -} - type ConfiguredModelOption = { value: string; label: string; diff --git a/ui/src/pages/agents/panels-overview.ts b/ui/src/pages/agents/panels-overview.ts index a838c3b1f83e..53dbe38bbaed 100644 --- a/ui/src/pages/agents/panels-overview.ts +++ b/ui/src/pages/agents/panels-overview.ts @@ -1,4 +1,5 @@ // Control UI view renders agents panels overview screen content. +import { normalizeCsvOrLooseStringList } from "@openclaw/normalization-core/string-normalization"; import { html, nothing } from "lit"; import type { AgentIdentityResult, @@ -13,7 +14,6 @@ import { t } from "../../i18n/index.ts"; import { buildModelOptions, normalizeModelValue, - parseFallbackList, resolveAgentConfig, resolveAgentRuntimeLabel, resolveAgentTextAvatar, @@ -141,7 +141,7 @@ export function renderAgentOverview(params: { const input = e.target as HTMLInputElement; if (e.key === "Enter" || e.key === ",") { e.preventDefault(); - const parsed = parseFallbackList(input.value); + const parsed = normalizeCsvOrLooseStringList(input.value); if (parsed.length > 0) { onModelFallbacksChange(agent.id, [...fallbackChips, ...parsed]); input.value = ""; @@ -357,7 +357,7 @@ export function renderAgentOverview(params: { @keydown=${handleChipKeydown} @blur=${(e: Event) => { const input = e.target as HTMLInputElement; - const parsed = parseFallbackList(input.value); + const parsed = normalizeCsvOrLooseStringList(input.value); if (parsed.length > 0) { onModelFallbacksChange(agent.id, [...fallbackChips, ...parsed]); input.value = "";