refactor: dedupe agent trimmed readers

This commit is contained in:
Peter Steinberger
2026-04-07 23:58:31 +01:00
parent b3ecabbbb7
commit ee0425a705
20 changed files with 62 additions and 46 deletions

View File

@@ -36,6 +36,7 @@ import { defaultRuntime, type RuntimeEnv } from "../runtime.js";
import { applyVerboseOverride } from "../sessions/level-overrides.js";
import { applyModelOverrideToSessionEntry } from "../sessions/model-overrides.js";
import { resolveSendPolicy } from "../sessions/send-policy.js";
import { normalizeOptionalString } from "../shared/string-coerce.js";
import { sanitizeForLog } from "../terminal/ansi.js";
import { resolveMessageChannel } from "../utils/message-channel.js";
import {
@@ -252,7 +253,7 @@ async function prepareAgentCommandExecution(
throw new Error('Invalid verbose level. Use "on", "full", or "off".');
}
const laneRaw = typeof opts.lane === "string" ? opts.lane.trim() : "";
const laneRaw = normalizeOptionalString(opts.lane) ?? "";
const isSubagentLane = laneRaw === String(AGENT_LANE_SUBAGENT);
const timeoutSecondsRaw =
opts.timeout !== undefined

View File

@@ -1,5 +1,6 @@
import fs from "node:fs";
import { loadJsonFile, saveJsonFile } from "../../infra/json-file.js";
import { normalizeOptionalString } from "../../shared/string-coerce.js";
import { AUTH_STORE_VERSION } from "./constants.js";
import { resolveAuthStatePath } from "./paths.js";
import type { AuthProfileState, AuthProfileStateStore, ProfileUsageStats } from "./types.js";
@@ -13,9 +14,7 @@ function normalizeAuthProfileOrder(raw: unknown): AuthProfileState["order"] {
if (!Array.isArray(value)) {
return acc;
}
const list = value
.map((entry) => (typeof entry === "string" ? entry.trim() : ""))
.filter(Boolean);
const list = value.map((entry) => normalizeOptionalString(entry) ?? "").filter(Boolean);
if (list.length > 0) {
acc[provider] = list;
}

View File

@@ -1,6 +1,7 @@
import fs from "node:fs/promises";
import type { OpenClawConfig } from "../config/config.js";
import type { AgentContextInjection } from "../config/types.agent-defaults.js";
import { normalizeOptionalString } from "../shared/string-coerce.js";
import { resolveAgentConfig, resolveSessionAgentIds } from "./agent-scope.js";
import { getOrLoadBootstrapFiles } from "./bootstrap-cache.js";
import { applyBootstrapHookOverrides } from "./bootstrap-hooks.js";
@@ -115,7 +116,7 @@ function sanitizeBootstrapFiles(
): WorkspaceBootstrapFile[] {
const sanitized: WorkspaceBootstrapFile[] = [];
for (const file of files) {
const pathValue = typeof file.path === "string" ? file.path.trim() : "";
const pathValue = normalizeOptionalString(file.path) ?? "";
if (!pathValue) {
warn?.(
`skipping bootstrap file "${file.name}" — missing or invalid "path" field (hook may have used "filePath" instead)`,

View File

@@ -7,6 +7,7 @@ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/
import type { FetchLike, Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
import { loadUndiciRuntimeDeps } from "../infra/net/undici-runtime.js";
import { logDebug } from "../logger.js";
import { normalizeOptionalString } from "../shared/string-coerce.js";
import { resolveMcpTransportConfig } from "./mcp-transport-config.js";
export type ResolvedMcpTransport = {
@@ -23,7 +24,8 @@ function attachStderrLogging(serverName: string, transport: StdioClientTransport
return undefined;
}
const onData = (chunk: Buffer | string) => {
const message = String(chunk).trim();
const message =
normalizeOptionalString(typeof chunk === "string" ? chunk : String(chunk)) ?? "";
if (!message) {
return;
}

View File

@@ -1,3 +1,4 @@
import { normalizeOptionalString } from "../shared/string-coerce.js";
import { isNonSecretApiKeyMarker } from "./model-auth-markers.js";
import type { ProviderConfig } from "./models-config.providers.secrets.js";
@@ -30,7 +31,7 @@ function getProviderModelId(model: unknown): string {
return "";
}
const id = (model as { id?: unknown }).id;
return typeof id === "string" ? id.trim() : "";
return normalizeOptionalString(id) ?? "";
}
export function mergeProviderModels(
@@ -136,7 +137,7 @@ export function mergeProviders(params: {
}): Record<string, ProviderConfig> {
const out: Record<string, ProviderConfig> = params.implicit ? { ...params.implicit } : {};
for (const [key, explicit] of Object.entries(params.explicit ?? {})) {
const providerKey = key.trim();
const providerKey = normalizeOptionalString(key) ?? "";
if (!providerKey) {
continue;
}
@@ -147,11 +148,7 @@ export function mergeProviders(params: {
}
function resolveProviderApi(entry: { api?: unknown } | undefined): string | undefined {
if (typeof entry?.api !== "string") {
return undefined;
}
const api = entry.api.trim();
return api || undefined;
return normalizeOptionalString(entry?.api);
}
function resolveModelApiSurface(entry: { models?: unknown } | undefined): string | undefined {
@@ -165,7 +162,8 @@ function resolveModelApiSurface(entry: { models?: unknown } | undefined): string
return [];
}
const api = (model as { api?: unknown }).api;
return typeof api === "string" && api.trim() ? [api.trim()] : [];
const normalized = normalizeOptionalString(api);
return normalized ? [normalized] : [];
})
.toSorted();

View File

@@ -1,4 +1,5 @@
import { MODEL_APIS } from "../config/types.models.js";
import { normalizeOptionalString } from "../shared/string-coerce.js";
import type { ProviderConfig } from "./models-config.providers.secrets.js";
const GENERIC_PROVIDER_APIS = new Set<string>([
@@ -12,7 +13,7 @@ export function resolveProviderPluginLookupKey(
providerKey: string,
provider?: ProviderConfig,
): string {
const api = typeof provider?.api === "string" ? provider.api.trim() : "";
const api = normalizeOptionalString(provider?.api) ?? "";
if (
api &&
MODEL_APIS.includes(api as (typeof MODEL_APIS)[number]) &&

View File

@@ -2,6 +2,7 @@ import fs from "node:fs/promises";
import path from "node:path";
import type { AgentMessage } from "@mariozechner/pi-agent-core";
import type { OpenClawConfig } from "../../config/config.js";
import { normalizeOptionalString } from "../../shared/string-coerce.js";
import { truncateUtf16Safe } from "../../utils.js";
import type { WorkspaceBootstrapFile } from "../workspace.js";
import type { EmbeddedContextFile } from "./types.js";
@@ -210,7 +211,7 @@ export function buildBootstrapContextFiles(
if (remainingTotalChars <= 0) {
break;
}
const pathValue = typeof file.path === "string" ? file.path.trim() : "";
const pathValue = normalizeOptionalString(file.path) ?? "";
if (!pathValue) {
opts?.warn?.(
`skipping bootstrap file "${file.name}" — missing or invalid "path" field (hook may have used "filePath" instead)`,

View File

@@ -1,5 +1,6 @@
import type { StreamFn } from "@mariozechner/pi-agent-core";
import { streamSimple } from "@mariozechner/pi-ai";
import { normalizeOptionalString } from "../../shared/string-coerce.js";
type AnthropicToolSchemaMode = "openai-functions";
type AnthropicToolChoiceMode = "openai-string-modes";
@@ -70,7 +71,7 @@ function normalizeOpenAiFunctionAnthropicToolDefinition(
return toolObj;
}
const rawName = typeof toolObj.name === "string" ? toolObj.name.trim() : "";
const rawName = normalizeOptionalString(toolObj.name) ?? "";
if (!rawName) {
return toolObj;
}

View File

@@ -3,6 +3,7 @@ import type { StreamFn } from "@mariozechner/pi-agent-core";
import type { Api, Model } from "@mariozechner/pi-ai";
import { parseGeminiAuth } from "../../infra/gemini-auth.js";
import { normalizeGoogleApiBaseUrl } from "../../infra/google-api-base-url.js";
import { normalizeOptionalString } from "../../shared/string-coerce.js";
import { buildGuardedModelFetch } from "../provider-transport-fetch.js";
import { stableStringify } from "../stable-stringify.js";
import { stripSystemPromptCacheBoundary } from "../system-prompt-cache-boundary.js";
@@ -226,7 +227,7 @@ async function createGooglePromptCache(params: {
return null;
}
const json = (await response.json()) as { name?: string; expireTime?: string };
const cachedContent = typeof json.name === "string" ? json.name.trim() : "";
const cachedContent = normalizeOptionalString(json.name) ?? "";
return cachedContent ? { cachedContent, expireTime: json.expireTime } : null;
}

View File

@@ -27,6 +27,7 @@ import { resolveToolCallArgumentsEncoding } from "../../../plugins/provider-mode
import { resolveProviderSystemPromptContribution } from "../../../plugins/provider-runtime.js";
import { isSubagentSessionKey } from "../../../routing/session-key.js";
import { normalizeOptionalLowercaseString } from "../../../shared/string-coerce.js";
import { normalizeOptionalString } from "../../../shared/string-coerce.js";
import { buildTtsSystemPromptHint } from "../../../tts/tts.js";
import { resolveUserPath } from "../../../utils.js";
import { normalizeMessageChannel } from "../../../utils/message-channel.js";
@@ -1654,8 +1655,7 @@ export async function runEmbeddedAttempt(
`hooks: prepended context to prompt (${hookResult.prependContext.length} chars)`,
);
}
const legacySystemPrompt =
typeof hookResult?.systemPrompt === "string" ? hookResult.systemPrompt.trim() : "";
const legacySystemPrompt = normalizeOptionalString(hookResult?.systemPrompt) ?? "";
if (legacySystemPrompt) {
applySystemPromptOverrideToSession(activeSession, legacySystemPrompt);
systemPromptText = legacySystemPrompt;

View File

@@ -8,6 +8,7 @@ import { emitAgentEvent } from "../infra/agent-events.js";
import { createSubsystemLogger } from "../logging/subsystem.js";
import type { InlineCodeState } from "../markdown/code-spans.js";
import { buildCodeSpanIndex, createInlineCodeState } from "../markdown/code-spans.js";
import { normalizeOptionalString } from "../shared/string-coerce.js";
import { EmbeddedBlockChunker } from "./pi-embedded-block-chunker.js";
import {
isMessagingToolDuplicateNormalized,
@@ -43,7 +44,7 @@ function collectPendingMediaFromInternalEvents(
continue;
}
for (const mediaUrl of event.mediaUrls) {
const normalized = typeof mediaUrl === "string" ? mediaUrl.trim() : "";
const normalized = normalizeOptionalString(mediaUrl) ?? "";
if (!normalized || seen.has(normalized)) {
continue;
}

View File

@@ -1,5 +1,8 @@
import type { OpenClawConfig } from "../config/config.js";
import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js";
import {
normalizeLowercaseStringOrEmpty,
normalizeOptionalString,
} from "../shared/string-coerce.js";
import type { SkillsInstallPreferences } from "./skills/types.js";
export {
@@ -38,8 +41,7 @@ export { buildWorkspaceSkillCommandSpecs } from "./skills/command-specs.js";
export function resolveSkillsInstallPreferences(config?: OpenClawConfig): SkillsInstallPreferences {
const raw = config?.skills?.install;
const preferBrew = raw?.preferBrew ?? true;
const managerRaw = typeof raw?.nodeManager === "string" ? raw.nodeManager.trim() : "";
const manager = normalizeLowercaseStringOrEmpty(managerRaw);
const manager = normalizeLowercaseStringOrEmpty(normalizeOptionalString(raw?.nodeManager));
const nodeManager: SkillsInstallPreferences["nodeManager"] =
manager === "pnpm" || manager === "yarn" || manager === "bun" || manager === "npm"
? manager

View File

@@ -3,6 +3,7 @@ import path from "node:path";
import chokidar, { type FSWatcher } from "chokidar";
import type { OpenClawConfig } from "../../config/config.js";
import { createSubsystemLogger } from "../../logging/subsystem.js";
import { normalizeOptionalString } from "../../shared/string-coerce.js";
import { CONFIG_DIR, resolveUserPath } from "../../utils.js";
import { resolvePluginSkillDirs } from "./plugin-skills.js";
import {
@@ -62,7 +63,7 @@ function resolveWatchPaths(workspaceDir: string, config?: OpenClawConfig): strin
paths.push(path.join(os.homedir(), ".agents", "skills"));
const extraDirsRaw = config?.skills?.load?.extraDirs ?? [];
const extraDirs = extraDirsRaw
.map((d) => (typeof d === "string" ? d.trim() : ""))
.map((d) => normalizeOptionalString(d) ?? "")
.filter(Boolean)
.map((dir) => resolveUserPath(dir));
paths.push(...extraDirs);

View File

@@ -4,6 +4,7 @@ import path from "node:path";
import type { OpenClawConfig } from "../../config/config.js";
import { isPathInside } from "../../infra/path-guards.js";
import { createSubsystemLogger } from "../../logging/subsystem.js";
import { normalizeOptionalString } from "../../shared/string-coerce.js";
import { CONFIG_DIR, resolveUserPath } from "../../utils.js";
import { resolveSandboxPath } from "../sandbox-paths.js";
import { resolveEffectiveAgentSkillFilter } from "./agent-filter.js";
@@ -410,9 +411,7 @@ function loadSkillEntries(
const workspaceSkillsDir = path.resolve(workspaceDir, "skills");
const bundledSkillsDir = opts?.bundledSkillsDir ?? resolveBundledSkillsDir();
const extraDirsRaw = opts?.config?.skills?.load?.extraDirs ?? [];
const extraDirs = extraDirsRaw
.map((d) => (typeof d === "string" ? d.trim() : ""))
.filter(Boolean);
const extraDirs = extraDirsRaw.map((d) => normalizeOptionalString(d) ?? "").filter(Boolean);
const pluginSkillDirs = resolvePluginSkillDirs({
workspaceDir,
config: opts?.config,

View File

@@ -282,7 +282,7 @@ async function wakeSubagentRunAfterDescendants(params: {
timeoutMs: announceTimeoutMs,
}),
});
wakeRunId = typeof wakeResponse?.runId === "string" ? wakeResponse.runId.trim() : "";
wakeRunId = normalizeOptionalString(wakeResponse?.runId) ?? "";
} catch {
return false;
}

View File

@@ -2,6 +2,7 @@ import crypto from "node:crypto";
import { promises as fs } from "node:fs";
import path from "node:path";
import type { OpenClawConfig } from "../config/config.js";
import { normalizeOptionalString } from "../shared/string-coerce.js";
import { resolveAgentWorkspaceDir } from "./agent-scope.js";
export function decodeStrictBase64(value: string, maxDecodedBytes: number): Buffer | null {
@@ -137,9 +138,9 @@ export async function materializeSubagentAttachments(params: {
let totalBytes = 0;
for (const raw of requestedAttachments) {
const name = typeof raw?.name === "string" ? raw.name.trim() : "";
const name = normalizeOptionalString(raw?.name) ?? "";
const contentVal = typeof raw?.content === "string" ? raw.content : "";
const encodingRaw = typeof raw?.encoding === "string" ? raw.encoding.trim() : "utf8";
const encodingRaw = normalizeOptionalString(raw?.encoding) ?? "utf8";
const encoding = encodingRaw === "base64" ? "base64" : "utf8";
if (!name) {

View File

@@ -6,7 +6,10 @@ import {
type OperatorScope,
} from "../../gateway/method-scopes.js";
import { formatErrorMessage } from "../../infra/errors.js";
import { normalizeLowercaseStringOrEmpty } from "../../shared/string-coerce.js";
import {
normalizeLowercaseStringOrEmpty,
normalizeOptionalString,
} from "../../shared/string-coerce.js";
import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../../utils/message-channel.js";
import { readStringParam } from "./common.js";
@@ -74,8 +77,7 @@ function validateGatewayUrlOverrideForAgentTools(params: {
]);
let remoteKey: string | undefined;
const remoteUrl =
typeof cfg.gateway?.remote?.url === "string" ? cfg.gateway.remote.url.trim() : "";
const remoteUrl = normalizeOptionalString(cfg.gateway?.remote?.url) ?? "";
if (remoteUrl) {
try {
const remote = canonicalizeToolGatewayWsUrl(remoteUrl);

View File

@@ -1,6 +1,9 @@
import type { OpenClawConfig } from "../../config/config.js";
import { isSubagentSessionKey, resolveAgentIdFromSessionKey } from "../../routing/session-key.js";
import { normalizeLowercaseStringOrEmpty } from "../../shared/string-coerce.js";
import {
normalizeLowercaseStringOrEmpty,
normalizeOptionalString,
} from "../../shared/string-coerce.js";
import {
listSpawnedSessionKeys,
resolveInternalSessionKey,
@@ -64,14 +67,14 @@ export function resolveSandboxedSessionToolContext(params: {
} {
const { mainKey, alias } = resolveMainSessionAlias(params.cfg);
const visibility = resolveSandboxSessionToolsVisibility(params.cfg);
const requesterInternalKey =
typeof params.agentSessionKey === "string" && params.agentSessionKey.trim()
? resolveInternalSessionKey({
key: params.agentSessionKey,
alias,
mainKey,
})
: undefined;
const requesterSessionKey = normalizeOptionalString(params.agentSessionKey);
const requesterInternalKey = requesterSessionKey
? resolveInternalSessionKey({
key: requesterSessionKey,
alias,
mainKey,
})
: undefined;
const effectiveRequesterKey = requesterInternalKey ?? alias;
const restrictToSpawned =
params.sandboxed === true &&
@@ -97,7 +100,9 @@ export function createAgentToAgentPolicy(cfg: OpenClawConfig): AgentToAgentPolic
return true;
}
return allowPatterns.some((pattern) => {
const raw = String(pattern ?? "").trim();
const raw =
normalizeOptionalString(typeof pattern === "string" ? pattern : String(pattern ?? "")) ??
"";
if (!raw) {
return false;
}

View File

@@ -156,7 +156,7 @@ export function createSessionsSendTool(opts?: {
params: resolveParams,
timeoutMs: 10_000,
});
resolvedKey = typeof resolved?.key === "string" ? resolved.key.trim() : "";
resolvedKey = normalizeOptionalString(resolved?.key) ?? "";
} catch (err) {
const msg = formatErrorMessage(err);
if (restrictToSpawned) {

View File

@@ -155,7 +155,7 @@ export function createSessionsSpawnTool(
);
}
const task = readStringParam(params, "task", { required: true });
const label = typeof params.label === "string" ? params.label.trim() : "";
const label = readStringParam(params, "label") ?? "";
const runtime = params.runtime === "acp" ? "acp" : "subagent";
const requestedAgentId = readStringParam(params, "agentId");
const resumeSessionId = readStringParam(params, "resumeSessionId");