From d9f1c61361b05bcea68eba3aa5a6f74cf0934070 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 7 Apr 2026 05:44:08 +0100 Subject: [PATCH] refactor: dedupe reply session readers --- src/auto-reply/reply/abort-cutoff.ts | 17 +++-- src/auto-reply/reply/acp-projector.ts | 9 +-- src/auto-reply/reply/agent-runner-utils.ts | 13 ++-- .../reply/commands-acp/lifecycle.ts | 30 ++++----- src/auto-reply/reply/commands-acp/shared.ts | 42 ++++++------ src/auto-reply/reply/commands-plugin.ts | 3 +- src/auto-reply/reply/dispatch-acp.ts | 65 ++++++++----------- src/auto-reply/reply/get-reply-fast-path.ts | 31 +++++---- src/auto-reply/reply/get-reply-run.ts | 23 ++++--- src/auto-reply/reply/queue/state.ts | 8 ++- src/auto-reply/reply/reply-payloads-base.ts | 11 ++-- 11 files changed, 130 insertions(+), 122 deletions(-) diff --git a/src/auto-reply/reply/abort-cutoff.ts b/src/auto-reply/reply/abort-cutoff.ts index 4bbc98d5a90..ff29cd5805b 100644 --- a/src/auto-reply/reply/abort-cutoff.ts +++ b/src/auto-reply/reply/abort-cutoff.ts @@ -1,4 +1,5 @@ import type { SessionEntry } from "../../config/sessions/types.js"; +import { normalizeOptionalString } from "../../shared/string-coerce.js"; import type { MsgContext } from "../templating.js"; export type AbortCutoff = { @@ -10,9 +11,7 @@ type SessionAbortCutoffEntry = Pick): string { const detailParts: string[] = []; - const title = event.title?.trim(); + const title = normalizeOptionalString(event.title); if (title) { detailParts.push(title); } - const status = event.status?.trim(); + const status = normalizeOptionalString(event.status); if (status) { detailParts.push(`status=${status}`); } - const fallback = event.text?.trim(); + const fallback = normalizeOptionalString(event.text); if (detailParts.length === 0 && fallback) { detailParts.push(fallback); } @@ -334,7 +335,7 @@ export function createAcpReplyProjector(params: { const renderedToolSummary = renderToolSummaryText(event); const toolSummary = truncateText(renderedToolSummary, settings.maxSessionUpdateChars); const hash = hashText(renderedToolSummary); - const toolCallId = event.toolCallId?.trim() || undefined; + const toolCallId = normalizeOptionalString(event.toolCallId); const status = normalizeToolStatus(event.status); const isTerminal = status ? TERMINAL_TOOL_STATUSES.has(status) : false; const isStart = status === "in_progress" || event.tag === "tool_call"; diff --git a/src/auto-reply/reply/agent-runner-utils.ts b/src/auto-reply/reply/agent-runner-utils.ts index b4ec6754712..9af998c2417 100644 --- a/src/auto-reply/reply/agent-runner-utils.ts +++ b/src/auto-reply/reply/agent-runner-utils.ts @@ -3,6 +3,7 @@ import { getChannelPlugin } from "../../channels/plugins/index.js"; import type { ChannelId, ChannelThreadingToolContext } from "../../channels/plugins/types.js"; import { normalizeAnyChannelId, normalizeChannelId } from "../../channels/registry.js"; import type { OpenClawConfig } from "../../config/config.js"; +import { normalizeOptionalString } from "../../shared/string-coerce.js"; import { isReasoningTagProvider } from "../../utils/provider-utils.js"; import type { TemplateContext } from "../templating.js"; import { @@ -38,7 +39,7 @@ export function buildThreadingToolContext(params: { currentMessageId, }; } - const rawProvider = originProvider?.trim().toLowerCase(); + const rawProvider = normalizeOptionalString(originProvider)?.toLowerCase(); if (!rawProvider) { return { currentMessageId, @@ -49,7 +50,7 @@ export function buildThreadingToolContext(params: { const threading = provider ? getChannelPlugin(provider)?.threading : undefined; if (!threading?.buildToolContext) { return { - currentChannelId: originTo?.trim() || undefined, + currentChannelId: normalizeOptionalString(originTo), currentChannelProvider: provider ?? (rawProvider as ChannelId), currentMessageId, hasRepliedRef, @@ -184,10 +185,10 @@ export function buildEmbeddedContextFromTemplate(params: { export function buildTemplateSenderContext(sessionCtx: TemplateContext) { return { - senderId: sessionCtx.SenderId?.trim() || undefined, - senderName: sessionCtx.SenderName?.trim() || undefined, - senderUsername: sessionCtx.SenderUsername?.trim() || undefined, - senderE164: sessionCtx.SenderE164?.trim() || undefined, + senderId: normalizeOptionalString(sessionCtx.SenderId), + senderName: normalizeOptionalString(sessionCtx.SenderName), + senderUsername: normalizeOptionalString(sessionCtx.SenderUsername), + senderE164: normalizeOptionalString(sessionCtx.SenderE164), }; } diff --git a/src/auto-reply/reply/commands-acp/lifecycle.ts b/src/auto-reply/reply/commands-acp/lifecycle.ts index 938a98b9265..b0fb9e55a38 100644 --- a/src/auto-reply/reply/commands-acp/lifecycle.ts +++ b/src/auto-reply/reply/commands-acp/lifecycle.ts @@ -41,6 +41,7 @@ import { type SessionBindingRecord, type SessionBindingService, } from "../../../infra/outbound/session-binding-service.js"; +import { normalizeOptionalString } from "../../../shared/string-coerce.js"; import type { CommandHandlerResult, HandleCommandsParams } from "../commands-types.js"; import { resolveAcpCommandAccountId, @@ -240,7 +241,7 @@ async function bindSpawnedAcpSessionToCurrentConversation(params: { }; } - const currentConversationId = bindingContext.conversationId?.trim() || ""; + const currentConversationId = normalizeOptionalString(bindingContext.conversationId) ?? ""; if (!currentConversationId) { return { ok: false, @@ -248,8 +249,8 @@ async function bindSpawnedAcpSessionToCurrentConversation(params: { }; } - const senderId = params.commandParams.command.senderId?.trim() || ""; - const parentConversationId = bindingContext.parentConversationId?.trim() || undefined; + const senderId = normalizeOptionalString(params.commandParams.command.senderId) ?? ""; + const parentConversationId = normalizeOptionalString(bindingContext.parentConversationId); const conversationRef = { channel: bindingPolicy.channel, accountId: bindingPolicy.accountId, @@ -259,10 +260,7 @@ async function bindSpawnedAcpSessionToCurrentConversation(params: { : {}), }; const existingBinding = bindingService.resolveByConversation(conversationRef); - const boundBy = - typeof existingBinding?.metadata?.boundBy === "string" - ? existingBinding.metadata.boundBy.trim() - : ""; + const boundBy = normalizeOptionalString(existingBinding?.metadata?.boundBy) ?? ""; if (existingBinding && boundBy && boundBy !== "system" && senderId && senderId !== boundBy) { const currentLabel = resolveAcpBindingLabelNoun({ placement: "current", @@ -364,7 +362,7 @@ async function bindSpawnedAcpSessionToThread(params: { } const currentThreadId = bindingContext.threadId ?? ""; - const currentConversationId = bindingContext.conversationId?.trim() || ""; + const currentConversationId = normalizeOptionalString(bindingContext.conversationId) ?? ""; const requiresThreadIdForHere = requiresNativeThreadContextForThreadHere(channel); if ( threadMode === "here" && @@ -394,8 +392,8 @@ async function bindSpawnedAcpSessionToThread(params: { }; } - const senderId = commandParams.command.senderId?.trim() || ""; - const parentConversationId = bindingContext.parentConversationId?.trim() || undefined; + const senderId = normalizeOptionalString(commandParams.command.senderId) ?? ""; + const parentConversationId = normalizeOptionalString(bindingContext.parentConversationId); const conversationRef = { channel: spawnPolicy.channel, accountId: spawnPolicy.accountId, @@ -406,10 +404,7 @@ async function bindSpawnedAcpSessionToThread(params: { }; if (placement === "current") { const existingBinding = bindingService.resolveByConversation(conversationRef); - const boundBy = - typeof existingBinding?.metadata?.boundBy === "string" - ? existingBinding.metadata.boundBy.trim() - : ""; + const boundBy = normalizeOptionalString(existingBinding?.metadata?.boundBy) ?? ""; if (existingBinding && boundBy && boundBy !== "system" && senderId && senderId !== boundBy) { const currentLabel = resolveAcpBindingLabelNoun({ placement, @@ -460,7 +455,7 @@ async function persistSpawnedSessionLabel(params: { sessionKey: string; label?: string; }): Promise { - const label = params.label?.trim(); + const label = normalizeOptionalString(params.label); if (!label) { return; } @@ -616,7 +611,8 @@ export async function handleAcpSpawnAction( `✅ Spawned ACP session ${sessionKey} (${spawn.mode}, backend ${initializedBackend}).`, ]; if (binding) { - const currentConversationId = resolveAcpCommandConversationId(params)?.trim() || ""; + const currentConversationId = + normalizeOptionalString(resolveAcpCommandConversationId(params)) ?? ""; const boundConversationId = binding.conversation.conversationId.trim(); const bindingPlacement = currentConversationId && boundConversationId === currentConversationId ? "current" : "child"; @@ -683,7 +679,7 @@ async function resolveAcpTokenTargetSessionKeyOrStop(params: { commandParams: HandleCommandsParams; restTokens: string[]; }): Promise { - const token = params.restTokens.join(" ").trim() || undefined; + const token = normalizeOptionalString(params.restTokens.join(" ")); const target = await resolveAcpTargetSessionKey({ commandParams: params.commandParams, token, diff --git a/src/auto-reply/reply/commands-acp/shared.ts b/src/auto-reply/reply/commands-acp/shared.ts index 8ad8759c802..e064c35f9be 100644 --- a/src/auto-reply/reply/commands-acp/shared.ts +++ b/src/auto-reply/reply/commands-acp/shared.ts @@ -5,6 +5,7 @@ import type { AcpRuntimeSessionMode } from "../../../acp/runtime/types.js"; import { supportsAutomaticThreadBindingSpawn } from "../../../channels/thread-bindings-policy.js"; import type { AcpSessionRuntimeOptions } from "../../../config/sessions/types.js"; import { normalizeAgentId } from "../../../routing/session-key.js"; +import { normalizeOptionalString } from "../../../shared/string-coerce.js"; import type { CommandHandlerResult, HandleCommandsParams } from "../commands-types.js"; import { resolveAcpCommandChannel, resolveAcpCommandThreadId } from "./context.js"; export { resolveAcpInstallCommandHint, resolveConfiguredAcpBackendId } from "./install-hints.js"; @@ -90,7 +91,7 @@ export function stopWithText(text: string): CommandHandlerResult { } export function resolveAcpAction(tokens: string[]): AcpAction { - const action = tokens[0]?.trim().toLowerCase(); + const action = normalizeOptionalString(tokens[0])?.toLowerCase(); if ( action === "spawn" || action === "cancel" || @@ -199,7 +200,7 @@ export function parseSpawnInput( if (modeOption.error) { return { ok: false, error: `${modeOption.error}. ${ACP_SPAWN_USAGE}` }; } - const raw = modeOption.value?.trim().toLowerCase(); + const raw = normalizeOptionalString(modeOption.value)?.toLowerCase(); if (raw !== "persistent" && raw !== "oneshot") { return { ok: false, @@ -216,7 +217,7 @@ export function parseSpawnInput( if (bindOption.error) { return { ok: false, error: `${bindOption.error}. ${ACP_SPAWN_USAGE}` }; } - const raw = bindOption.value?.trim().toLowerCase(); + const raw = normalizeOptionalString(bindOption.value)?.toLowerCase(); if (raw !== "here" && raw !== "off") { return { ok: false, @@ -237,7 +238,7 @@ export function parseSpawnInput( if (threadOption.error) { return { ok: false, error: `${threadOption.error}. ${ACP_SPAWN_USAGE}` }; } - const raw = threadOption.value?.trim().toLowerCase(); + const raw = normalizeOptionalString(threadOption.value)?.toLowerCase(); if (raw !== "auto" && raw !== "here" && raw !== "off") { return { ok: false, @@ -255,7 +256,7 @@ export function parseSpawnInput( if (cwdOption.error) { return { ok: false, error: `${cwdOption.error}. ${ACP_SPAWN_USAGE}` }; } - cwd = cwdOption.value?.trim(); + cwd = normalizeOptionalString(cwdOption.value); i = cwdOption.nextIndex; continue; } @@ -265,7 +266,7 @@ export function parseSpawnInput( if (labelOption.error) { return { ok: false, error: `${labelOption.error}. ${ACP_SPAWN_USAGE}` }; } - label = labelOption.value?.trim(); + label = normalizeOptionalString(labelOption.value); i = labelOption.nextIndex; continue; } @@ -278,7 +279,7 @@ export function parseSpawnInput( } if (!rawAgentId) { - rawAgentId = token.trim(); + rawAgentId = normalizeOptionalString(token); i += 1; continue; } @@ -289,8 +290,8 @@ export function parseSpawnInput( }; } - const fallbackAgent = params.cfg.acp?.defaultAgent?.trim() || ""; - const selectedAgent = (rawAgentId?.trim() || fallbackAgent).trim(); + const fallbackAgent = normalizeOptionalString(params.cfg.acp?.defaultAgent) ?? ""; + const selectedAgent = normalizeOptionalString(rawAgentId) ?? fallbackAgent; if (!selectedAgent) { return { ok: false, @@ -316,7 +317,7 @@ export function parseSpawnInput( thread, bind, cwd, - label: label || undefined, + label, }, }; } @@ -341,7 +342,7 @@ export function parseSteerInput( error: `${sessionOption.error}. ${ACP_STEER_USAGE}`, }; } - sessionToken = sessionOption.value?.trim() || undefined; + sessionToken = normalizeOptionalString(sessionOption.value); i = sessionOption.nextIndex; continue; } @@ -371,14 +372,14 @@ export function parseSingleValueCommandInput( tokens: string[], usage: string, ): { ok: true; value: ParsedSingleValueCommandInput } | { ok: false; error: string } { - const value = tokens[0]?.trim() || ""; + const value = normalizeOptionalString(tokens[0]) ?? ""; if (!value) { return { ok: false, error: usage }; } if (tokens.length > 2) { return { ok: false, error: usage }; } - const sessionToken = tokens[1]?.trim() || undefined; + const sessionToken = normalizeOptionalString(tokens[1]); return { ok: true, value: { @@ -391,8 +392,8 @@ export function parseSingleValueCommandInput( export function parseSetCommandInput( tokens: string[], ): { ok: true; value: ParsedSetCommandInput } | { ok: false; error: string } { - const key = tokens[0]?.trim() || ""; - const value = tokens[1]?.trim() || ""; + const key = normalizeOptionalString(tokens[0]) ?? ""; + const value = normalizeOptionalString(tokens[1]) ?? ""; if (!key || !value) { return { ok: false, @@ -405,7 +406,7 @@ export function parseSetCommandInput( error: ACP_SET_USAGE, }; } - const sessionToken = tokens[2]?.trim() || undefined; + const sessionToken = normalizeOptionalString(tokens[2]); return { ok: true, value: { @@ -423,7 +424,7 @@ export function parseOptionalSingleTarget( if (tokens.length > 1) { return { ok: false, error: usage }; } - const token = tokens[0]?.trim() || ""; + const token = normalizeOptionalString(tokens[0]) ?? ""; return { ok: true, ...(token ? { sessionToken: token } : {}), @@ -492,8 +493,11 @@ export function resolveCommandRequestId(params: HandleCommandsParams): string { params.ctx.MessageSid ?? params.ctx.MessageSidFirst ?? params.ctx.MessageSidLast; - if (typeof value === "string" && value.trim()) { - return value.trim(); + if (typeof value === "string") { + const normalizedValue = normalizeOptionalString(value); + if (normalizedValue) { + return normalizedValue; + } } if (typeof value === "number" || typeof value === "bigint") { return String(value); diff --git a/src/auto-reply/reply/commands-plugin.ts b/src/auto-reply/reply/commands-plugin.ts index 29d1ee8b7a0..519aa59bbea 100644 --- a/src/auto-reply/reply/commands-plugin.ts +++ b/src/auto-reply/reply/commands-plugin.ts @@ -6,6 +6,7 @@ */ import { matchPluginCommand, executePluginCommand } from "../../plugins/commands.js"; +import { normalizeOptionalString } from "../../shared/string-coerce.js"; import type { CommandHandler, CommandHandlerResult } from "./commands-types.js"; /** @@ -50,7 +51,7 @@ export const handlePluginCommand: CommandHandler = async ( typeof params.ctx.MessageThreadId === "number" ? params.ctx.MessageThreadId : undefined, - threadParentId: params.ctx.ThreadParentId?.trim() || undefined, + threadParentId: normalizeOptionalString(params.ctx.ThreadParentId), }); return { diff --git a/src/auto-reply/reply/dispatch-acp.ts b/src/auto-reply/reply/dispatch-acp.ts index c91cdac6995..e9d51c5596f 100644 --- a/src/auto-reply/reply/dispatch-acp.ts +++ b/src/auto-reply/reply/dispatch-acp.ts @@ -14,6 +14,7 @@ import { formatErrorMessage } from "../../infra/errors.js"; import { generateSecureUuid } from "../../infra/secure-random.js"; import { prefixSystemMessage } from "../../infra/system-message.js"; import { resolveAgentIdFromSessionKey } from "../../routing/session-key.js"; +import { normalizeOptionalString } from "../../shared/string-coerce.js"; import { resolveStatusTtsSnapshot } from "../../tts/status-config.js"; import { resolveConfiguredTtsMode } from "../../tts/tts-config.js"; import type { FinalizedMsgContext } from "../templating.js"; @@ -94,8 +95,11 @@ function hasInboundMediaForAcp(ctx: FinalizedMsgContext): boolean { function resolveAcpRequestId(ctx: FinalizedMsgContext): string { const id = ctx.MessageSidFull ?? ctx.MessageSid ?? ctx.MessageSidFirst ?? ctx.MessageSidLast; - if (typeof id === "string" && id.trim()) { - return id.trim(); + if (typeof id === "string") { + const normalizedId = normalizeOptionalString(id); + if (normalizedId) { + return normalizedId; + } } if (typeof id === "number" || typeof id === "bigint") { return String(id); @@ -109,33 +113,24 @@ async function hasBoundConversationForSession(params: { channelRaw: string | undefined; accountIdRaw: string | undefined; }): Promise { - const channel = String(params.channelRaw ?? "") - .trim() - .toLowerCase(); + const channel = normalizeOptionalString(params.channelRaw)?.toLowerCase() ?? ""; if (!channel) { return false; } - const accountId = String(params.accountIdRaw ?? "") - .trim() - .toLowerCase(); + const accountId = normalizeOptionalString(params.accountIdRaw)?.toLowerCase() ?? ""; const channels = params.cfg.channels as Record; const configuredDefaultAccountId = channels?.[channel]?.defaultAccount; const normalizedAccountId = - accountId || - (typeof configuredDefaultAccountId === "string" && configuredDefaultAccountId.trim() - ? configuredDefaultAccountId.trim().toLowerCase() - : "default"); + accountId || normalizeOptionalString(configuredDefaultAccountId)?.toLowerCase() || "default"; const { getSessionBindingService } = await loadDispatchAcpManagerRuntime(); const bindingService = getSessionBindingService(); const bindings = bindingService.listBySession(params.sessionKey); return bindings.some((binding) => { - const bindingChannel = String(binding.conversation.channel ?? "") - .trim() - .toLowerCase(); - const bindingAccountId = String(binding.conversation.accountId ?? "") - .trim() - .toLowerCase(); - const conversationId = String(binding.conversation.conversationId ?? "").trim(); + const bindingChannel = + normalizeOptionalString(binding.conversation.channel)?.toLowerCase() ?? ""; + const bindingAccountId = + normalizeOptionalString(binding.conversation.accountId)?.toLowerCase() ?? ""; + const conversationId = normalizeOptionalString(binding.conversation.conversationId) ?? ""; return ( bindingChannel === channel && (bindingAccountId || "default") === normalizedAccountId && @@ -149,21 +144,17 @@ function resolveDispatchAccountId(params: { channelRaw: string | undefined; accountIdRaw: string | undefined; }): string | undefined { - const channel = String(params.channelRaw ?? "") - .trim() - .toLowerCase(); + const channel = normalizeOptionalString(params.channelRaw)?.toLowerCase() ?? ""; if (!channel) { - return params.accountIdRaw?.trim() || undefined; + return normalizeOptionalString(params.accountIdRaw); } - const explicit = params.accountIdRaw?.trim(); + const explicit = normalizeOptionalString(params.accountIdRaw); if (explicit) { return explicit; } const channels = params.cfg.channels as Record; const configuredDefaultAccountId = channels?.[channel]?.defaultAccount; - return typeof configuredDefaultAccountId === "string" && configuredDefaultAccountId.trim() - ? configuredDefaultAccountId.trim() - : undefined; + return normalizeOptionalString(configuredDefaultAccountId); } export type AcpDispatchAttemptResult = { @@ -315,7 +306,7 @@ export async function tryDispatchAcpReply(params: { recordProcessed: DispatchProcessedRecorder; markIdle: (reason: string) => void; }): Promise { - const sessionKey = params.sessionKey?.trim(); + const sessionKey = normalizeOptionalString(params.sessionKey); if (!sessionKey || params.bypassForCommand) { return null; } @@ -362,11 +353,9 @@ export async function tryDispatchAcpReply(params: { const resolvedAcpAgent = acpResolution.kind === "ready" - ? ( - acpResolution.meta.agent?.trim() || - params.cfg.acp?.defaultAgent?.trim() || - resolveAgentIdFromSessionKey(canonicalSessionKey) - ).trim() + ? (normalizeOptionalString(acpResolution.meta.agent) ?? + normalizeOptionalString(params.cfg.acp?.defaultAgent) ?? + resolveAgentIdFromSessionKey(canonicalSessionKey)) : resolveAgentIdFromSessionKey(canonicalSessionKey); const effectiveDispatchAccountId = resolveDispatchAccountId({ cfg: params.cfg, @@ -470,9 +459,10 @@ export async function tryDispatchAcpReply(params: { const counts = params.dispatcher.getQueuedCounts(); delivery.applyRoutedCounts(counts); const acpStats = acpManager.getObservabilitySnapshot(params.cfg); - if (params.runId?.trim()) { + const runId = normalizeOptionalString(params.runId); + if (runId) { emitAgentEvent({ - runId: params.runId.trim(), + runId, sessionKey, stream: "lifecycle", data: { @@ -507,9 +497,10 @@ export async function tryDispatchAcpReply(params: { const counts = params.dispatcher.getQueuedCounts(); delivery.applyRoutedCounts(counts); const acpStats = acpManager.getObservabilitySnapshot(params.cfg); - if (params.runId?.trim()) { + const runId = normalizeOptionalString(params.runId); + if (runId) { emitAgentEvent({ - runId: params.runId.trim(), + runId, sessionKey, stream: "lifecycle", data: { diff --git a/src/auto-reply/reply/get-reply-fast-path.ts b/src/auto-reply/reply/get-reply-fast-path.ts index 10643739b3c..3879f542df4 100644 --- a/src/auto-reply/reply/get-reply-fast-path.ts +++ b/src/auto-reply/reply/get-reply-fast-path.ts @@ -5,6 +5,7 @@ import { normalizeAnyChannelId } from "../../channels/registry.js"; import type { OpenClawConfig } from "../../config/config.js"; import { applyMergePatch } from "../../config/merge-patch.js"; import type { SessionEntry } from "../../config/sessions/types.js"; +import { normalizeOptionalString } from "../../shared/string-coerce.js"; import { normalizeCommandBody } from "../commands-registry.js"; import type { MsgContext, TemplateContext } from "../templating.js"; import type { CommandContext } from "./commands-types.js"; @@ -26,12 +27,14 @@ function isSlowReplyTestAllowed(env: NodeJS.ProcessEnv = process.env): boolean { } function resolveFastSessionKey(ctx: MsgContext): string { - const existing = ctx.SessionKey?.trim(); + const existing = normalizeOptionalString(ctx.SessionKey); if (existing) { return existing; } - const provider = ctx.Provider?.trim() || ctx.Surface?.trim() || "main"; - const destination = ctx.To?.trim() || ctx.From?.trim() || "default"; + const provider = + normalizeOptionalString(ctx.Provider) ?? normalizeOptionalString(ctx.Surface) ?? "main"; + const destination = + normalizeOptionalString(ctx.To) ?? normalizeOptionalString(ctx.From) ?? "default"; return `agent:main:${provider}:${destination}`; } @@ -152,10 +155,10 @@ export function buildFastReplyCommandContext(params: { }): CommandContext { const { ctx, cfg, agentId, sessionKey, isGroup, triggerBodyNormalized, commandAuthorized } = params; - const surface = (ctx.Surface ?? ctx.Provider ?? "").trim().toLowerCase(); - const channel = (ctx.Provider ?? surface).trim().toLowerCase(); - const from = ctx.From?.trim() || undefined; - const to = ctx.To?.trim() || undefined; + const surface = normalizeOptionalString(ctx.Surface ?? ctx.Provider)?.toLowerCase() ?? ""; + const channel = normalizeOptionalString(ctx.Provider ?? surface)?.toLowerCase() ?? ""; + const from = normalizeOptionalString(ctx.From); + const to = normalizeOptionalString(ctx.To); return { surface, channel, @@ -212,9 +215,15 @@ export function initFastReplySessionState(params: { sessionFile, updatedAt: now, ...(normalizedChatType ? { chatType: normalizedChatType } : {}), - ...(ctx.Provider?.trim() ? { channel: ctx.Provider.trim() } : {}), - ...(ctx.GroupSubject?.trim() ? { subject: ctx.GroupSubject.trim() } : {}), - ...(ctx.GroupChannel?.trim() ? { groupChannel: ctx.GroupChannel.trim() } : {}), + ...(normalizeOptionalString(ctx.Provider) + ? { channel: normalizeOptionalString(ctx.Provider) } + : {}), + ...(normalizeOptionalString(ctx.GroupSubject) + ? { subject: normalizeOptionalString(ctx.GroupSubject) } + : {}), + ...(normalizeOptionalString(ctx.GroupChannel) + ? { groupChannel: normalizeOptionalString(ctx.GroupChannel) } + : {}), }; const sessionStore: Record = { [sessionKey]: sessionEntry }; const sessionCtx: TemplateContext = { @@ -235,7 +244,7 @@ export function initFastReplySessionState(params: { resetTriggered, systemSent: false, abortedLastRun: false, - storePath: cfg.session?.store?.trim() ?? "", + storePath: normalizeOptionalString(cfg.session?.store) ?? "", sessionScope, groupResolution: undefined, isGroup, diff --git a/src/auto-reply/reply/get-reply-run.ts b/src/auto-reply/reply/get-reply-run.ts index 2e51453a3f5..cc0c82a29b4 100644 --- a/src/auto-reply/reply/get-reply-run.ts +++ b/src/auto-reply/reply/get-reply-run.ts @@ -13,6 +13,7 @@ import type { SessionEntry } from "../../config/sessions/types.js"; import { logVerbose } from "../../globals.js"; import { clearCommandLane, getQueueSize } from "../../process/command-queue.js"; import { normalizeMainKey } from "../../routing/session-key.js"; +import { normalizeOptionalString } from "../../shared/string-coerce.js"; import { isReasoningTagProvider } from "../../utils/provider-utils.js"; import { hasControlCommand } from "../command-detection.js"; import { resolveEnvelopeFormatOptions } from "../envelope.js"; @@ -220,7 +221,7 @@ export async function runPreparedReply( silentToken: SILENT_REPLY_TOKEN, }) : ""; - const groupSystemPrompt = sessionCtx.GroupSystemPrompt?.trim() ?? ""; + const groupSystemPrompt = normalizeOptionalString(sessionCtx.GroupSystemPrompt) ?? ""; const inboundMetaPrompt = buildInboundMetaSystemPrompt( isNewSession ? sessionCtx : { ...sessionCtx, ThreadStarterBody: undefined }, { includeFormattingHints: !useFastReplyRuntime }, @@ -256,7 +257,7 @@ export async function runPreparedReply( isNewSession ? { ...sessionCtx, - ...(sessionCtx.ThreadHistoryBody?.trim() + ...(normalizeOptionalString(sessionCtx.ThreadHistoryBody) ? { InboundHistory: undefined, ThreadStarterBody: undefined } : {}), } @@ -306,8 +307,8 @@ export async function runPreparedReply( } } const prefixedBodyCore = prefixedBodyBase; - const threadStarterBody = ctx.ThreadStarterBody?.trim(); - const threadHistoryBody = ctx.ThreadHistoryBody?.trim(); + const threadStarterBody = normalizeOptionalString(ctx.ThreadStarterBody); + const threadHistoryBody = normalizeOptionalString(ctx.ThreadHistoryBody); const threadContextNote = threadHistoryBody ? `[Thread history - for context]\n${threadHistoryBody}` : threadStarterBody @@ -542,12 +543,14 @@ export async function runPreparedReply( }), agentAccountId: sessionCtx.AccountId, groupId: resolveGroupSessionKey(sessionCtx)?.id ?? undefined, - groupChannel: sessionCtx.GroupChannel?.trim() ?? sessionCtx.GroupSubject?.trim(), - groupSpace: sessionCtx.GroupSpace?.trim() ?? undefined, - senderId: sessionCtx.SenderId?.trim() || undefined, - senderName: sessionCtx.SenderName?.trim() || undefined, - senderUsername: sessionCtx.SenderUsername?.trim() || undefined, - senderE164: sessionCtx.SenderE164?.trim() || undefined, + groupChannel: + normalizeOptionalString(sessionCtx.GroupChannel) ?? + normalizeOptionalString(sessionCtx.GroupSubject), + groupSpace: normalizeOptionalString(sessionCtx.GroupSpace), + senderId: normalizeOptionalString(sessionCtx.SenderId), + senderName: normalizeOptionalString(sessionCtx.SenderName), + senderUsername: normalizeOptionalString(sessionCtx.SenderUsername), + senderE164: normalizeOptionalString(sessionCtx.SenderE164), senderIsOwner: command.senderIsOwner, sessionFile: preparedSessionState.sessionFile, workspaceDir, diff --git a/src/auto-reply/reply/queue/state.ts b/src/auto-reply/reply/queue/state.ts index 954251ac59e..1c2e04d5673 100644 --- a/src/auto-reply/reply/queue/state.ts +++ b/src/auto-reply/reply/queue/state.ts @@ -1,4 +1,5 @@ import { resolveGlobalMap } from "../../../shared/global-singleton.js"; +import { normalizeOptionalString } from "../../../shared/string-coerce.js"; import { applyQueueRuntimeSettings } from "../../../utils/queue-helpers.js"; import type { FollowupRun, QueueDropPolicy, QueueMode, QueueSettings } from "./types.js"; @@ -123,8 +124,9 @@ export function refreshQueuedFollowupSession(params: { } if (shouldRewriteSession && run.sessionId === params.previousSessionId) { run.sessionId = params.nextSessionId!; - if (params.nextSessionFile?.trim()) { - run.sessionFile = params.nextSessionFile; + const nextSessionFile = normalizeOptionalString(params.nextSessionFile); + if (nextSessionFile) { + run.sessionFile = nextSessionFile; } } if (shouldRewriteSelection) { @@ -135,7 +137,7 @@ export function refreshQueuedFollowupSession(params: { run.model = params.nextModel; } if (Object.hasOwn(params, "nextAuthProfileId")) { - run.authProfileId = params.nextAuthProfileId?.trim() || undefined; + run.authProfileId = normalizeOptionalString(params.nextAuthProfileId); } if (Object.hasOwn(params, "nextAuthProfileIdSource")) { run.authProfileIdSource = run.authProfileId ? params.nextAuthProfileIdSource : undefined; diff --git a/src/auto-reply/reply/reply-payloads-base.ts b/src/auto-reply/reply/reply-payloads-base.ts index 68e178d91db..5f55cd9cb64 100644 --- a/src/auto-reply/reply/reply-payloads-base.ts +++ b/src/auto-reply/reply/reply-payloads-base.ts @@ -1,5 +1,6 @@ import type { ReplyToMode } from "../../config/types.js"; import { hasReplyPayloadContent } from "../../interactive/payload.js"; +import { normalizeOptionalString } from "../../shared/string-coerce.js"; import type { OriginatingChannelType } from "../templating.js"; import type { ReplyPayload, ReplyThreadingPolicy } from "../types.js"; import { extractReplyToTag } from "./reply-tags.js"; @@ -9,11 +10,11 @@ import { } from "./reply-threading.js"; export function formatBtwTextForExternalDelivery(payload: ReplyPayload): string | undefined { - const text = payload.text?.trim(); + const text = normalizeOptionalString(payload.text); if (!text) { return payload.text; } - const question = payload.btw?.question?.trim(); + const question = normalizeOptionalString(payload.btw?.question); if (!question) { return payload.text; } @@ -28,8 +29,8 @@ function resolveReplyThreadingForPayload(params: { currentMessageId?: string; replyThreading?: ReplyThreadingPolicy; }): ReplyPayload { - const implicitReplyToId = params.implicitReplyToId?.trim() || undefined; - const currentMessageId = params.currentMessageId?.trim() || undefined; + const implicitReplyToId = normalizeOptionalString(params.implicitReplyToId); + const currentMessageId = normalizeOptionalString(params.currentMessageId); const allowImplicitReplyToCurrentMessage = resolveImplicitCurrentMessageReplyAllowance( params.replyToMode, params.replyThreading, @@ -91,7 +92,7 @@ export function applyReplyThreading(params: { }): ReplyPayload[] { const { payloads, replyToMode, replyToChannel, currentMessageId, replyThreading } = params; const applyReplyToMode = createReplyToModeFilterForChannel(replyToMode, replyToChannel); - const implicitReplyToId = currentMessageId?.trim() || undefined; + const implicitReplyToId = normalizeOptionalString(currentMessageId); return payloads .map((payload) => resolveReplyThreadingForPayload({