refactor: dedupe reply control readers

This commit is contained in:
Peter Steinberger
2026-04-07 06:10:40 +01:00
parent 059197e496
commit ea7297b344
7 changed files with 32 additions and 20 deletions

View File

@@ -6,6 +6,7 @@ import { isCommandFlagEnabled } from "../../config/commands.js";
import type { OpenClawConfig } from "../../config/config.js";
import { logVerbose } from "../../globals.js";
import { formatErrorMessage } from "../../infra/errors.js";
import { normalizeOptionalString } from "../../shared/string-coerce.js";
import { clampInt } from "../../utils.js";
import type { MsgContext } from "../templating.js";
import type { ReplyPayload } from "../types.js";
@@ -82,8 +83,8 @@ function parseBashRequest(raw: string): BashRequest | null {
return { action: "help" };
}
const tokenMatch = rest.match(/^(\S+)(?:\s+([\s\S]+))?$/);
const token = tokenMatch?.[1]?.trim() ?? "";
const remainder = tokenMatch?.[2]?.trim() ?? "";
const token = normalizeOptionalString(tokenMatch?.[1]) ?? "";
const remainder = normalizeOptionalString(tokenMatch?.[2]) ?? "";
const lowered = token.toLowerCase();
if (lowered === "poll") {
return { action: "poll", sessionId: remainder || undefined };
@@ -236,7 +237,8 @@ export async function handleBashChatCommand(params: {
if (request.action === "poll") {
const sessionId =
request.sessionId?.trim() || (liveJob?.state === "running" ? liveJob.sessionId : "");
normalizeOptionalString(request.sessionId) ||
(liveJob?.state === "running" ? liveJob.sessionId : "");
if (!sessionId) {
return { text: "⚙️ No active bash job." };
}
@@ -279,7 +281,8 @@ export async function handleBashChatCommand(params: {
if (request.action === "stop") {
const sessionId =
request.sessionId?.trim() || (liveJob?.state === "running" ? liveJob.sessionId : "");
normalizeOptionalString(request.sessionId) ||
(liveJob?.state === "running" ? liveJob.sessionId : "");
if (!sessionId) {
return { text: "⚙️ No active bash job." };
}

View File

@@ -85,10 +85,10 @@ function hasInboundMediaForAcp(ctx: FinalizedMsgContext): boolean {
return Boolean(
ctx.StickerMediaIncluded ||
ctx.Sticker ||
ctx.MediaPath?.trim() ||
ctx.MediaUrl?.trim() ||
ctx.MediaPaths?.some((value) => value?.trim()) ||
ctx.MediaUrls?.some((value) => value?.trim()) ||
normalizeOptionalString(ctx.MediaPath) ||
normalizeOptionalString(ctx.MediaUrl) ||
ctx.MediaPaths?.some((value) => normalizeOptionalString(value)) ||
ctx.MediaUrls?.some((value) => normalizeOptionalString(value)) ||
ctx.MediaTypes?.length,
);
}

View File

@@ -1,4 +1,5 @@
import { CHAT_CHANNEL_ORDER } from "../../channels/registry.js";
import { normalizeOptionalString } from "../../shared/string-coerce.js";
import { normalizeAtHashSlug } from "../../shared/string-normalization.js";
export type ExplicitElevatedAllowField = "id" | "from" | "e164" | "name" | "username" | "tag";
@@ -111,7 +112,7 @@ export function matchesFormattedTokens(params: {
export function buildMutableTokens(value?: string): Set<string> {
const tokens = new Set<string>();
const trimmed = value?.trim();
const trimmed = normalizeOptionalString(value);
if (!trimmed) {
return tokens;
}

View File

@@ -8,6 +8,7 @@ import type { SkillCommandSpec } from "../../agents/skills.js";
import type { OpenClawConfig } from "../../config/config.js";
import type { SessionEntry } from "../../config/sessions.js";
import { normalizeAgentId } from "../../routing/session-key.js";
import { normalizeOptionalString } from "../../shared/string-coerce.js";
import { shouldHandleTextCommands } from "../commands-text-routing.js";
import type { MsgContext, TemplateContext } from "../templating.js";
import type { ElevatedLevel, ReasoningLevel, ThinkLevel, VerboseLevel } from "../thinking.js";
@@ -80,7 +81,7 @@ function resolveConfiguredDirectiveAliases(params: {
return [];
}
return Object.values(params.cfg.agents?.defaults?.models ?? {})
.map((entry) => entry.alias?.trim())
.map((entry) => normalizeOptionalString(entry.alias))
.filter((alias): alias is string => Boolean(alias))
.filter((alias) => !params.reservedCommands.has(alias.toLowerCase()));
}
@@ -379,7 +380,9 @@ export async function resolveReplyDirectives(params: {
sessionCtx.BodyStripped = cleanedBody;
const messageProviderKey =
sessionCtx.Provider?.trim().toLowerCase() ?? ctx.Provider?.trim().toLowerCase() ?? "";
normalizeOptionalString(sessionCtx.Provider)?.toLowerCase() ??
normalizeOptionalString(ctx.Provider)?.toLowerCase() ??
"";
const elevated = resolveElevatedPermissions({
cfg,
agentId,
@@ -464,8 +467,8 @@ export async function resolveReplyDirectives(params: {
useFastReplyRuntime &&
!directives.hasModelDirective &&
!hasResolvedHeartbeatModelOverride &&
!sessionEntry?.modelOverride?.trim() &&
!sessionEntry?.providerOverride?.trim()
!normalizeOptionalString(sessionEntry?.modelOverride) &&
!normalizeOptionalString(sessionEntry?.providerOverride)
? createFastTestModelSelectionState({
agentCfg,
provider,
@@ -522,7 +525,9 @@ export async function resolveReplyDirectives(params: {
alias ? `Model switched to ${alias} (${label}).` : `Model switched to ${label}.`;
const isModelListAlias =
directives.hasModelDirective &&
["status", "list"].includes(directives.rawModelDirective?.trim().toLowerCase() ?? "");
["status", "list"].includes(
normalizeOptionalString(directives.rawModelDirective)?.toLowerCase() ?? "",
);
const effectiveModelDirective = isModelListAlias ? undefined : directives.rawModelDirective;
const inlineStatusRequested = hasInlineStatus && allowTextCommands && command.isAuthorizedSender;

View File

@@ -12,6 +12,7 @@ import { resolvePreferredOpenClawTmpDir } from "../../infra/tmp-openclaw-dir.js"
import { resolveChannelRemoteInboundAttachmentRoots } from "../../media/channel-inbound-roots.js";
import { isInboundPathAllowed } from "../../media/inbound-path-policy.js";
import { getMediaDir, MEDIA_MAX_BYTES } from "../../media/store.js";
import { normalizeOptionalString } from "../../shared/string-coerce.js";
import { CONFIG_DIR } from "../../utils.js";
import type { MsgContext, TemplateContext } from "../templating.js";
@@ -156,8 +157,8 @@ function resolveRawPaths(ctx: MsgContext): string[] {
const pathsFromArray = Array.isArray(ctx.MediaPaths) ? ctx.MediaPaths : undefined;
return pathsFromArray && pathsFromArray.length > 0
? pathsFromArray
: ctx.MediaPath?.trim()
? [ctx.MediaPath.trim()]
: normalizeOptionalString(ctx.MediaPath)
? [normalizeOptionalString(ctx.MediaPath)!]
: [];
}
@@ -243,7 +244,7 @@ function rewriteStagedMediaPaths(params: {
hasPathsArray: boolean;
}): void {
const rewriteIfStaged = (value: string | undefined): string | undefined => {
const raw = value?.trim();
const raw = normalizeOptionalString(value);
if (!raw) {
return value;
}

View File

@@ -1,4 +1,5 @@
import type { TypingMode } from "../../config/types.js";
import { normalizeOptionalString } from "../../shared/string-coerce.js";
import { isSilentReplyText, SILENT_REPLY_TOKEN } from "../tokens.js";
import type { TypingPolicy } from "../types.js";
import type { TypingController } from "./typing.js";
@@ -67,7 +68,7 @@ export function createTypingSignaler(params: {
let hasRenderableText = false;
const isRenderableText = (text?: string): boolean => {
const trimmed = text?.trim();
const trimmed = normalizeOptionalString(text);
if (!trimmed) {
return false;
}
@@ -98,7 +99,7 @@ export function createTypingSignaler(params: {
const renderable = isRenderableText(text);
if (renderable) {
hasRenderableText = true;
} else if (text?.trim()) {
} else if (normalizeOptionalString(text)) {
return;
} else {
return;

View File

@@ -1,5 +1,6 @@
import { createTypingKeepaliveLoop } from "../../channels/typing-lifecycle.js";
import { createTypingStartGuard } from "../../channels/typing-start-guard.js";
import { normalizeOptionalString } from "../../shared/string-coerce.js";
import { isSilentReplyPrefixText, isSilentReplyText, SILENT_REPLY_TOKEN } from "../tokens.js";
export type TypingController = {
@@ -181,7 +182,7 @@ export function createTypingController(params: {
if (sealed) {
return;
}
const trimmed = text?.trim();
const trimmed = normalizeOptionalString(text);
if (!trimmed) {
return;
}