refactor(core): adopt normalization-core leaf helpers across production (#120350)

* refactor(core): adopt normalization-core leaf helpers across production

* fix(ci): keep plugin contract source-resolvable

* fix(errors): preserve adapter-owned error fields

* fix(errors): short-circuit existing errors before stringifying

* fix(errors): skip throwing structured getters

* ci: retrigger checks on current base

* fix(errors): guard structured error enumeration

* fix: harden error detail copying
This commit is contained in:
Peter Steinberger
2026-08-08 12:00:49 -07:00
committed by GitHub
parent e81d7e62e8
commit e7a9f33d89
165 changed files with 748 additions and 848 deletions
@@ -1,16 +1,10 @@
// Memory Host SDK module implements embeddings remote fetch behavior. // Memory Host SDK module implements embeddings remote fetch behavior.
import { asOptionalRecord } from "@openclaw/normalization-core/record-coerce";
import { postJson } from "./post-json.js"; import { postJson } from "./post-json.js";
import type { SsrFPolicy } from "./ssrf-policy.js"; import type { SsrFPolicy } from "./ssrf-policy.js";
// Fetches and validates OpenAI-compatible embedding responses. // Fetches and validates OpenAI-compatible embedding responses.
/** Narrow unknown JSON payloads to plain objects. */
function asRecord(value: unknown): Record<string, unknown> | undefined {
return typeof value === "object" && value !== null && !Array.isArray(value)
? (value as Record<string, unknown>)
: undefined;
}
/** Build the common malformed embedding response error. */ /** Build the common malformed embedding response error. */
function malformedEmbeddingResponse(errorPrefix: string): Error { function malformedEmbeddingResponse(errorPrefix: string): Error {
return new Error(`${errorPrefix}: malformed JSON response`); return new Error(`${errorPrefix}: malformed JSON response`);
@@ -31,7 +25,7 @@ function readEmbeddingVector(value: unknown, errorPrefix: string): number[] {
/** Resolve expected response count from the request body when input is an array. */ /** Resolve expected response count from the request body when input is an array. */
function resolveExpectedEmbeddingCount(body: unknown): number | undefined { function resolveExpectedEmbeddingCount(body: unknown): number | undefined {
const input = asRecord(body)?.input; const input = asOptionalRecord(body)?.input;
return Array.isArray(input) ? input.length : undefined; return Array.isArray(input) ? input.length : undefined;
} }
@@ -54,7 +48,7 @@ export async function fetchRemoteEmbeddingVectors(params: {
body: params.body, body: params.body,
errorPrefix: params.errorPrefix, errorPrefix: params.errorPrefix,
parse: (payload) => { parse: (payload) => {
const root = asRecord(payload); const root = asOptionalRecord(payload);
if (!root || !Array.isArray(root.data)) { if (!root || !Array.isArray(root.data)) {
throw malformedEmbeddingResponse(params.errorPrefix); throw malformedEmbeddingResponse(params.errorPrefix);
} }
@@ -63,7 +57,7 @@ export async function fetchRemoteEmbeddingVectors(params: {
throw malformedEmbeddingResponse(params.errorPrefix); throw malformedEmbeddingResponse(params.errorPrefix);
} }
return root.data.map((entry) => { return root.data.map((entry) => {
const record = asRecord(entry); const record = asOptionalRecord(entry);
if (!record) { if (!record) {
throw malformedEmbeddingResponse(params.errorPrefix); throw malformedEmbeddingResponse(params.errorPrefix);
} }
@@ -1,4 +1,5 @@
// Memory Host SDK module implements qmd query parser behavior. // Memory Host SDK module implements qmd query parser behavior.
import { asPositiveSafeInteger as parseQmdLineNumber } from "@openclaw/normalization-core/number-coercion";
import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce"; import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce";
import { truncateUtf16Safe } from "@openclaw/normalization-core/utf16-slice"; import { truncateUtf16Safe } from "@openclaw/normalization-core/utf16-slice";
import { formatErrorMessage } from "./error-utils.js"; import { formatErrorMessage } from "./error-utils.js";
@@ -124,11 +125,6 @@ function parseQmdQueryResultArray(raw: string): QmdQueryResult[] | null {
} }
} }
/** Normalize qmd line numbers, rejecting zero, negative, and non-integer values. */
function parseQmdLineNumber(value: unknown): number | undefined {
return typeof value === "number" && Number.isSafeInteger(value) && value > 0 ? value : undefined;
}
/** Extract the first complete, standalone JSON result array from noisy stdout. */ /** Extract the first complete, standalone JSON result array from noisy stdout. */
function extractFirstJsonArray(raw: string): string | null { function extractFirstJsonArray(raw: string): string | null {
let start = -1; let start = -1;
@@ -1,6 +1,9 @@
// Secret input parsing shared by memory provider config and gateway-resolved snapshots. // Secret input parsing shared by memory provider config and gateway-resolved snapshots.
import { isRecord } from "@openclaw/normalization-core/record-coerce"; import { isRecord } from "@openclaw/normalization-core/record-coerce";
import { hasNonEmptyString } from "@openclaw/normalization-core/string-coerce"; import {
hasNonEmptyString,
normalizeOptionalString,
} from "@openclaw/normalization-core/string-coerce";
/** Supported secret reference backing stores. */ /** Supported secret reference backing stores. */
type SecretRefSource = "env" | "file" | "exec"; type SecretRefSource = "env" | "file" | "exec";
@@ -18,15 +21,6 @@ const LEGACY_SECRETREF_ENV_MARKER_PREFIX = "secretref-env:";
const ENV_SECRET_TEMPLATE_RE = /^\$\{([A-Z][A-Z0-9_]{0,127})\}$/; const ENV_SECRET_TEMPLATE_RE = /^\$\{([A-Z][A-Z0-9_]{0,127})\}$/;
const SECRET_REF_SOURCES = new Set<SecretRefSource>(["env", "file", "exec"]); const SECRET_REF_SOURCES = new Set<SecretRefSource>(["env", "file", "exec"]);
/** Normalize literal secret strings and reject empty placeholders. */
function normalizeSecretInputString(value: unknown): string | undefined {
if (typeof value !== "string") {
return undefined;
}
const trimmed = value.trim();
return trimmed.length > 0 ? trimmed : undefined;
}
/** Narrow a string to a supported SecretRef source. */ /** Narrow a string to a supported SecretRef source. */
function hasSecretRefSource(value: unknown): value is SecretRefSource { function hasSecretRefSource(value: unknown): value is SecretRefSource {
return typeof value === "string" && SECRET_REF_SOURCES.has(value as SecretRefSource); return typeof value === "string" && SECRET_REF_SOURCES.has(value as SecretRefSource);
@@ -111,7 +105,7 @@ function coerceSecretRef(value: unknown): SecretRef | null {
/** Return true when a secret input has either a literal value or resolvable reference shape. */ /** Return true when a secret input has either a literal value or resolvable reference shape. */
export function hasConfiguredMemorySecretInputValue(value: unknown): boolean { export function hasConfiguredMemorySecretInputValue(value: unknown): boolean {
if (normalizeSecretInputString(value)) { if (normalizeOptionalString(value)) {
return true; return true;
} }
return coerceSecretRef(value) !== null; return coerceSecretRef(value) !== null;
@@ -139,7 +133,7 @@ export function normalizeResolvedMemorySecretInputString(params: {
value: unknown; value: unknown;
path: string; path: string;
}): string | undefined { }): string | undefined {
const normalized = normalizeSecretInputString(params.value); const normalized = normalizeOptionalString(params.value);
if (normalized) { if (normalized) {
return normalized; return normalized;
} }
@@ -152,5 +146,5 @@ export function normalizeResolvedMemorySecretInputString(params: {
/** Normalize env-provided secret values before use. */ /** Normalize env-provided secret values before use. */
export function normalizeEnvSecretInputString(value: unknown): string | undefined { export function normalizeEnvSecretInputString(value: unknown): string | undefined {
return normalizeSecretInputString(value); return normalizeOptionalString(value);
} }
@@ -1,4 +1,5 @@
// Model Catalog Core helper module supports model catalog normalize behavior. // Model Catalog Core helper module supports model catalog normalize behavior.
import { asFiniteNumber as normalizeFiniteNumber } from "@openclaw/normalization-core/number-coercion";
import { isRecord } from "@openclaw/normalization-core/record-coerce"; import { isRecord } from "@openclaw/normalization-core/record-coerce";
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce"; import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
import { import {
@@ -127,10 +128,6 @@ function normalizeNonNegativeNumber(value: unknown): number | undefined {
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : undefined; return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : undefined;
} }
function normalizeFiniteNumber(value: unknown): number | undefined {
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
}
function normalizeStringOrNumber(value: unknown): string | number | undefined { function normalizeStringOrNumber(value: unknown): string | number | undefined {
return normalizeOptionalString(value) ?? normalizeFiniteNumber(value); return normalizeOptionalString(value) ?? normalizeFiniteNumber(value);
} }
+1
View File
@@ -44,6 +44,7 @@
"build": "tsdown src/index.ts src/ip.ts src/ipv4.ts src/redact-sensitive-url.ts src/url-protocol.ts src/url-userinfo.ts --no-config --platform node --format esm --dts --out-dir dist --clean" "build": "tsdown src/index.ts src/ip.ts src/ipv4.ts src/redact-sensitive-url.ts src/url-protocol.ts src/url-userinfo.ts --no-config --platform node --format esm --dts --out-dir dist --clean"
}, },
"dependencies": { "dependencies": {
"@openclaw/normalization-core": "workspace:*",
"ipaddr.js": "2.4.0" "ipaddr.js": "2.4.0"
} }
} }
+4 -12
View File
@@ -1,18 +1,10 @@
// Network Policy module implements ip behavior. // Network Policy module implements ip behavior.
import {
normalizeLowercaseStringOrEmpty,
normalizeOptionalString,
} from "@openclaw/normalization-core/string-coerce";
import ipaddr from "ipaddr.js"; import ipaddr from "ipaddr.js";
function normalizeOptionalString(value: unknown): string | undefined {
if (typeof value !== "string") {
return undefined;
}
const trimmed = value.trim();
return trimmed || undefined;
}
function normalizeLowercaseStringOrEmpty(value: unknown): string {
return typeof value === "string" ? value.trim().toLowerCase() : "";
}
/** Parsed IP address value returned by the net-policy parsing helpers. */ /** Parsed IP address value returned by the net-policy parsing helpers. */
export type ParsedIpAddress = ipaddr.IPv4 | ipaddr.IPv6; export type ParsedIpAddress = ipaddr.IPv4 | ipaddr.IPv6;
type Ipv4Range = ReturnType<ipaddr.IPv4["range"]>; type Ipv4Range = ReturnType<ipaddr.IPv4["range"]>;
@@ -1,12 +1,10 @@
// Network Policy module implements redact sensitive url behavior. // Network Policy module implements redact sensitive url behavior.
import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce";
type ConfigUiHintTags = { type ConfigUiHintTags = {
tags?: string[]; tags?: string[];
}; };
function normalizeLowercaseStringOrEmpty(value: unknown): string {
return typeof value === "string" ? value.trim().toLowerCase() : "";
}
/** Config UI hint tag for URL-like values that may embed credentials or tokens. */ /** Config UI hint tag for URL-like values that may embed credentials or tokens. */
export const SENSITIVE_URL_HINT_TAG = "url-secret"; export const SENSITIVE_URL_HINT_TAG = "url-secret";
@@ -4,6 +4,7 @@ import {
avoidTrailingHighSurrogateBreak, avoidTrailingHighSurrogateBreak,
sliceUtf16Safe, sliceUtf16Safe,
truncateUtf16Safe, truncateUtf16Safe,
truncateWithMarker,
} from "./utf16-slice.js"; } from "./utf16-slice.js";
describe("avoidTrailingHighSurrogateBreak", () => { describe("avoidTrailingHighSurrogateBreak", () => {
@@ -105,3 +106,52 @@ describe("truncateUtf16Safe", () => {
expect(truncateUtf16Safe(input, 1)).toBe(""); expect(truncateUtf16Safe(input, 1)).toBe("");
}); });
}); });
describe("truncateWithMarker", () => {
it.each([
{
name: "returns values at the boundary unchanged",
value: "hello",
max: 5,
options: { marker: "...", reserve: 3, trimEnd: false },
expected: "hello",
},
{
name: "reserves marker width",
value: "hello world",
max: 8,
options: { marker: "...", reserve: 3, trimEnd: false },
expected: "hello...",
},
{
name: "supports markers outside the limit",
value: "hello world",
max: 5,
options: { marker: "...", reserve: 0, trimEnd: false },
expected: "hello...",
},
{
name: "trims only the truncated prefix",
value: "hello world",
max: 9,
options: { marker: "...", reserve: 3, trimEnd: true },
expected: "hello...",
},
{
name: "keeps surrogate pairs well formed",
value: "ab🚀tail",
max: 4,
options: { marker: "…", reserve: 1, trimEnd: false },
expected: "ab…",
},
{
name: "preserves marker output at zero limits",
value: "hello",
max: 0,
options: { marker: "…", reserve: 1, trimEnd: false },
expected: "…",
},
] as const)("$name", ({ value, max, options, expected }) => {
expect(truncateWithMarker(value, max, options)).toBe(expected);
});
});
@@ -62,3 +62,16 @@ export function truncateUtf16Safe(input: string, maxLen: number): string {
} }
return sliceUtf16Safe(input, 0, limit); return sliceUtf16Safe(input, 0, limit);
} }
/** Truncates text and appends a marker while preserving the caller's reserved width contract. */
export function truncateWithMarker(
value: string,
max: number,
options: { marker: string; reserve: number; trimEnd: boolean },
): string {
if (value.length <= max) {
return value;
}
const prefix = truncateUtf16Safe(value, max - options.reserve);
return `${options.trimEnd ? prefix.trimEnd() : prefix}${options.marker}`;
}
+2 -14
View File
@@ -1,4 +1,6 @@
// External code plugin package.json compatibility and validation contracts. // External code plugin package.json compatibility and validation contracts.
import { isRecord } from "../../normalization-core/src/record-coerce.js";
import { normalizeOptionalString } from "../../normalization-core/src/string-coerce.js";
/** JSON object shape accepted by package contract helpers. */ /** JSON object shape accepted by package contract helpers. */
export type JsonObject = Record<string, unknown>; export type JsonObject = Record<string, unknown>;
@@ -29,20 +31,6 @@ export const EXTERNAL_CODE_PLUGIN_REQUIRED_FIELD_PATHS = [
"openclaw.build.openclawVersion", "openclaw.build.openclawVersion",
] as const; ] as const;
/** Narrow unknown values to plain records. */
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
/** Normalize optional package metadata strings. */
function normalizeOptionalString(value: unknown): string | undefined {
if (typeof value !== "string") {
return undefined;
}
const trimmed = value.trim();
return trimmed ? trimmed : undefined;
}
/** Read OpenClaw package.json blocks without trusting caller input shape. */ /** Read OpenClaw package.json blocks without trusting caller input shape. */
function readOpenClawBlock(packageJson: unknown) { function readOpenClawBlock(packageJson: unknown) {
const root = isRecord(packageJson) ? packageJson : undefined; const root = isRecord(packageJson) ? packageJson : undefined;
+2 -1
View File
@@ -19,6 +19,7 @@
"build": "tsdown src/index.ts --no-config --platform node --format esm --dts --out-dir dist --clean" "build": "tsdown src/index.ts --no-config --platform node --format esm --dts --out-dir dist --clean"
}, },
"dependencies": { "dependencies": {
"@openclaw/gateway-client": "workspace:*" "@openclaw/gateway-client": "workspace:*",
"@openclaw/normalization-core": "workspace:*"
} }
} }
+1 -4
View File
@@ -1,5 +1,6 @@
// OpenClaw SDK module implements client behavior. // OpenClaw SDK module implements client behavior.
import { randomUUID } from "node:crypto"; import { randomUUID } from "node:crypto";
import { asRecord } from "@openclaw/normalization-core/record-coerce";
import { EventHub } from "./event-hub.js"; import { EventHub } from "./event-hub.js";
import { normalizeGatewayEvent } from "./normalize.js"; import { normalizeGatewayEvent } from "./normalize.js";
import { GatewayClientTransport, isConnectableTransport } from "./transport.js"; import { GatewayClientTransport, isConnectableTransport } from "./transport.js";
@@ -222,10 +223,6 @@ type ChatProjection = {
payload: Record<string, unknown>; payload: Record<string, unknown>;
}; };
function asRecord(value: unknown): Record<string, unknown> {
return typeof value === "object" && value !== null ? (value as Record<string, unknown>) : {};
}
function hasArtifactQueryScope(params: unknown): params is ArtifactQuery { function hasArtifactQueryScope(params: unknown): params is ArtifactQuery {
const record = asRecord(params); const record = asRecord(params);
return [record.sessionKey, record.runId, record.taskId].some( return [record.sessionKey, record.runId, record.taskId].some(
+2 -9
View File
@@ -1,19 +1,12 @@
// OpenClaw SDK helper module supports normalize behavior. // OpenClaw SDK helper module supports normalize behavior.
import { asFiniteNumber as readNumber } from "@openclaw/normalization-core/number-coercion";
import { asRecord } from "@openclaw/normalization-core/record-coerce";
import type { GatewayEvent, JsonObject, OpenClawEvent, OpenClawEventType } from "./types.js"; import type { GatewayEvent, JsonObject, OpenClawEvent, OpenClawEventType } from "./types.js";
// Normalize raw Gateway events into stable SDK event types and common metadata.
function asRecord(value: unknown): JsonObject {
return typeof value === "object" && value !== null ? (value as JsonObject) : {};
}
function readString(value: unknown): string | undefined { function readString(value: unknown): string | undefined {
return typeof value === "string" && value.length > 0 ? value : undefined; return typeof value === "string" && value.length > 0 ? value : undefined;
} }
function readNumber(value: unknown): number | undefined {
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
}
function readLowerString(value: unknown): string | undefined { function readLowerString(value: unknown): string | undefined {
return readString(value)?.toLowerCase(); return readString(value)?.toLowerCase();
} }
+1 -4
View File
@@ -1,3 +1,4 @@
import { asOptionalObjectRecord as asRecord } from "@openclaw/normalization-core/record-coerce";
import type { PlainTextToolCallProtectedRangeResolver } from "./contracts.js"; import type { PlainTextToolCallProtectedRangeResolver } from "./contracts.js";
// Tool Call Repair module implements promote behavior. // Tool Call Repair module implements promote behavior.
import { parseStandalonePlainTextToolCallBlocks, type PlainTextToolCallBlock } from "./payload.js"; import { parseStandalonePlainTextToolCallBlocks, type PlainTextToolCallBlock } from "./payload.js";
@@ -45,10 +46,6 @@ export function createPromotedPlainTextToolCallBlock(
}; };
} }
function asRecord(value: unknown): Record<string, unknown> | undefined {
return value && typeof value === "object" ? (value as Record<string, unknown>) : undefined;
}
/** Emits the complete provider-neutral lifecycle for promoted tool-call blocks. */ /** Emits the complete provider-neutral lifecycle for promoted tool-call blocks. */
export function createPromotedPlainTextToolCallEvents( export function createPromotedPlainTextToolCallEvents(
message: Record<string, unknown>, message: Record<string, unknown>,
+6
View File
@@ -2306,6 +2306,9 @@ importers:
packages/net-policy: packages/net-policy:
dependencies: dependencies:
'@openclaw/normalization-core':
specifier: workspace:*
version: link:../normalization-core
ipaddr.js: ipaddr.js:
specifier: 2.4.0 specifier: 2.4.0
version: 2.4.0 version: 2.4.0
@@ -2330,6 +2333,9 @@ importers:
'@openclaw/gateway-client': '@openclaw/gateway-client':
specifier: workspace:* specifier: workspace:*
version: link:../gateway-client version: link:../gateway-client
'@openclaw/normalization-core':
specifier: workspace:*
version: link:../normalization-core
packages/session-url-contract: {} packages/session-url-contract: {}
+1 -4
View File
@@ -10,6 +10,7 @@ import {
type AnyMessage, type AnyMessage,
} from "@agentclientprotocol/sdk"; } from "@agentclientprotocol/sdk";
import type { AcpServerOptions } from "@openclaw/acp-core/types"; import type { AcpServerOptions } from "@openclaw/acp-core/types";
import { isRecord as isJsonObject } from "@openclaw/normalization-core/record-coerce";
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce"; import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
import { import {
GATEWAY_CLIENT_CAPS, GATEWAY_CLIENT_CAPS,
@@ -289,10 +290,6 @@ function normalizeAcpInitializeProtocolVersion(message: AnyMessage): AnyMessage
} as AnyMessage; } as AnyMessage;
} }
function isJsonObject(value: unknown): value is JsonObject {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
function isUint16Integer(value: unknown): value is number { function isUint16Integer(value: unknown): value is number {
return typeof value === "number" && Number.isInteger(value) && value >= 0 && value <= 0xffff; return typeof value === "number" && Number.isInteger(value) && value >= 0 && value <= 0xffff;
} }
+1 -4
View File
@@ -35,6 +35,7 @@ import { defaultAcpSessionStore, type AcpSessionStore } from "@openclaw/acp-core
import { toAcpSessionLineageMeta } from "@openclaw/acp-core/session-lineage-meta"; import { toAcpSessionLineageMeta } from "@openclaw/acp-core/session-lineage-meta";
import type { AcpServerOptions } from "@openclaw/acp-core/types"; import type { AcpServerOptions } from "@openclaw/acp-core/types";
import { timestampMsToIsoString } from "@openclaw/normalization-core/number-coercion"; import { timestampMsToIsoString } from "@openclaw/normalization-core/number-coercion";
import { normalizeLowercaseStringOrEmpty as normalizedChatSendAckStatus } from "@openclaw/normalization-core/string-coerce";
import { import {
normalizeFastMode, normalizeFastMode,
normalizeOptionalString, normalizeOptionalString,
@@ -113,10 +114,6 @@ type ChatSendAck = {
status?: unknown; status?: unknown;
}; };
function normalizedChatSendAckStatus(status: unknown): string {
return typeof status === "string" ? status.trim().toLowerCase() : "";
}
function isTerminalChatSendAckFailure(status: unknown): boolean { function isTerminalChatSendAckFailure(status: unknown): boolean {
const normalized = normalizedChatSendAckStatus(status); const normalized = normalizedChatSendAckStatus(status);
return normalized === "timeout" || normalized === "error"; return normalized === "timeout" || normalized === "error";
+7 -8
View File
@@ -1,7 +1,12 @@
/** Relays child ACP session stream updates back into the requester parent session. */ /** Relays child ACP session stream updates back into the requester parent session. */
import { asFiniteNumber } from "@openclaw/normalization-core/number-coercion"; import { asFiniteNumber } from "@openclaw/normalization-core/number-coercion";
import { asOptionalRecord as asObjectRecord } from "@openclaw/normalization-core/record-coerce";
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce"; import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
import { sliceUtf16Safe, truncateUtf16Safe } from "@openclaw/normalization-core/utf16-slice"; import {
sliceUtf16Safe,
truncateUtf16Safe,
truncateWithMarker,
} from "@openclaw/normalization-core/utf16-slice";
import { import {
isAcpTagVisible, isAcpTagVisible,
resolveAcpProjectionSettings, resolveAcpProjectionSettings,
@@ -58,7 +63,7 @@ function truncate(value: string, maxChars: number): string {
if (maxChars <= 1) { if (maxChars <= 1) {
return truncateUtf16Safe(value, maxChars); return truncateUtf16Safe(value, maxChars);
} }
return `${truncateUtf16Safe(value, maxChars - 1)}`; return truncateWithMarker(value, maxChars, { marker: "…", reserve: 1, trimEnd: false });
} }
function normalizeStringArray(value: unknown): string[] { function normalizeStringArray(value: unknown): string[] {
@@ -75,12 +80,6 @@ function formatProxyEnvSummary(keys: string[]): string {
return `proxy env: ${keys.join(", ")}`; return `proxy env: ${keys.join(", ")}`;
} }
function asObjectRecord(value: unknown): Record<string, unknown> | undefined {
return value && typeof value === "object" && !Array.isArray(value)
? (value as Record<string, unknown>)
: undefined;
}
function mergeStreamingConfig(base: unknown, override: unknown): unknown { function mergeStreamingConfig(base: unknown, override: unknown): unknown {
const baseRecord = asObjectRecord(base); const baseRecord = asObjectRecord(base);
const overrideRecord = asObjectRecord(override); const overrideRecord = asObjectRecord(override);
+1 -4
View File
@@ -1,3 +1,4 @@
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
import type { ReplyPayload } from "../auto-reply/reply-payload.js"; import type { ReplyPayload } from "../auto-reply/reply-payload.js";
import type { RestartRecoveryTerminalDeliveryEvidenceResult } from "../config/sessions/restart-recovery-types.js"; import type { RestartRecoveryTerminalDeliveryEvidenceResult } from "../config/sessions/restart-recovery-types.js";
import type { SessionEntry } from "../config/sessions/types.js"; import type { SessionEntry } from "../config/sessions/types.js";
@@ -13,10 +14,6 @@ import {
} from "./embedded-agent-runner/delivery-evidence.js"; } from "./embedded-agent-runner/delivery-evidence.js";
import { mergeAttemptToolMediaPayloads } from "./embedded-agent-runner/run/tool-media-payloads.js"; import { mergeAttemptToolMediaPayloads } from "./embedded-agent-runner/run/tool-media-payloads.js";
function normalizeOptionalString(value: unknown): string | undefined {
return typeof value === "string" && value.trim() ? value.trim() : undefined;
}
function normalizeOptionalThreadId(value: unknown): string | undefined { function normalizeOptionalThreadId(value: unknown): string | undefined {
return ( return (
normalizeOptionalString(value) ?? normalizeOptionalString(value) ??
+1 -4
View File
@@ -1,4 +1,5 @@
/** Normalizes agent run wait/liveness/timeout metadata into sticky terminal outcomes. */ /** Normalizes agent run wait/liveness/timeout metadata into sticky terminal outcomes. */
import { asFiniteNumber as asFiniteTimestamp } from "@openclaw/normalization-core/number-coercion";
import { import {
formatAbandonedLivenessError, formatAbandonedLivenessError,
formatBlockedLivenessError, formatBlockedLivenessError,
@@ -461,10 +462,6 @@ export const AGENT_RUN_TERMINAL_RETRY_GRACE_MS = 15_000;
const HARD_TIMEOUT_PHASES = new Set<AgentRunTimeoutPhase>(["preflight", "provider", "post_turn"]); const HARD_TIMEOUT_PHASES = new Set<AgentRunTimeoutPhase>(["preflight", "provider", "post_turn"]);
function asFiniteTimestamp(value: unknown): number | undefined {
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
}
function asNonEmptyString(value: unknown): string | undefined { function asNonEmptyString(value: unknown): string | undefined {
return typeof value === "string" && value.trim() ? value : undefined; return typeof value === "string" && value.trim() ? value : undefined;
} }
@@ -1,4 +1,5 @@
/** Pure, non-resolving credential availability checks shared by status and route selection. */ /** Pure, non-resolving credential availability checks shared by status and route selection. */
import { hasNonEmptyString as hasSecret } from "@openclaw/normalization-core/string-coerce";
import type { OpenClawConfig } from "../../config/types.openclaw.js"; import type { OpenClawConfig } from "../../config/types.openclaw.js";
import { import {
isSecretRef, isSecretRef,
@@ -20,10 +21,6 @@ import type { AuthProfileCredential } from "./types.js";
type ReadOnlyCredentialAvailability = boolean | undefined; type ReadOnlyCredentialAvailability = boolean | undefined;
function hasSecret(value: unknown): value is string {
return typeof value === "string" && value.trim().length > 0;
}
export function hasMalformedSecretInputSyntax(value: unknown): boolean { export function hasMalformedSecretInputSyntax(value: unknown): boolean {
if (typeof value !== "string") { if (typeof value !== "string") {
return false; return false;
+2 -5
View File
@@ -6,6 +6,7 @@
import fs from "node:fs"; import fs from "node:fs";
import path from "node:path"; import path from "node:path";
import type { DatabaseSync } from "node:sqlite"; import type { DatabaseSync } from "node:sqlite";
import { safeParseJson } from "@openclaw/normalization-core";
import { sha256HexPrefix } from "../../infra/crypto-digest.js"; import { sha256HexPrefix } from "../../infra/crypto-digest.js";
import { import {
clearNodeSqliteKyselyCacheForDatabase, clearNodeSqliteKyselyCacheForDatabase,
@@ -85,11 +86,7 @@ function parseJsonCell(raw: string | null | undefined): unknown {
if (!raw) { if (!raw) {
return null; return null;
} }
try { return safeParseJson(raw) ?? null;
return JSON.parse(raw) as unknown;
} catch {
return null;
}
} }
type PersistedAuthProfileStoreInspection = type PersistedAuthProfileStoreInspection =
+1 -6
View File
@@ -12,6 +12,7 @@ import {
resolveExpiresAtMsFromEpochSeconds, resolveExpiresAtMsFromEpochSeconds,
} from "@openclaw/normalization-core/number-coercion"; } from "@openclaw/normalization-core/number-coercion";
import type { OpenClawConfig } from "../../config/types.openclaw.js"; import type { OpenClawConfig } from "../../config/types.openclaw.js";
import { cancelUnreadResponseBody } from "../../infra/http-body.js";
import { createSubsystemLogger } from "../../logging/subsystem.js"; import { createSubsystemLogger } from "../../logging/subsystem.js";
import { readProviderJsonResponse } from "../provider-http-errors.js"; import { readProviderJsonResponse } from "../provider-http-errors.js";
import { resolveProviderRequestHeaders } from "../provider-request-config.js"; import { resolveProviderRequestHeaders } from "../provider-request-config.js";
@@ -253,12 +254,6 @@ function applyWhamCooldownResult(params: {
}; };
} }
async function cancelUnreadResponseBody(response: Response): Promise<void> {
if (!response.bodyUsed) {
await response.body?.cancel().catch(() => undefined);
}
}
async function probeWhamForCooldown( async function probeWhamForCooldown(
store: AuthProfileStore, store: AuthProfileStore,
profileId: string, profileId: string,
+2 -2
View File
@@ -3,7 +3,7 @@
* These references are surfaced in agent context so follow-up turns can * These references are surfaced in agent context so follow-up turns can
* reconnect to prior long-running work. * reconnect to prior long-running work.
*/ */
import { truncateUtf16Safe } from "../utils.js"; import { truncateUtf16Safe, truncateWithMarker } from "@openclaw/normalization-core/utf16-slice";
import { listRunningSessions } from "./bash-process-registry.js"; import { listRunningSessions } from "./bash-process-registry.js";
import { deriveSessionName } from "./bash-tools.shared.js"; import { deriveSessionName } from "./bash-tools.shared.js";
@@ -31,7 +31,7 @@ function truncate(value: string, maxChars: number): string {
if (maxChars <= 1) { if (maxChars <= 1) {
return truncateUtf16Safe(value, maxChars); return truncateUtf16Safe(value, maxChars);
} }
return `${truncateUtf16Safe(value, Math.max(0, maxChars - 3))}...`; return truncateWithMarker(value, maxChars, { marker: "...", reserve: 3, trimEnd: false });
} }
/** List active background process sessions for one scope key, newest first. */ /** List active background process sessions for one scope key, newest first. */
@@ -1,4 +1,5 @@
import { asNullableRecord } from "@openclaw/normalization-core/record-coerce"; import { asNullableRecord } from "@openclaw/normalization-core/record-coerce";
import { normalizeOptionalString as readString } from "@openclaw/normalization-core/string-coerce";
import type { OperatorScope } from "../gateway/operator-scopes.js"; import type { OperatorScope } from "../gateway/operator-scopes.js";
import { renderExecUpdateText } from "./bash-tools.exec-output.js"; import { renderExecUpdateText } from "./bash-tools.exec-output.js";
import type { ExecToolDetails } from "./bash-tools.exec-types.js"; import type { ExecToolDetails } from "./bash-tools.exec-types.js";
@@ -27,10 +28,6 @@ type NodeSystemRunInvokeResult =
| { ok: true; raw: unknown } | { ok: true; raw: unknown }
| { ok: false; failure: NodeInvokeFailure }; | { ok: false; failure: NodeInvokeFailure };
function readString(value: unknown): string | undefined {
return typeof value === "string" && value.trim() ? value.trim() : undefined;
}
/** Only NOT_CONNECTED plus explicit pre-dispatch provenance proves a retry cannot duplicate work. */ /** Only NOT_CONNECTED plus explicit pre-dispatch provenance proves a retry cannot duplicate work. */
function classifyNodeInvokeFailure(error: unknown): NodeInvokeFailure { function classifyNodeInvokeFailure(error: unknown): NodeInvokeFailure {
const errorRecord = asNullableRecord(error); const errorRecord = asNullableRecord(error);
+1 -6
View File
@@ -5,6 +5,7 @@
import { randomBytes } from "node:crypto"; import { randomBytes } from "node:crypto";
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce"; import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
import { sha256Base64Url } from "../infra/crypto-digest.js"; import { sha256Base64Url } from "../infra/crypto-digest.js";
import { cancelUnreadResponseBody } from "../infra/http-body.js";
import { resolveExpiresAtMsFromDurationSeconds } from "../infra/parse-finite-number.js"; import { resolveExpiresAtMsFromDurationSeconds } from "../infra/parse-finite-number.js";
import type { OAuthCredentials } from "../llm/oauth.js"; import type { OAuthCredentials } from "../llm/oauth.js";
import { buildOAuthRequestSignal } from "../llm/utils/oauth/abort.js"; import { buildOAuthRequestSignal } from "../llm/utils/oauth/abort.js";
@@ -102,12 +103,6 @@ function resolveChutesExpiresAt(value: unknown, now: number): number | undefined
}); });
} }
async function cancelUnreadResponseBody(response: Response): Promise<void> {
if (!response.bodyUsed) {
await response.body?.cancel().catch(() => undefined);
}
}
async function fetchChutesUserInfo(params: { async function fetchChutesUserInfo(params: {
accessToken: string; accessToken: string;
fetchFn?: typeof fetch; fetchFn?: typeof fetch;
@@ -3,7 +3,7 @@
*/ */
import { safeParseJson } from "@openclaw/normalization-core"; import { safeParseJson } from "@openclaw/normalization-core";
import { asOptionalRecord } from "@openclaw/normalization-core/record-coerce"; import { asOptionalRecord } from "@openclaw/normalization-core/record-coerce";
import { readStringValue } from "@openclaw/normalization-core/string-coerce"; import { hasNonEmptyString, readStringValue } from "@openclaw/normalization-core/string-coerce";
import type { SourceReplyDeliveryMode } from "../auto-reply/get-reply-options.types.js"; import type { SourceReplyDeliveryMode } from "../auto-reply/get-reply-options.types.js";
import { import {
isMessageToolConversationCreateActionName, isMessageToolConversationCreateActionName,
@@ -32,29 +32,22 @@ const BROADCAST_SEND_ENVELOPE_KEYS = ["payload", "result", "sendResult", "toolRe
const PARTIAL_DELIVERY_ENVELOPE_KEYS = [...RESULT_ENVELOPE_KEYS, "error", "cause"]; const PARTIAL_DELIVERY_ENVELOPE_KEYS = [...RESULT_ENVELOPE_KEYS, "error", "cause"];
const SESSIONS_SEND_DELIVERY_STATUSES = new Set(["accepted", "ok"]); const SESSIONS_SEND_DELIVERY_STATUSES = new Set(["accepted", "ok"]);
const BARE_OK_DELIVERY_STATUS = "ok"; const BARE_OK_DELIVERY_STATUS = "ok";
function asRecord(value: unknown): Record<string, unknown> {
return value && typeof value === "object" && !Array.isArray(value)
? (value as Record<string, unknown>)
: {};
}
function resultConfirmsCurrentSourceRoute(value: unknown): boolean { function resultConfirmsCurrentSourceRoute(value: unknown): boolean {
return asRecord(asRecord(value).details).sourceReplyRoute === "current-source"; return (
} (asOptionalRecord(asOptionalRecord(value)?.details) ?? {}).sourceReplyRoute === "current-source"
);
function hasStringValue(value: unknown): boolean {
return typeof value === "string" && value.trim().length > 0;
} }
function hasConversationIdValue(value: unknown): boolean { function hasConversationIdValue(value: unknown): boolean {
return hasStringValue(value) || (typeof value === "number" && Number.isFinite(value)); return hasNonEmptyString(value) || (typeof value === "number" && Number.isFinite(value));
} }
function hasExplicitMessageRoute(args: Record<string, unknown>): boolean { function hasExplicitMessageRoute(args: Record<string, unknown>): boolean {
if (EXPLICIT_MESSAGE_ROUTE_KEYS.some((key) => hasStringValue(args[key]))) { if (EXPLICIT_MESSAGE_ROUTE_KEYS.some((key) => hasNonEmptyString(args[key]))) {
return true; return true;
} }
return Array.isArray(args.targets) && args.targets.some((value) => hasStringValue(value)); return Array.isArray(args.targets) && args.targets.some((value) => hasNonEmptyString(value));
} }
function isMessageToolSourceReplyActionName(action: unknown): boolean { function isMessageToolSourceReplyActionName(action: unknown): boolean {
@@ -73,7 +66,7 @@ function isMessageToolSourceReplyActionName(action: unknown): boolean {
/** Read the visible text delivered by a source-reply message action. */ /** Read the visible text delivered by a source-reply message action. */
export function readMessageToolSourceReplyText(args: unknown): string | undefined { export function readMessageToolSourceReplyText(args: unknown): string | undefined {
const record = asRecord(args); const record = asOptionalRecord(args) ?? {};
if (!isMessageToolSourceReplyActionName(record.action)) { if (!isMessageToolSourceReplyActionName(record.action)) {
return undefined; return undefined;
} }
@@ -110,7 +103,7 @@ function recordHasDeliveredMessageId(record: Record<string, unknown>): boolean {
const normalized = normalizeStatus(value); const normalized = normalizeStatus(value);
return Boolean(normalized && !NON_DELIVERY_MESSAGE_IDS.has(normalized)); return Boolean(normalized && !NON_DELIVERY_MESSAGE_IDS.has(normalized));
}; };
const message = asRecord(record.message); const message = asOptionalRecord(record.message) ?? {};
if ( if (
hasDeliveredId(record.messageId) || hasDeliveredId(record.messageId) ||
hasDeliveredId(record.pollId) || hasDeliveredId(record.pollId) ||
@@ -504,7 +497,7 @@ export function isDeliveredMessagingToolResult(params: {
hookResult?: unknown; hookResult?: unknown;
isError?: boolean; isError?: boolean;
}): boolean { }): boolean {
const args = asRecord(params.args); const args = asOptionalRecord(params.args) ?? {};
const action = normalizeStatus(args.action); const action = normalizeStatus(args.action);
if ( if (
args.dryRun === true || args.dryRun === true ||
@@ -595,7 +588,7 @@ export function isDeliveredMessageToolOnlySourceReplyResult(params: {
if (normalizeToolName(params.toolName) !== MESSAGE_TOOL_NAME) { if (normalizeToolName(params.toolName) !== MESSAGE_TOOL_NAME) {
return false; return false;
} }
const args = asRecord(params.args); const args = asOptionalRecord(params.args) ?? {};
const sourceRouteReplyAction = const sourceRouteReplyAction =
(params.allowExplicitSourceRoute === true || confirmedCurrentSourceRoute) && (params.allowExplicitSourceRoute === true || confirmedCurrentSourceRoute) &&
isMessageToolSourceReplyActionName(args.action); isMessageToolSourceReplyActionName(args.action);
@@ -1,3 +1,4 @@
import { hasNonEmptyString } from "@openclaw/normalization-core/string-coerce";
import { normalizeMediaReferenceForComparison } from "../../media/media-reference-comparison.js"; import { normalizeMediaReferenceForComparison } from "../../media/media-reference-comparison.js";
/** /**
* Extracts visible delivery evidence from embedded-agent run results. * Extracts visible delivery evidence from embedded-agent run results.
@@ -97,10 +98,6 @@ export function hasCompletedTerminalDeliveryEvidence(
); );
} }
function hasNonEmptyString(value: unknown): value is string {
return typeof value === "string" && value.trim().length > 0;
}
function hasNonEmptyArray(value: unknown): boolean { function hasNonEmptyArray(value: unknown): boolean {
return Array.isArray(value) && value.length > 0; return Array.isArray(value) && value.length > 0;
} }
@@ -2,6 +2,7 @@
* Builds extension factories available to embedded-agent runtime sessions. * Builds extension factories available to embedded-agent runtime sessions.
*/ */
import { randomUUID } from "node:crypto"; import { randomUUID } from "node:crypto";
import { asOptionalRecord } from "@openclaw/normalization-core/record-coerce";
import type { OpenClawConfig } from "../../config/types.openclaw.js"; import type { OpenClawConfig } from "../../config/types.openclaw.js";
import type { ProviderRuntimeModel } from "../../plugins/provider-runtime-model.types.js"; import type { ProviderRuntimeModel } from "../../plugins/provider-runtime-model.types.js";
import { normalizeAcceptedSessionSpawnResult } from "../accepted-session-spawn.js"; import { normalizeAcceptedSessionSpawnResult } from "../accepted-session-spawn.js";
@@ -31,14 +32,8 @@ type AgentToolResultEvent = {
isError?: boolean; isError?: boolean;
}; };
function recordFromUnknown(value: unknown): Record<string, unknown> {
return value && typeof value === "object" && !Array.isArray(value)
? (value as Record<string, unknown>)
: {};
}
function snapshotToolSendReceipt(details: unknown): unknown { function snapshotToolSendReceipt(details: unknown): unknown {
const toolSend = recordFromUnknown(details).toolSend; const toolSend = (asOptionalRecord(details) ?? {}).toolSend;
return toolSend && typeof toolSend === "object" && !Array.isArray(toolSend) return toolSend && typeof toolSend === "object" && !Array.isArray(toolSend)
? { ...(toolSend as Record<string, unknown>) } ? { ...(toolSend as Record<string, unknown>) }
: toolSend; : toolSend;
@@ -66,7 +61,7 @@ function buildAgentToolResultMiddlewareFactory(
}); });
return (agent) => { return (agent) => {
agent.on("tool_result", async (rawEvent: unknown, ctx: { cwd?: string }) => { agent.on("tool_result", async (rawEvent: unknown, ctx: { cwd?: string }) => {
const event = recordFromUnknown(rawEvent) as AgentToolResultEvent; const event = (asOptionalRecord(rawEvent) ?? {}) as AgentToolResultEvent;
if (!event.toolName) { if (!event.toolName) {
return undefined; return undefined;
} }
@@ -94,7 +89,7 @@ function buildAgentToolResultMiddlewareFactory(
turnId: event.turnId, turnId: event.turnId,
toolCallId, toolCallId,
toolName: event.toolName, toolName: event.toolName,
args: recordFromUnknown(adjustedInput ?? event.input), args: asOptionalRecord(adjustedInput ?? event.input) ?? {},
cwd: ctx.cwd, cwd: ctx.cwd,
isError: event.isError, isError: event.isError,
result: current, result: current,
@@ -16,7 +16,7 @@ import {
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce"; import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
import { parseGeminiAuth } from "../../infra/gemini-auth.js"; import { parseGeminiAuth } from "../../infra/gemini-auth.js";
import { normalizeGoogleApiBaseUrl } from "../../infra/google-api-base-url.js"; import { normalizeGoogleApiBaseUrl } from "../../infra/google-api-base-url.js";
import { readResponseWithLimit } from "../../infra/http-body.js"; import { cancelUnreadResponseBody, readResponseWithLimit } from "../../infra/http-body.js";
import { streamWithPayloadPatch } from "../../llm/providers/stream-wrappers/stream-payload-utils.js"; import { streamWithPayloadPatch } from "../../llm/providers/stream-wrappers/stream-payload-utils.js";
import type { Model } from "../../llm/types.js"; import type { Model } from "../../llm/types.js";
import { isSecretValueRegisteredForRedaction } from "../../logging/secret-redaction-registry.js"; import { isSecretValueRegisteredForRedaction } from "../../logging/secret-redaction-registry.js";
@@ -286,12 +286,6 @@ function buildManagedContextForCachedContent(context: GooglePromptCacheContext)
}; };
} }
async function cancelUnreadResponseBody(response: Response | undefined): Promise<void> {
if (response && !response.bodyUsed) {
await response.body?.cancel().catch(() => undefined);
}
}
/** /**
* Reads a Google cachedContents JSON body under a byte cap and parses it. * Reads a Google cachedContents JSON body under a byte cap and parses it.
* Streams through the shared limiter so an oversized response is cancelled * Streams through the shared limiter so an oversized response is cancelled
@@ -1,3 +1,4 @@
import { hasNonEmptyString } from "@openclaw/normalization-core/string-coerce";
import { import {
isSilentReplyPayloadText, isSilentReplyPayloadText,
isSilentReplyText, isSilentReplyText,
@@ -24,10 +25,6 @@ type PayloadVisibilityOptions = {
includeSilentReplyPayloads?: boolean; includeSilentReplyPayloads?: boolean;
}; };
function hasNonEmptyString(value: unknown): value is string {
return typeof value === "string" && value.trim().length > 0;
}
function hasNonEmptyStringArray(value: unknown): boolean { function hasNonEmptyStringArray(value: unknown): boolean {
return Array.isArray(value) && value.some(hasNonEmptyString); return Array.isArray(value) && value.some(hasNonEmptyString);
} }
@@ -1,3 +1,4 @@
import { asOptionalRecord as readModelParams } from "@openclaw/normalization-core/record-coerce";
import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce"; import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce";
import type { ModelCompatConfig, ModelMediaInputConfig } from "../../config/types.models.js"; import type { ModelCompatConfig, ModelMediaInputConfig } from "../../config/types.models.js";
import type { OpenClawConfig } from "../../config/types.openclaw.js"; import type { OpenClawConfig } from "../../config/types.openclaw.js";
@@ -228,13 +229,6 @@ export function hasConfiguredFallbackSurface(params: {
return Boolean(params.providerConfig?.baseUrl?.trim()); return Boolean(params.providerConfig?.baseUrl?.trim());
} }
function readModelParams(value: unknown): Record<string, unknown> | undefined {
if (!value || typeof value !== "object" || Array.isArray(value)) {
return undefined;
}
return value as Record<string, unknown>;
}
function mergeModelParams( function mergeModelParams(
...entries: Array<Record<string, unknown> | undefined> ...entries: Array<Record<string, unknown> | undefined>
): Record<string, unknown> | undefined { ): Record<string, unknown> | undefined {
@@ -19,7 +19,7 @@
*/ */
import { formatErrorMessage } from "../../infra/errors.js"; import { formatErrorMessage } from "../../infra/errors.js";
import { readResponseWithLimit } from "../../infra/http-body.js"; import { cancelUnreadResponseBody, readResponseWithLimit } from "../../infra/http-body.js";
import { resolveProxyFetchFromEnv } from "../../infra/net/proxy-fetch.js"; import { resolveProxyFetchFromEnv } from "../../infra/net/proxy-fetch.js";
import { parseStrictFiniteNumber } from "../../infra/parse-finite-number.js"; import { parseStrictFiniteNumber } from "../../infra/parse-finite-number.js";
import { createSubsystemLogger } from "../../logging/subsystem.js"; import { createSubsystemLogger } from "../../logging/subsystem.js";
@@ -177,12 +177,6 @@ function parseModel(model: OpenRouterApiModel): OpenRouterModelCapabilities {
}; };
} }
async function cancelUnreadResponseBody(response: Response | undefined): Promise<void> {
if (response && !response.bodyUsed) {
await response.body?.cancel().catch(() => undefined);
}
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// API fetch // API fetch
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -2,6 +2,7 @@
* Sanitizes and validates replayed session history before model calls. * Sanitizes and validates replayed session history before model calls.
*/ */
import { isDeepStrictEqual } from "node:util"; import { isDeepStrictEqual } from "node:util";
import { asFiniteNumber as toFiniteCostNumber } from "@openclaw/normalization-core/number-coercion";
import { stripInternalMetadataForDisplay } from "../../auto-reply/reply/display-text-sanitize.js"; import { stripInternalMetadataForDisplay } from "../../auto-reply/reply/display-text-sanitize.js";
import { isSilentReplyPayloadText, SILENT_REPLY_TOKEN } from "../../auto-reply/tokens.js"; import { isSilentReplyPayloadText, SILENT_REPLY_TOKEN } from "../../auto-reply/tokens.js";
import type { OpenClawConfig } from "../../config/types.openclaw.js"; import type { OpenClawConfig } from "../../config/types.openclaw.js";
@@ -522,10 +523,6 @@ function normalizeAssistantUsageCost(usage: unknown): AssistantUsageSnapshot["co
return { input, output, cacheRead, cacheWrite, total, ...(totalOrigin ? { totalOrigin } : {}) }; return { input, output, cacheRead, cacheWrite, total, ...(totalOrigin ? { totalOrigin } : {}) };
} }
function toFiniteCostNumber(value: unknown): number | undefined {
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
}
function ensureAssistantUsageSnapshots(messages: AgentMessage[]): AgentMessage[] { function ensureAssistantUsageSnapshots(messages: AgentMessage[]): AgentMessage[] {
if (messages.length === 0) { if (messages.length === 0) {
return messages; return messages;
@@ -2,7 +2,10 @@
* Normalizes tool-call names, ids, and standalone text calls for providers. * Normalizes tool-call names, ids, and standalone text calls for providers.
*/ */
import { randomUUID } from "node:crypto"; import { randomUUID } from "node:crypto";
import { normalizeLowercaseStringOrEmpty } from "../../../../packages/normalization-core/src/string-coerce.js"; import {
hasNonEmptyString as replayToolCallNonEmptyString,
normalizeLowercaseStringOrEmpty,
} from "../../../../packages/normalization-core/src/string-coerce.js";
import { normalizeStringEntries } from "../../../../packages/normalization-core/src/string-normalization.js"; import { normalizeStringEntries } from "../../../../packages/normalization-core/src/string-normalization.js";
import { import {
createPromotedPlainTextToolCallEvents, createPromotedPlainTextToolCallEvents,
@@ -336,10 +339,6 @@ function collectFollowingToolResults(
return { ids, displaced }; return { ids, displaced };
} }
function replayToolCallNonEmptyString(value: unknown): value is string {
return typeof value === "string" && value.trim().length > 0;
}
function resolveReplayToolCallName( function resolveReplayToolCallName(
rawName: string, rawName: string,
rawId: string, rawId: string,
@@ -4,6 +4,7 @@
import type { LlmRuntime } from "@openclaw/ai"; import type { LlmRuntime } from "@openclaw/ai";
import { stripSystemPromptCacheBoundary } from "@openclaw/ai/internal/shared"; import { stripSystemPromptCacheBoundary } from "@openclaw/ai/internal/shared";
import { createBoundaryAwareStreamFnForModel } from "@openclaw/ai/transports"; import { createBoundaryAwareStreamFnForModel } from "@openclaw/ai/transports";
import { hasNonEmptyString as hasResolvedRuntimeApiKey } from "@openclaw/normalization-core/string-coerce";
import { getStreamLlmRuntime } from "../../llm/model-runtime-binding.js"; import { getStreamLlmRuntime } from "../../llm/model-runtime-binding.js";
import "../ai-transport-runtime-host.js"; import "../ai-transport-runtime-host.js";
import { createAnthropicVertexStreamFnForModel } from "../anthropic-vertex-stream.js"; import { createAnthropicVertexStreamFnForModel } from "../anthropic-vertex-stream.js";
@@ -69,10 +70,6 @@ function isDefaultOpenClawStreamFnForModel(
return streamFn === provider?.streamSimple || streamFn === provider?.stream; return streamFn === provider?.streamSimple || streamFn === provider?.stream;
} }
function hasResolvedRuntimeApiKey(apiKey: string | undefined): boolean {
return typeof apiKey === "string" && apiKey.trim().length > 0;
}
function isOpenAICodexResponsesModel(model: EmbeddedRunAttemptParams["model"]): boolean { function isOpenAICodexResponsesModel(model: EmbeddedRunAttemptParams["model"]): boolean {
return model.provider === "openai" && model.api === "openai-chatgpt-responses"; return model.provider === "openai" && model.api === "openai-chatgpt-responses";
} }
@@ -1,4 +1,4 @@
import { truncateUtf16Safe } from "@openclaw/normalization-core/utf16-slice"; import { truncateUtf16Safe, truncateWithMarker } from "@openclaw/normalization-core/utf16-slice";
import type { import type {
JsonValue, JsonValue,
NativeHookRelayEvent, NativeHookRelayEvent,
@@ -213,8 +213,5 @@ function snapshotString(value: string, state: { remainingStringLength: number })
} }
export function truncateText(value: string, maxLength: number): string { export function truncateText(value: string, maxLength: number): string {
if (value.length <= maxLength) { return truncateWithMarker(value, maxLength, { marker: "...", reserve: 3, trimEnd: false });
return value;
}
return `${truncateUtf16Safe(value, Math.max(0, maxLength - 3))}...`;
} }
+1 -4
View File
@@ -1,4 +1,5 @@
import { normalizeProviderId } from "@openclaw/model-catalog-core/provider-id"; import { normalizeProviderId } from "@openclaw/model-catalog-core/provider-id";
import { normalizeOptionalString as readStringParam } from "@openclaw/normalization-core/string-coerce";
import { import {
resolveMergedModelProviderConfig, resolveMergedModelProviderConfig,
resolveMergedModelProviderModels, resolveMergedModelProviderModels,
@@ -258,10 +259,6 @@ function isSupportedHarness(entry: {
return entry.support.supported; return entry.support.supported;
} }
function readStringParam(value: unknown): string | undefined {
return typeof value === "string" && value.trim() ? value.trim() : undefined;
}
function normalizeModelId(provider: string, modelId: string): string { function normalizeModelId(provider: string, modelId: string): string {
const trimmed = modelId.trim(); const trimmed = modelId.trim();
const slashIndex = trimmed.indexOf("/"); const slashIndex = trimmed.indexOf("/");
+1 -4
View File
@@ -4,6 +4,7 @@ import {
normalizeProviderId, normalizeProviderId,
normalizeProviderIdForAuth, normalizeProviderIdForAuth,
} from "@openclaw/model-catalog-core/provider-id"; } from "@openclaw/model-catalog-core/provider-id";
import { hasNonEmptyString as hasSecret } from "@openclaw/normalization-core/string-coerce";
import { resolveAgentModelPrimaryValue } from "../config/model-input.js"; import { resolveAgentModelPrimaryValue } from "../config/model-input.js";
import { resolveMergedModelProviderConfig } from "../config/model-provider-config.js"; import { resolveMergedModelProviderConfig } from "../config/model-provider-config.js";
import type { OpenClawConfig } from "../config/types.openclaw.js"; import type { OpenClawConfig } from "../config/types.openclaw.js";
@@ -128,10 +129,6 @@ type AuthSourceEvaluation = Pick<
"availability" | "selectedAuthMode" | "evidence" | "selectedProfileId" "availability" | "selectedAuthMode" | "evidence" | "selectedProfileId"
>; >;
function hasSecret(value: unknown): value is string {
return typeof value === "string" && value.trim().length > 0;
}
function modeAllowed(provider: string, target: AuthTarget, mode: string | undefined): boolean { function modeAllowed(provider: string, target: AuthTarget, mode: string | undefined): boolean {
const requirement = resolveProviderModelRouteAuthRequirement(mode); const requirement = resolveProviderModelRouteAuthRequirement(mode);
return target.authRequirement return target.authRequirement
+1 -4
View File
@@ -1,3 +1,4 @@
import { normalizeLowercaseStringOrEmpty as normalizeApi } from "@openclaw/normalization-core/string-coerce";
import type { ModelCompatConfig } from "../config/types.models.js"; import type { ModelCompatConfig } from "../config/types.models.js";
type ModelTransportRoute = { type ModelTransportRoute = {
@@ -5,10 +6,6 @@ type ModelTransportRoute = {
baseUrl?: unknown; baseUrl?: unknown;
}; };
function normalizeApi(value: unknown): string {
return typeof value === "string" ? value.trim().toLowerCase() : "";
}
function normalizeBaseUrl(value: unknown): string { function normalizeBaseUrl(value: unknown): string {
if (typeof value !== "string") { if (typeof value !== "string") {
return ""; return "";
+2 -3
View File
@@ -20,6 +20,7 @@ import {
import pMap from "p-map"; import pMap from "p-map";
import { Type } from "typebox"; import { Type } from "typebox";
import { formatErrorMessage } from "../infra/errors.js"; import { formatErrorMessage } from "../infra/errors.js";
import { cancelUnreadResponseBody } from "../infra/http-body.js";
/** /**
* Scans remote provider model catalogs for configured providers. * Scans remote provider model catalogs for configured providers.
*/ */
@@ -284,9 +285,7 @@ async function fetchOpenRouterModels(
"OpenRouter model scan", "OpenRouter model scan",
); );
} finally { } finally {
if (res && !res.bodyUsed) { await cancelUnreadResponseBody(res);
await res.body?.cancel().catch(() => undefined);
}
} }
} }
+3 -9
View File
@@ -3,6 +3,8 @@
* store preserves typed columns for hot delivery state while retaining the * store preserves typed columns for hot delivery state while retaining the
* normalized payload JSON for forward-compatible record hydration. * normalized payload JSON for forward-compatible record hydration.
*/ */
import { safeParseJson } from "@openclaw/normalization-core";
import { asFiniteNumber as normalizeFiniteNumber } from "@openclaw/normalization-core/number-coercion";
import { isRecord } from "@openclaw/normalization-core/record-coerce"; import { isRecord } from "@openclaw/normalization-core/record-coerce";
import { sql, type Insertable, type Selectable, type Updateable } from "kysely"; import { sql, type Insertable, type Selectable, type Updateable } from "kysely";
import { executeSqliteQuerySync, getNodeSqliteKysely } from "../infra/kysely-sync.js"; import { executeSqliteQuerySync, getNodeSqliteKysely } from "../infra/kysely-sync.js";
@@ -82,21 +84,13 @@ function parseJson(raw: string | null): unknown {
if (!raw) { if (!raw) {
return undefined; return undefined;
} }
try { return safeParseJson(raw);
return JSON.parse(raw);
} catch {
return undefined;
}
} }
function boolToSqlite(value: boolean | undefined): number | null { function boolToSqlite(value: boolean | undefined): number | null {
return value === undefined ? null : value ? 1 : 0; return value === undefined ? null : value ? 1 : 0;
} }
function normalizeFiniteNumber(value: number | null): number | undefined {
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
}
/** Rehydrates one sqlite row into the normalized subagent run record shape. */ /** Rehydrates one sqlite row into the normalized subagent run record shape. */
function rowToSubagentRunRecord(row: SubagentRunSqliteRow): SubagentRunRecord | null { function rowToSubagentRunRecord(row: SubagentRunSqliteRow): SubagentRunRecord | null {
const payload = parseJson(row.payload_json); const payload = parseJson(row.payload_json);
+1 -4
View File
@@ -1,6 +1,7 @@
// Persists runtime tool-schema quarantines in the shared SQLite-backed core // Persists runtime tool-schema quarantines in the shared SQLite-backed core
// plugin-state store so health surfaces can see failures from any live // plugin-state store so health surfaces can see failures from any live
// runtime process. // runtime process.
import { hasNonEmptyString as isNonEmptyString } from "@openclaw/normalization-core/string-coerce";
import { import {
createRuntimeHealthRecordEnvelope, createRuntimeHealthRecordEnvelope,
createRuntimeHealthStore, createRuntimeHealthStore,
@@ -20,10 +21,6 @@ type PersistedRuntimeToolSchemaQuarantineRecord = RuntimeHealthRecordEnvelope &
reason: string; reason: string;
}; };
function isNonEmptyString(value: unknown): value is string {
return typeof value === "string" && value.trim().length > 0;
}
const quarantineStore = createRuntimeHealthStore<PersistedRuntimeToolSchemaQuarantineRecord>({ const quarantineStore = createRuntimeHealthStore<PersistedRuntimeToolSchemaQuarantineRecord>({
ownerId: "core:runtime-tool-quarantine-health", ownerId: "core:runtime-tool-quarantine-health",
namespace: "schema-quarantines", namespace: "schema-quarantines",
+1 -4
View File
@@ -4,6 +4,7 @@
* Recovers flat or partial model/tool inputs into the structured cron job/patch shape. * Recovers flat or partial model/tool inputs into the structured cron job/patch shape.
*/ */
import { timestampMsToIsoString } from "@openclaw/normalization-core/number-coercion"; import { timestampMsToIsoString } from "@openclaw/normalization-core/number-coercion";
import { hasNonEmptyString as isNonEmptyString } from "@openclaw/normalization-core/string-coerce";
import { isRecord } from "../../utils.js"; import { isRecord } from "../../utils.js";
import { isStringOption } from "../../utils/string-readers.js"; import { isStringOption } from "../../utils/string-readers.js";
@@ -75,10 +76,6 @@ function isCronPayloadKind(value: unknown): value is (typeof CRON_PAYLOAD_KINDS)
return value === "systemEvent" || value === "agentTurn" || value === "script"; return value === "systemEvent" || value === "agentTurn" || value === "script";
} }
function isNonEmptyString(value: unknown): value is string {
return typeof value === "string" && value.trim().length > 0;
}
function isStringArrayOrNull(value: unknown): boolean { function isStringArrayOrNull(value: unknown): boolean {
return ( return (
value === null || (Array.isArray(value) && value.every((entry) => typeof entry === "string")) value === null || (Array.isArray(value) && value.every((entry) => typeof entry === "string"))
+2 -6
View File
@@ -1,7 +1,7 @@
import { truncateWithMarker } from "@openclaw/normalization-core/utf16-slice";
/** Reminder-context projection for cron tool job creation. */ /** Reminder-context projection for cron tool job creation. */
import { getRuntimeConfig } from "../../config/config.js"; import { getRuntimeConfig } from "../../config/config.js";
import { extractTextFromChatContent } from "../../shared/chat-content.js"; import { extractTextFromChatContent } from "../../shared/chat-content.js";
import { truncateUtf16Safe } from "../../utils.js";
import { REMINDER_CONTEXT_MESSAGES_MAX } from "./cron-tool-schema.js"; import { REMINDER_CONTEXT_MESSAGES_MAX } from "./cron-tool-schema.js";
import type { ChatMessage, GatewayToolCaller } from "./cron-tool.types.js"; import type { ChatMessage, GatewayToolCaller } from "./cron-tool.types.js";
import type { GatewayCallOptions } from "./gateway.js"; import type { GatewayCallOptions } from "./gateway.js";
@@ -20,11 +20,7 @@ export function stripExistingContext(text: string) {
} }
function truncateText(input: string, maxLen: number) { function truncateText(input: string, maxLen: number) {
if (input.length <= maxLen) { return truncateWithMarker(input, maxLen, { marker: "...", reserve: 3, trimEnd: true });
return input;
}
const truncated = truncateUtf16Safe(input, Math.max(0, maxLen - 3)).trimEnd();
return `${truncated}...`;
} }
function extractMessageText(message: ChatMessage): { role: string; text: string } | null { function extractMessageText(message: ChatMessage): { role: string; text: string } | null {
+2 -7
View File
@@ -4,6 +4,7 @@
* Manages live capture, manual import, summarization, and process-local transcript sessions. * Manages live capture, manual import, summarization, and process-local transcript sessions.
*/ */
import path from "node:path"; import path from "node:path";
import { asOptionalRecord } from "@openclaw/normalization-core/record-coerce";
import { uniqueStrings } from "@openclaw/normalization-core/string-normalization"; import { uniqueStrings } from "@openclaw/normalization-core/string-normalization";
import { Type } from "typebox"; import { Type } from "typebox";
import { resolveStateDir } from "../../config/paths.js"; import { resolveStateDir } from "../../config/paths.js";
@@ -59,12 +60,6 @@ function ownsTranscriptSession(
return ctx.agentId === "main"; return ctx.agentId === "main";
} }
function asParamsRecord(params: unknown): Record<string, unknown> {
return params && typeof params === "object" && !Array.isArray(params)
? (params as Record<string, unknown>)
: {};
}
const TranscriptsSchema = Type.Object( const TranscriptsSchema = Type.Object(
{ {
action: Type.String({ action: Type.String({
@@ -356,7 +351,7 @@ export function createTranscriptsTool(options?: {
if (!config.enabled) { if (!config.enabled) {
throw new Error("transcripts are disabled"); throw new Error("transcripts are disabled");
} }
const params = asParamsRecord(rawParams); const params = asOptionalRecord(rawParams) ?? {};
const action = readStringParam(params, "action", { required: true, trim: true }); const action = readStringParam(params, "action", { required: true, trim: true });
const store = createStore(ctx); const store = createStore(ctx);
switch (action) { switch (action) {
+1 -4
View File
@@ -7,6 +7,7 @@
* re-wrapped here unconditionally, so no provider-controlled metadata can * re-wrapped here unconditionally, so no provider-controlled metadata can
* spoof the trust marker and transport-specific extras never reach the model. * spoof the trust marker and transport-specific extras never reach the model.
*/ */
import { asFiniteNumber as readFiniteNumber } from "@openclaw/normalization-core/number-coercion";
import { isRecord } from "@openclaw/normalization-core/record-coerce"; import { isRecord } from "@openclaw/normalization-core/record-coerce";
import { truncateUtf16Safe } from "@openclaw/normalization-core/utf16-slice"; import { truncateUtf16Safe } from "@openclaw/normalization-core/utf16-slice";
import type { Static } from "typebox"; import type { Static } from "typebox";
@@ -119,10 +120,6 @@ function unwrapEnvelopes(value: string): string {
return value.replace(ENVELOPE_OPEN_RE, "").replace(ENVELOPE_END_RE, "").trim(); return value.replace(ENVELOPE_OPEN_RE, "").replace(ENVELOPE_END_RE, "").trim();
} }
function readFiniteNumber(value: unknown): number | undefined {
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
}
// URLs are emitted canonicalized (percent-encoded), so whitespace or readable // URLs are emitted canonicalized (percent-encoded), so whitespace or readable
// prose smuggled into a URL slot cannot ride outside the envelope as-is. // prose smuggled into a URL slot cannot ride outside the envelope as-is.
function toHttpUrl(value: string): string | undefined { function toHttpUrl(value: string): string | undefined {
+1 -6
View File
@@ -22,6 +22,7 @@ import type { ReadableStream as NodeReadableStream } from "node:stream/web";
import chalk from "chalk"; import chalk from "chalk";
import { extractArchive } from "../../infra/archive.js"; import { extractArchive } from "../../infra/archive.js";
import { isTruthyEnvValue } from "../../infra/env.js"; import { isTruthyEnvValue } from "../../infra/env.js";
import { cancelUnreadResponseBody } from "../../infra/http-body.js";
import { fetchWithSsrFGuard } from "../../infra/net/fetch-guard.js"; import { fetchWithSsrFGuard } from "../../infra/net/fetch-guard.js";
import { APP_NAME, getBinDir } from "../config.js"; import { APP_NAME, getBinDir } from "../config.js";
import { readProviderJsonResponse } from "../provider-http-errors.js"; import { readProviderJsonResponse } from "../provider-http-errors.js";
@@ -36,12 +37,6 @@ const ARCHIVE_EXTRACT_TIMEOUT_MS = 60_000;
const CONTENT_LENGTH_RE = /^\d+$/; const CONTENT_LENGTH_RE = /^\d+$/;
const GITHUB_RELEASE_JSON_MAX_BYTES = 1024 * 1024; const GITHUB_RELEASE_JSON_MAX_BYTES = 1024 * 1024;
async function cancelUnreadResponseBody(response: Response): Promise<void> {
if (!response.bodyUsed) {
await response.body?.cancel().catch(() => undefined);
}
}
function isOfflineModeEnabled(): boolean { function isOfflineModeEnabled(): boolean {
return isTruthyEnvValue(process.env.OPENCLAW_OFFLINE); return isTruthyEnvValue(process.env.OPENCLAW_OFFLINE);
} }
+1 -6
View File
@@ -2,7 +2,6 @@
// unintentionally breaking on newlines. Using [\s\S] keeps newlines inside // unintentionally breaking on newlines. Using [\s\S] keeps newlines inside
// the chunk so messages are only split when they truly exceed the limit. // the chunk so messages are only split when they truly exceed the limit.
import { resolveIntegerOption } from "@openclaw/normalization-core/number-coercion";
import { import {
findFenceSpanAt, findFenceSpanAt,
isSafeFenceBreak, isSafeFenceBreak,
@@ -16,6 +15,7 @@ import { normalizeAccountId } from "../routing/session-key.js";
import { import {
avoidTrailingHighSurrogateBreak, avoidTrailingHighSurrogateBreak,
chunkTextByBreakResolver, chunkTextByBreakResolver,
normalizeChunkLimit,
} from "../shared/text-chunking.js"; } from "../shared/text-chunking.js";
import { INTERNAL_MESSAGE_CHANNEL } from "../utils/message-channel-constants.js"; import { INTERNAL_MESSAGE_CHANNEL } from "../utils/message-channel-constants.js";
@@ -33,11 +33,6 @@ export type ChunkMode = "length" | "newline";
const DEFAULT_CHUNK_LIMIT = 4000; const DEFAULT_CHUNK_LIMIT = 4000;
const DEFAULT_CHUNK_MODE: ChunkMode = "length"; const DEFAULT_CHUNK_MODE: ChunkMode = "length";
function normalizeChunkLimit(limit: number): number {
// String slicing truncates fractional indexes, so positive limits need an integer progress step.
return Number.isFinite(limit) && limit > 0 ? resolveIntegerOption(limit, 1, { min: 1 }) : limit;
}
type ProviderChunkConfig = { type ProviderChunkConfig = {
textChunkLimit?: number; textChunkLimit?: number;
streaming?: unknown; streaming?: unknown;
+2 -2
View File
@@ -4,7 +4,7 @@ import {
normalizeOptionalLowercaseString, normalizeOptionalLowercaseString,
normalizeOptionalString, normalizeOptionalString,
} from "@openclaw/normalization-core/string-coerce"; } from "@openclaw/normalization-core/string-coerce";
import { truncateUtf16Safe } from "@openclaw/normalization-core/utf16-slice"; import { truncateUtf16Safe, truncateWithMarker } from "@openclaw/normalization-core/utf16-slice";
import { resolveAcpToolTerminalOutcome } from "../../acp/tool-status.js"; import { resolveAcpToolTerminalOutcome } from "../../acp/tool-status.js";
import { EmbeddedBlockChunker } from "../../agents/embedded-agent-block-chunker.js"; import { EmbeddedBlockChunker } from "../../agents/embedded-agent-block-chunker.js";
import { formatToolSummary, resolveToolDisplay } from "../../agents/tool-display.js"; import { formatToolSummary, resolveToolDisplay } from "../../agents/tool-display.js";
@@ -53,7 +53,7 @@ function truncateText(input: string, maxChars: number): string {
if (maxChars <= 1) { if (maxChars <= 1) {
return truncateUtf16Safe(input, maxChars); return truncateUtf16Safe(input, maxChars);
} }
return `${truncateUtf16Safe(input, maxChars - 1)}`; return truncateWithMarker(input, maxChars, { marker: "…", reserve: 1, trimEnd: false });
} }
function hashText(text: string): string { function hashText(text: string): string {
@@ -1,3 +1,5 @@
import { asFiniteNumber as readFiniteNumberValue } from "@openclaw/normalization-core/number-coercion";
import { asOptionalRecord as readRecordValue } from "@openclaw/normalization-core/record-coerce";
import { import {
normalizeLowercaseStringOrEmpty, normalizeLowercaseStringOrEmpty,
readStringValue, readStringValue,
@@ -5,12 +7,6 @@ import {
import { inferToolMetaFromArgs } from "../../agents/embedded-agent-utils.js"; import { inferToolMetaFromArgs } from "../../agents/embedded-agent-utils.js";
import type { GetReplyOptions } from "../types.js"; import type { GetReplyOptions } from "../types.js";
function readRecordValue(value: unknown): Record<string, unknown> | undefined {
return value && typeof value === "object" && !Array.isArray(value)
? (value as Record<string, unknown>)
: undefined;
}
/** /**
* CLI backends report a tool result as its raw content: a string, or the text * CLI backends report a tool result as its raw content: a string, or the text
* blocks the harness streamed. Structured runners send a record instead, so the * blocks the harness streamed. Structured runners send a record instead, so the
@@ -32,10 +28,6 @@ function readToolResultText(value: unknown): string | undefined {
return text || undefined; return text || undefined;
} }
function readFiniteNumberValue(value: unknown): number | undefined {
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
}
function readNullableNumberValue(value: unknown): number | null | undefined { function readNullableNumberValue(value: unknown): number | null | undefined {
if (value === null) { if (value === null) {
return null; return null;
+5 -8
View File
@@ -2,6 +2,7 @@
import { randomUUID } from "node:crypto"; import { randomUUID } from "node:crypto";
import { toAcpRuntimeErrorText } from "@openclaw/acp-core/runtime/error-text"; import { toAcpRuntimeErrorText } from "@openclaw/acp-core/runtime/error-text";
import type { AcpRuntimeSessionMode } from "@openclaw/acp-core/runtime/types"; import type { AcpRuntimeSessionMode } from "@openclaw/acp-core/runtime/types";
import type { Result } from "@openclaw/normalization-core/result";
import { import {
normalizeOptionalLowercaseString, normalizeOptionalLowercaseString,
normalizeOptionalString, normalizeOptionalString,
@@ -183,7 +184,7 @@ function resolveDefaultSpawnThreadMode(params: HandleCommandsParams): AcpSpawnTh
export function parseSpawnInput( export function parseSpawnInput(
params: HandleCommandsParams, params: HandleCommandsParams,
tokens: string[], tokens: string[],
): { ok: true; value: ParsedSpawnInput } | { ok: false; error: string } { ): Result<ParsedSpawnInput, string> {
const normalizedTokens = tokens.map((token) => normalizeAcpOptionToken(token)); const normalizedTokens = tokens.map((token) => normalizeAcpOptionToken(token));
let mode: AcpRuntimeSessionMode = "persistent"; let mode: AcpRuntimeSessionMode = "persistent";
let thread = resolveDefaultSpawnThreadMode(params); let thread = resolveDefaultSpawnThreadMode(params);
@@ -323,9 +324,7 @@ export function parseSpawnInput(
}; };
} }
export function parseSteerInput( export function parseSteerInput(tokens: string[]): Result<ParsedSteerInput, string> {
tokens: string[],
): { ok: true; value: ParsedSteerInput } | { ok: false; error: string } {
const normalizedTokens = tokens.map((token) => normalizeAcpOptionToken(token)); const normalizedTokens = tokens.map((token) => normalizeAcpOptionToken(token));
let sessionToken: string | undefined; let sessionToken: string | undefined;
const instructionTokens: string[] = []; const instructionTokens: string[] = [];
@@ -372,7 +371,7 @@ export function parseSteerInput(
export function parseSingleValueCommandInput( export function parseSingleValueCommandInput(
tokens: string[], tokens: string[],
usage: string, usage: string,
): { ok: true; value: ParsedSingleValueCommandInput } | { ok: false; error: string } { ): Result<ParsedSingleValueCommandInput, string> {
const value = normalizeOptionalString(tokens[0]) ?? ""; const value = normalizeOptionalString(tokens[0]) ?? "";
if (!value) { if (!value) {
return { ok: false, error: usage }; return { ok: false, error: usage };
@@ -390,9 +389,7 @@ export function parseSingleValueCommandInput(
}; };
} }
export function parseSetCommandInput( export function parseSetCommandInput(tokens: string[]): Result<ParsedSetCommandInput, string> {
tokens: string[],
): { ok: true; value: ParsedSetCommandInput } | { ok: false; error: string } {
const key = normalizeOptionalString(tokens[0]) ?? ""; const key = normalizeOptionalString(tokens[0]) ?? "";
const value = normalizeOptionalString(tokens[1]) ?? ""; const value = normalizeOptionalString(tokens[1]) ?? "";
if (!key || !value) { if (!key || !value) {
+1 -4
View File
@@ -1,6 +1,7 @@
import { type FSWatcher, readFileSync, watch } from "node:fs"; import { type FSWatcher, readFileSync, watch } from "node:fs";
import { homedir } from "node:os"; import { homedir } from "node:os";
import { isAbsolute, resolve } from "node:path"; import { isAbsolute, resolve } from "node:path";
import { isRecord as isPlainObject } from "@openclaw/normalization-core/record-coerce";
import { createDedupeCache } from "../../infra/dedupe.js"; import { createDedupeCache } from "../../infra/dedupe.js";
import { createSubsystemLogger } from "../../logging/subsystem.js"; import { createSubsystemLogger } from "../../logging/subsystem.js";
import { DEFAULT_USAGE_BAR_TEMPLATE } from "./default-template.js"; import { DEFAULT_USAGE_BAR_TEMPLATE } from "./default-template.js";
@@ -31,10 +32,6 @@ function expandPath(p: string): string {
return isAbsolute(p) ? p : resolve(p); return isAbsolute(p) ? p : resolve(p);
} }
function isPlainObject(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
function hasPieces(value: unknown): boolean { function hasPieces(value: unknown): boolean {
return Array.isArray(value) && value.some(isPlainObject); return Array.isArray(value) && value.some(isPlainObject);
} }
+1 -3
View File
@@ -1,15 +1,13 @@
import { import {
asSafeIntegerInRange, asSafeIntegerInRange,
expectDefined, expectDefined,
isRecord as isObject,
parseStrictInteger, parseStrictInteger,
} from "@openclaw/normalization-core"; } from "@openclaw/normalization-core";
export type UsageBarTemplate = Record<string, unknown>; export type UsageBarTemplate = Record<string, unknown>;
export type UsageContract = Record<string, unknown>; export type UsageContract = Record<string, unknown>;
type Vocab = Record<string, unknown>; type Vocab = Record<string, unknown>;
const isObject = (v: unknown): v is Record<string, unknown> =>
typeof v === "object" && v !== null && !Array.isArray(v);
function toGlyphs(scale: unknown): string[] { function toGlyphs(scale: unknown): string[] {
if (Array.isArray(scale)) { if (Array.isArray(scale)) {
return scale.filter((g): g is string => typeof g === "string"); return scale.filter((g): g is string => typeof g === "string");
+1 -6
View File
@@ -3,6 +3,7 @@
* *
* Reads, writes, migrates, and normalizes direct-message policy and allowFrom fields. * Reads, writes, migrates, and normalizes direct-message policy and allowFrom fields.
*/ */
import { asNullableRecord as asObjectRecord } from "@openclaw/normalization-core/record-coerce";
import { normalizeStringEntries } from "@openclaw/normalization-core/string-normalization"; import { normalizeStringEntries } from "@openclaw/normalization-core/string-normalization";
/** /**
@@ -52,12 +53,6 @@ export function normalizeChannelDmPolicy(value: string | undefined): ChannelDmPo
: undefined; : undefined;
} }
function asObjectRecord(value: unknown): DmAccessRecord | null {
return value && typeof value === "object" && !Array.isArray(value)
? (value as DmAccessRecord)
: null;
}
function cloneDm(entry: DmAccessRecord): DmAccessRecord | null { function cloneDm(entry: DmAccessRecord): DmAccessRecord | null {
const dm = asObjectRecord(entry.dm); const dm = asObjectRecord(entry.dm);
return dm ? { ...dm } : null; return dm ? { ...dm } : null;
+1 -6
View File
@@ -1,5 +1,6 @@
import { expectDefined } from "@openclaw/normalization-core"; import { expectDefined } from "@openclaw/normalization-core";
// Channel streaming config normalization and progress-draft formatting helpers. // Channel streaming config normalization and progress-draft formatting helpers.
import { asNullableRecord as asObjectRecord } from "@openclaw/normalization-core/record-coerce";
import { normalizeOptionalLowercaseString } from "@openclaw/normalization-core/string-coerce"; import { normalizeOptionalLowercaseString } from "@openclaw/normalization-core/string-coerce";
import { normalizeTrimmedStringList } from "@openclaw/normalization-core/string-normalization"; import { normalizeTrimmedStringList } from "@openclaw/normalization-core/string-normalization";
import { import {
@@ -40,12 +41,6 @@ export type { SlackChannelStreamingConfig } from "../config/types.slack.js";
// Runtime reads are nested-only; doctor migrates legacy streaming spellings. // Runtime reads are nested-only; doctor migrates legacy streaming spellings.
function asObjectRecord(value: unknown): Record<string, unknown> | null {
return value && typeof value === "object" && !Array.isArray(value)
? (value as Record<string, unknown>)
: null;
}
function asInteger(value: unknown): number | undefined { function asInteger(value: unknown): number | undefined {
return typeof value === "number" && Number.isInteger(value) ? value : undefined; return typeof value === "number" && Number.isInteger(value) ? value : undefined;
} }
+1 -4
View File
@@ -1,5 +1,6 @@
import fs from "node:fs/promises"; import fs from "node:fs/promises";
import path from "node:path"; import path from "node:path";
import { isRecord as isObjectRecord } from "@openclaw/normalization-core/record-coerce";
import { import {
normalizeLowercaseStringOrEmpty, normalizeLowercaseStringOrEmpty,
normalizeOptionalString, normalizeOptionalString,
@@ -401,10 +402,6 @@ function buildTtsConfigWithHydratedProvider(params: {
return tts; return tts;
} }
function isObjectRecord(value: unknown): value is Record<string, unknown> {
return Boolean(value && typeof value === "object" && !Array.isArray(value));
}
function ttsProviderConfigHasApiKey(value: unknown): boolean { function ttsProviderConfigHasApiKey(value: unknown): boolean {
return isObjectRecord(value) && "apiKey" in value; return isObjectRecord(value) && "apiKey" in value;
} }
+6 -11
View File
@@ -4,6 +4,7 @@ import * as path from "node:path";
import { canonicalizeBase64, estimateBase64DecodedBytes } from "@openclaw/media-core/base64"; import { canonicalizeBase64, estimateBase64DecodedBytes } from "@openclaw/media-core/base64";
import { parseMediaContentLength } from "@openclaw/media-core/content-length"; import { parseMediaContentLength } from "@openclaw/media-core/content-length";
import { toErrorObject } from "../infra/errors.js"; import { toErrorObject } from "../infra/errors.js";
import { cancelUnreadResponseBody } from "../infra/http-body.js";
import { fetchWithSsrFGuard } from "../infra/net/fetch-guard.js"; import { fetchWithSsrFGuard } from "../infra/net/fetch-guard.js";
import { normalizeHostname } from "../infra/net/hostname.js"; import { normalizeHostname } from "../infra/net/hostname.js";
import { resolveCliName } from "./cli-name.js"; import { resolveCliName } from "./cli-name.js";
@@ -81,12 +82,6 @@ type CameraClipPayload = {
hasAudio: boolean; hasAudio: boolean;
}; };
async function cancelIgnoredResponseBody(response: Response | undefined): Promise<void> {
if (response?.bodyUsed !== true) {
await response?.body?.cancel().catch(() => undefined);
}
}
/** Validate and normalize an unknown camera still-image payload. */ /** Validate and normalize an unknown camera still-image payload. */
export function parseCameraSnapPayload(value: unknown): CameraSnapPayload { export function parseCameraSnapPayload(value: unknown): CameraSnapPayload {
const obj = asRecord(value); const obj = asRecord(value);
@@ -170,13 +165,13 @@ async function writeUrlToFile(filePath: string, url: string, opts: { expectedHos
const res = guarded.response; const res = guarded.response;
const finalUrl = new URL(guarded.finalUrl); const finalUrl = new URL(guarded.finalUrl);
if (normalizeHostname(finalUrl.hostname) !== expectedHost) { if (normalizeHostname(finalUrl.hostname) !== expectedHost) {
await cancelIgnoredResponseBody(res); await cancelUnreadResponseBody(res);
throw new Error( throw new Error(
`writeUrlToFile: redirect host ${finalUrl.hostname} must match node host ${opts.expectedHost}`, `writeUrlToFile: redirect host ${finalUrl.hostname} must match node host ${opts.expectedHost}`,
); );
} }
if (!res.ok) { if (!res.ok) {
await cancelIgnoredResponseBody(res); await cancelUnreadResponseBody(res);
throw new Error(`failed to download ${url}: ${res.status} ${res.statusText}`); throw new Error(`failed to download ${url}: ${res.status} ${res.statusText}`);
} }
@@ -184,11 +179,11 @@ async function writeUrlToFile(filePath: string, url: string, opts: { expectedHos
try { try {
contentLength = parseMediaContentLength(res.headers.get("content-length")); contentLength = parseMediaContentLength(res.headers.get("content-length"));
} catch (err) { } catch (err) {
await cancelIgnoredResponseBody(res); await cancelUnreadResponseBody(res);
throw err; throw err;
} }
if (contentLength !== null && contentLength > MAX_CAMERA_URL_DOWNLOAD_BYTES) { if (contentLength !== null && contentLength > MAX_CAMERA_URL_DOWNLOAD_BYTES) {
await cancelIgnoredResponseBody(res); await cancelUnreadResponseBody(res);
throw new Error( throw new Error(
`writeUrlToFile: content-length ${contentLength} exceeds max ${MAX_CAMERA_URL_DOWNLOAD_BYTES}`, `writeUrlToFile: content-length ${contentLength} exceeds max ${MAX_CAMERA_URL_DOWNLOAD_BYTES}`,
); );
@@ -196,7 +191,7 @@ async function writeUrlToFile(filePath: string, url: string, opts: { expectedHos
const body = res.body; const body = res.body;
if (!body) { if (!body) {
await cancelIgnoredResponseBody(res); await cancelUnreadResponseBody(res);
throw new Error(`failed to download ${url}: empty response body`); throw new Error(`failed to download ${url}: empty response body`);
} }
+2 -2
View File
@@ -2,7 +2,7 @@
import { timestampMsToIsoString } from "@openclaw/normalization-core/number-coercion"; import { timestampMsToIsoString } from "@openclaw/normalization-core/number-coercion";
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce"; import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
import { normalizeUniqueStringEntries } from "@openclaw/normalization-core/string-normalization"; import { normalizeUniqueStringEntries } from "@openclaw/normalization-core/string-normalization";
import { truncateUtf16Safe } from "@openclaw/normalization-core/utf16-slice"; import { truncateWithMarker } from "@openclaw/normalization-core/utf16-slice";
import { sanitizeTerminalText } from "../../packages/terminal-core/src/safe-text.js"; import { sanitizeTerminalText } from "../../packages/terminal-core/src/safe-text.js";
import { isRich, theme } from "../../packages/terminal-core/src/theme.js"; import { isRich, theme } from "../../packages/terminal-core/src/theme.js";
import { formatCliCommand } from "../cli/command-format.js"; import { formatCliCommand } from "../cli/command-format.js";
@@ -24,7 +24,7 @@ const STATUS_VALUES = new Set<CommitmentStatus>([
]); ]);
function truncate(value: string, maxChars: number): string { function truncate(value: string, maxChars: number): string {
return value.length <= maxChars ? value : `${truncateUtf16Safe(value, maxChars - 1)}`; return truncateWithMarker(value, maxChars, { marker: "…", reserve: 1, trimEnd: false });
} }
function safe(value: string): string { function safe(value: string): string {
@@ -1,3 +1,4 @@
import { asOptionalRecord as readRecord } from "@openclaw/normalization-core/record-coerce";
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce"; import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
import { import {
createAccountCronScheduledToolPolicy, createAccountCronScheduledToolPolicy,
@@ -36,12 +37,6 @@ export function createScheduledToolPolicyMigrationCollector() {
}; };
} }
function readRecord(value: unknown): Record<string, unknown> | undefined {
return value && typeof value === "object" && !Array.isArray(value)
? (value as Record<string, unknown>)
: undefined;
}
function usesToolRuntime(raw: Record<string, unknown>): boolean { function usesToolRuntime(raw: Record<string, unknown>): boolean {
const payload = readRecord(raw.payload); const payload = readRecord(raw.payload);
const trigger = readRecord(raw.trigger); const trigger = readRecord(raw.trigger);
@@ -1,5 +1,6 @@
import { isDeepStrictEqual } from "node:util"; import { isDeepStrictEqual } from "node:util";
import { normalizeConfiguredProviderCatalogModelId } from "@openclaw/model-catalog-core/provider-model-id-normalization"; import { normalizeConfiguredProviderCatalogModelId } from "@openclaw/model-catalog-core/provider-model-id-normalization";
import { normalizeLowercaseStringOrEmpty as normalizeString } from "@openclaw/normalization-core/string-coerce";
import { splitTrailingAuthProfile } from "../../../agents/model-ref-profile.js"; import { splitTrailingAuthProfile } from "../../../agents/model-ref-profile.js";
import { ensureRecord, getRecord } from "../../../config/legacy.shared.js"; import { ensureRecord, getRecord } from "../../../config/legacy.shared.js";
import { normalizeAgentModelRefForConfig } from "../../../config/model-input.js"; import { normalizeAgentModelRefForConfig } from "../../../config/model-input.js";
@@ -14,10 +15,6 @@ export function hasOwnDefinedProperty(record: Record<string, unknown>, key: stri
return Object.hasOwn(record, key) && record[key] !== undefined; return Object.hasOwn(record, key) && record[key] !== undefined;
} }
function normalizeString(value: unknown): string {
return typeof value === "string" ? value.trim().toLowerCase() : "";
}
function preferredClaudeSeparator(provider: string | undefined): "." | "-" { function preferredClaudeSeparator(provider: string | undefined): "." | "-" {
return provider === "github-copilot" || provider === "copilot-proxy" ? "." : "-"; return provider === "github-copilot" || provider === "copilot-proxy" ? "." : "-";
} }
+1 -6
View File
@@ -1,7 +1,2 @@
// Shared nullable record guard for doctor config walkers. // Shared nullable record guard for doctor config walkers.
export function asObjectRecord(value: unknown): Record<string, unknown> | null { export { asNullableRecord as asObjectRecord } from "@openclaw/normalization-core/record-coerce";
if (!value || typeof value !== "object" || Array.isArray(value)) {
return null;
}
return value as Record<string, unknown>;
}
+2 -2
View File
@@ -1,7 +1,7 @@
/** CLI commands for listing, inspecting, and cancelling TaskFlow records. */ /** CLI commands for listing, inspecting, and cancelling TaskFlow records. */
import { timestampMsToIsoString } from "@openclaw/normalization-core/number-coercion"; import { timestampMsToIsoString } from "@openclaw/normalization-core/number-coercion";
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce"; import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
import { truncateUtf16Safe } from "@openclaw/normalization-core/utf16-slice"; import { truncateUtf16Safe, truncateWithMarker } from "@openclaw/normalization-core/utf16-slice";
import { truncateToVisibleWidth, visibleWidth } from "../../packages/terminal-core/src/ansi.js"; import { truncateToVisibleWidth, visibleWidth } from "../../packages/terminal-core/src/ansi.js";
import { sanitizeTerminalText } from "../../packages/terminal-core/src/safe-text.js"; import { sanitizeTerminalText } from "../../packages/terminal-core/src/safe-text.js";
import { isRich, theme } from "../../packages/terminal-core/src/theme.js"; import { isRich, theme } from "../../packages/terminal-core/src/theme.js";
@@ -38,7 +38,7 @@ function truncate(value: string, maxChars: number) {
if (maxChars <= 1) { if (maxChars <= 1) {
return truncateUtf16Safe(value, maxChars); return truncateUtf16Safe(value, maxChars);
} }
return `${truncateUtf16Safe(value, maxChars - 1)}`; return truncateWithMarker(value, maxChars, { marker: "…", reserve: 1, trimEnd: false });
} }
function safeFlowDisplayText(value: string | undefined, maxChars?: number): string { function safeFlowDisplayText(value: string | undefined, maxChars?: number): string {
@@ -1,6 +1,7 @@
/** Reads persisted generated catalogs without constructing a model registry. */ /** Reads persisted generated catalogs without constructing a model registry. */
import { normalizeProviderId } from "@openclaw/model-catalog-core/provider-id"; import { normalizeProviderId } from "@openclaw/model-catalog-core/provider-id";
import { isRecord } from "@openclaw/normalization-core/record-coerce"; import { isRecord } from "@openclaw/normalization-core/record-coerce";
import { normalizeOptionalString as readString } from "@openclaw/normalization-core/string-coerce";
import type { ModelCatalogEntry, ModelInputType } from "../../agents/model-catalog.types.js"; import type { ModelCatalogEntry, ModelInputType } from "../../agents/model-catalog.types.js";
import { import {
filterGeneratedPluginModelCatalogProviders, filterGeneratedPluginModelCatalogProviders,
@@ -13,10 +14,6 @@ import type { PluginMetadataSnapshot } from "../../plugins/plugin-metadata-snaps
const modelApis = new Set<string>(MODEL_APIS); const modelApis = new Set<string>(MODEL_APIS);
const modelInputs = new Set<ModelInputType>(["text", "image", "audio", "video", "document"]); const modelInputs = new Set<ModelInputType>(["text", "image", "audio", "video", "document"]);
function readString(value: unknown): string | undefined {
return typeof value === "string" && value.trim() ? value.trim() : undefined;
}
function readPositiveNumber(value: unknown): number | undefined { function readPositiveNumber(value: unknown): number | undefined {
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : undefined; return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : undefined;
} }
+1 -4
View File
@@ -4,6 +4,7 @@
* It selects active or requested sessions, renders recent trajectory events, * It selects active or requested sessions, renders recent trajectory events,
* and can follow newly appended SQLite trajectory rows. * and can follow newly appended SQLite trajectory rows.
*/ */
import { normalizeOptionalString as toOptionalString } from "@openclaw/normalization-core/string-coerce";
import { readAcpSessionMeta } from "../acp/runtime/session-meta.js"; import { readAcpSessionMeta } from "../acp/runtime/session-meta.js";
import { getRuntimeConfig } from "../config/config.js"; import { getRuntimeConfig } from "../config/config.js";
import { listSessionEntriesReadOnly } from "../config/sessions/session-accessor.js"; import { listSessionEntriesReadOnly } from "../config/sessions/session-accessor.js";
@@ -79,10 +80,6 @@ function parseTailCount(value: string | number | undefined): number | null {
return parseStrictNonNegativeInteger(value) ?? null; return parseStrictNonNegativeInteger(value) ?? null;
} }
function toOptionalString(value: unknown): string | undefined {
return typeof value === "string" && value.trim() ? value.trim() : undefined;
}
function formatTimestamp(ts: string): string { function formatTimestamp(ts: string): string {
const date = new Date(ts); const date = new Date(ts);
if (Number.isNaN(date.getTime())) { if (Number.isNaN(date.getTime())) {
+4 -9
View File
@@ -1,6 +1,7 @@
// Gateway log-tail helpers for status diagnostics. // Gateway log-tail helpers for status diagnostics.
// Summaries compact repeated auth/runtime failures while preserving enough context for operators. // Summaries compact repeated auth/runtime failures while preserving enough context for operators.
import { safeParseJson } from "@openclaw/normalization-core";
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce"; import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
import { truncateUtf16Safe } from "@openclaw/normalization-core/utf16-slice"; import { truncateUtf16Safe } from "@openclaw/normalization-core/utf16-slice";
import { classifyOAuthRefreshFailureReason } from "../../agents/auth-profiles/oauth-refresh-failure.js"; import { classifyOAuthRefreshFailureReason } from "../../agents/auth-profiles/oauth-refresh-failure.js";
@@ -108,15 +109,9 @@ export function summarizeLogTail(rawLines: string[], opts?: { maxLines?: number
const block = consumeJsonBlock(lines, i); const block = consumeJsonBlock(lines, i);
if (block) { if (block) {
i = block.endIndex; i = block.endIndex;
const parsed = (() => { const parsed = (safeParseJson(block.json) ?? null) as {
try { error?: { code?: string; message?: string };
return JSON.parse(block.json) as { } | null;
error?: { code?: string; message?: string };
};
} catch {
return null;
}
})();
const code = normalizeOptionalString(parsed?.error?.code) ?? null; const code = normalizeOptionalString(parsed?.error?.code) ?? null;
const msg = normalizeOptionalString(parsed?.error?.message) ?? null; const msg = normalizeOptionalString(parsed?.error?.message) ?? null;
const refreshReason = classifyOAuthRefreshFailureReason(msg ?? ""); const refreshReason = classifyOAuthRefreshFailureReason(msg ?? "");
+4 -2
View File
@@ -3,7 +3,7 @@
import { timestampMsToIsoString } from "@openclaw/normalization-core/number-coercion"; import { timestampMsToIsoString } from "@openclaw/normalization-core/number-coercion";
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce"; import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
import { truncateUtf16Safe } from "@openclaw/normalization-core/utf16-slice"; import { truncateWithMarker } from "@openclaw/normalization-core/utf16-slice";
import { sanitizeTerminalText } from "../../packages/terminal-core/src/safe-text.js"; import { sanitizeTerminalText } from "../../packages/terminal-core/src/safe-text.js";
import { isRich, theme } from "../../packages/terminal-core/src/theme.js"; import { isRich, theme } from "../../packages/terminal-core/src/theme.js";
import { formatCliCommand } from "../cli/command-format.js"; import { formatCliCommand } from "../cli/command-format.js";
@@ -206,7 +206,9 @@ function truncate(value: string, maxChars: number) {
if (value.length <= maxChars) { if (value.length <= maxChars) {
return value; return value;
} }
return maxChars <= 0 ? "" : `${truncateUtf16Safe(value, maxChars - 1)}`; return maxChars <= 0
? ""
: truncateWithMarker(value, maxChars, { marker: "…", reserve: 1, trimEnd: false });
} }
function shortToken(value: string | undefined, maxChars = ID_PAD): string { function shortToken(value: string | undefined, maxChars = ID_PAD): string {
+2 -7
View File
@@ -1,10 +1,12 @@
// Normalizes channel config compatibility fields during config loading. // Normalizes channel config compatibility fields during config loading.
import { asNullableRecord as asObjectRecord } from "@openclaw/normalization-core/record-coerce";
import { import {
normalizeLegacyDmAliases, normalizeLegacyDmAliases,
type CompatMutationResult, type CompatMutationResult,
} from "../channels/plugins/dm-access.js"; } from "../channels/plugins/dm-access.js";
export { normalizeLegacyDmAliases }; export { normalizeLegacyDmAliases };
export { asObjectRecord };
export type { CompatMutationResult }; export type { CompatMutationResult };
/** Resolved streaming values a channel doctor supplies while migrating legacy aliases. */ /** Resolved streaming values a channel doctor supplies while migrating legacy aliases. */
@@ -41,13 +43,6 @@ export type RetiredChannelKeyRemoval = {
pathPrefix: string; pathPrefix: string;
}; };
/** Narrows unknown config JSON values to mutable object records. */
export function asObjectRecord(value: unknown): Record<string, unknown> | null {
return value && typeof value === "object" && !Array.isArray(value)
? (value as Record<string, unknown>)
: null;
}
function parseAliasStreamingMode(value: unknown): "off" | "partial" | "block" | "progress" | null { function parseAliasStreamingMode(value: unknown): "off" | "partial" | "block" | "progress" | null {
if (typeof value !== "string") { if (typeof value !== "string") {
return null; return null;
+1 -4
View File
@@ -1,4 +1,5 @@
// Normalizes MCP config records into canonical runtime shape. // Normalizes MCP config records into canonical runtime shape.
import { normalizeLowercaseStringOrEmpty as normalizeMcpString } from "@openclaw/normalization-core/string-coerce";
import { isRecord } from "../utils.js"; import { isRecord } from "../utils.js";
type ConfigMcpServers = Record<string, Record<string, unknown>>; type ConfigMcpServers = Record<string, Record<string, unknown>>;
@@ -11,10 +12,6 @@ const CLI_MCP_TYPE_TO_OPENCLAW_TRANSPORT: Record<string, OpenClawMcpHttpTranspor
stdio: "stdio", stdio: "stdio",
}; };
function normalizeMcpString(value: unknown): string {
return typeof value === "string" ? value.trim().toLowerCase() : "";
}
/** Maps CLI-native MCP type aliases to OpenClaw HTTP transport names. */ /** Maps CLI-native MCP type aliases to OpenClaw HTTP transport names. */
export function resolveOpenClawMcpTransportAlias( export function resolveOpenClawMcpTransportAlias(
value: unknown, value: unknown,
+1 -6
View File
@@ -1,4 +1,5 @@
import { normalizeProviderId } from "@openclaw/model-catalog-core/provider-id"; import { normalizeProviderId } from "@openclaw/model-catalog-core/provider-id";
import { asOptionalRecord as readRecord } from "@openclaw/normalization-core/record-coerce";
import type { ProviderRouteOverridePresence } from "../plugin-sdk/provider-model-types.js"; import type { ProviderRouteOverridePresence } from "../plugin-sdk/provider-model-types.js";
import type { ModelDefinitionConfig, ModelProviderConfig } from "./types.models.js"; import type { ModelDefinitionConfig, ModelProviderConfig } from "./types.models.js";
import type { OpenClawConfig } from "./types.openclaw.js"; import type { OpenClawConfig } from "./types.openclaw.js";
@@ -36,12 +37,6 @@ function normalizeModelId(provider: string, modelId: string): string {
: trimmed; : trimmed;
} }
function readRecord(value: unknown): Record<string, unknown> | undefined {
return value && typeof value === "object" && !Array.isArray(value)
? (value as Record<string, unknown>)
: undefined;
}
function hasNonEmptyRecord(value: unknown): boolean { function hasNonEmptyRecord(value: unknown): boolean {
const record = readRecord(value); const record = readRecord(value);
return record !== undefined && Object.keys(record).length > 0; return record !== undefined && Object.keys(record).length > 0;
+1 -4
View File
@@ -1,3 +1,4 @@
import { normalizeOptionalString as normalizeText } from "@openclaw/normalization-core/string-coerce";
import type { MsgContext } from "../../auto-reply/templating.js"; import type { MsgContext } from "../../auto-reply/templating.js";
import { normalizeChatType } from "../../channels/chat-type.js"; import { normalizeChatType } from "../../channels/chat-type.js";
import { resolveConversationLabel } from "../../channels/conversation-label.js"; import { resolveConversationLabel } from "../../channels/conversation-label.js";
@@ -35,10 +36,6 @@ export type ConversationIdentity = {
metadata?: Record<string, unknown>; metadata?: Record<string, unknown>;
}; };
function normalizeText(value: unknown): string | undefined {
return typeof value === "string" && value.trim() ? value.trim() : undefined;
}
function normalizeThreadId(value: unknown): string | undefined { function normalizeThreadId(value: unknown): string | undefined {
if (typeof value === "number" && Number.isFinite(value)) { if (typeof value === "number" && Number.isFinite(value)) {
return String(value); return String(value);
@@ -1,4 +1,5 @@
import { isDeepStrictEqual } from "node:util"; import { isDeepStrictEqual } from "node:util";
import { normalizeOptionalString as normalizeRunId } from "@openclaw/normalization-core/string-coerce";
import { import {
normalizeDeliveryContext, normalizeDeliveryContext,
type DeliveryContext, type DeliveryContext,
@@ -17,10 +18,6 @@ type RestartRecoveryChannelAuthority = {
sourceTurnId: string; sourceTurnId: string;
}; };
function normalizeRunId(value: unknown): string | undefined {
return typeof value === "string" && value.trim() ? value.trim() : undefined;
}
/** Resolves only a complete durable channel claim; session-route fallbacks carry no authority. */ /** Resolves only a complete durable channel claim; session-route fallbacks carry no authority. */
export function resolveRestartRecoveryChannelAuthority( export function resolveRestartRecoveryChannelAuthority(
entry: SessionEntry, entry: SessionEntry,
+2 -5
View File
@@ -1,6 +1,6 @@
import { truncateWithMarker } from "@openclaw/normalization-core/utf16-slice";
/** Name, agent id, and payload text normalization helpers for cron service ops. */ /** Name, agent id, and payload text normalization helpers for cron service ops. */
import { normalizeOptionalAgentId } from "../../routing/session-key.js"; import { normalizeOptionalAgentId } from "../../routing/session-key.js";
import { truncateUtf16Safe } from "../../utils.js";
import type { CronPayload } from "../types.js"; import type { CronPayload } from "../types.js";
/** Normalizes a required cron job name and throws the public validation error when absent. */ /** Normalizes a required cron job name and throws the public validation error when absent. */
@@ -16,10 +16,7 @@ export function normalizeRequiredName(raw: unknown) {
} }
function truncateText(input: string, maxLen: number) { function truncateText(input: string, maxLen: number) {
if (input.length <= maxLen) { return truncateWithMarker(input, maxLen, { marker: "…", reserve: 1, trimEnd: true });
return input;
}
return `${truncateUtf16Safe(input, Math.max(0, maxLen - 1)).trimEnd()}`;
} }
/** Normalizes optional cron agent ids through the canonical session-key agent id rules. */ /** Normalizes optional cron agent ids through the canonical session-key agent id rules. */
+1 -4
View File
@@ -1,4 +1,5 @@
import { asOptionalRecord } from "@openclaw/normalization-core/record-coerce"; import { asOptionalRecord } from "@openclaw/normalization-core/record-coerce";
import { normalizeLowercaseStringOrEmpty as normalizeErrorSignal } from "@openclaw/normalization-core/string-coerce";
import { isContextOverflowError } from "../agents/embedded-agent-helpers/context-overflow.js"; import { isContextOverflowError } from "../agents/embedded-agent-helpers/context-overflow.js";
import { STREAM_ERROR_FALLBACK_TEXT } from "../agents/stream-message-shared.js"; import { STREAM_ERROR_FALLBACK_TEXT } from "../agents/stream-message-shared.js";
import { import {
@@ -88,10 +89,6 @@ const GATEWAY_ASSISTANT_ERROR_FALLBACK_TEXT = "The agent run failed before produ
const GATEWAY_ASSISTANT_CONTEXT_OVERFLOW_FALLBACK_TEXT = const GATEWAY_ASSISTANT_CONTEXT_OVERFLOW_FALLBACK_TEXT =
"Context overflow: this conversation is too large for the model. Try /compact, use /new to start a fresh session, or retry the command with a tighter output limit."; "Context overflow: this conversation is too large for the model. Try /compact, use /new to start a fresh session, or retry the command with a tighter output limit.";
function normalizeErrorSignal(value: unknown): string {
return typeof value === "string" ? value.trim().toLowerCase() : "";
}
function isContextOverflowErrorSignal(value: unknown): boolean { function isContextOverflowErrorSignal(value: unknown): boolean {
if (typeof value !== "string") { if (typeof value !== "string") {
return false; return false;
@@ -1,6 +1,7 @@
// CLI backend live probe helpers run cron/MCP/image probes through the gateway // CLI backend live probe helpers run cron/MCP/image probes through the gateway
// CLI backend and poll for externally visible live results. // CLI backend and poll for externally visible live results.
import { randomUUID } from "node:crypto"; import { randomUUID } from "node:crypto";
import { asNullableRecord as asLoopbackSchemaRecord } from "@openclaw/normalization-core/record-coerce";
import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce"; import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce";
import { renderCatFacePngBase64 } from "../../test/helpers/live-image-probe.js"; import { renderCatFacePngBase64 } from "../../test/helpers/live-image-probe.js";
import { AUTOMATIONS_TOOL_NAME } from "../agents/tools/automations-tool-name.js"; import { AUTOMATIONS_TOOL_NAME } from "../agents/tools/automations-tool-name.js";
@@ -126,12 +127,6 @@ function parsePositiveInt(value: string | undefined, fallback: number, name: str
return parsed; return parsed;
} }
function asLoopbackSchemaRecord(schema: unknown): Record<string, unknown> | null {
return schema && typeof schema === "object" && !Array.isArray(schema)
? (schema as Record<string, unknown>)
: null;
}
function assertLoopbackObjectSchemasHaveProperties(params: { function assertLoopbackObjectSchemasHaveProperties(params: {
tools: LoopbackToolListEntry[]; tools: LoopbackToolListEntry[];
expectedSchemaProbeToolName?: string; expectedSchemaProbeToolName?: string;
+11 -17
View File
@@ -1,6 +1,7 @@
// Gateway webhook helpers for external hook dispatch into agents and wake flows. // Gateway webhook helpers for external hook dispatch into agents and wake flows.
import { randomUUID } from "node:crypto"; import { randomUUID } from "node:crypto";
import type { IncomingMessage } from "node:http"; import type { IncomingMessage } from "node:http";
import type { Result } from "@openclaw/normalization-core/result";
import { import {
normalizeLowercaseStringOrEmpty, normalizeLowercaseStringOrEmpty,
normalizeOptionalString, normalizeOptionalString,
@@ -175,7 +176,7 @@ export function extractHookToken(req: IncomingMessage): string | undefined {
export async function readJsonBody( export async function readJsonBody(
req: IncomingMessage, req: IncomingMessage,
maxBytes: number, maxBytes: number,
): Promise<{ ok: true; value: unknown } | { ok: false; error: string }> { ): Promise<Result<unknown, string>> {
const result = await readJsonBodyWithLimit(req, { maxBytes, emptyObjectOnEmpty: true }); const result = await readJsonBodyWithLimit(req, { maxBytes, emptyObjectOnEmpty: true });
if (result.ok) { if (result.ok) {
return result; return result;
@@ -209,9 +210,7 @@ export function normalizeHookHeaders(req: IncomingMessage) {
/** Validate a hook wake payload. */ /** Validate a hook wake payload. */
export function normalizeWakePayload( export function normalizeWakePayload(
payload: Record<string, unknown>, payload: Record<string, unknown>,
): ): Result<{ text: string; mode: "now" | "next-heartbeat" }, string> {
| { ok: true; value: { text: string; mode: "now" | "next-heartbeat" } }
| { ok: false; error: string } {
const normalizedText = normalizeOptionalString(payload.text) ?? ""; const normalizedText = normalizeOptionalString(payload.text) ?? "";
if (!normalizedText) { if (!normalizedText) {
return { ok: false, error: "text required" }; return { ok: false, error: "text required" };
@@ -287,12 +286,10 @@ function normalizeHookAgentDelivery(params: {
channel: unknown; channel: unknown;
to: unknown; to: unknown;
accountId: unknown; accountId: unknown;
}): }): Result<
| { Pick<HookAgentPayload, "deliver" | "channel" | "to" | "accountId" | "delivery">,
ok: true; string
value: Pick<HookAgentPayload, "deliver" | "channel" | "to" | "accountId" | "delivery">; > {
}
| { ok: false; error: string } {
const deliver = resolveHookDeliver(params.deliver); const deliver = resolveHookDeliver(params.deliver);
if (!deliver) { if (!deliver) {
return { return {
@@ -449,7 +446,7 @@ export function resolveHookSessionKey(params: {
source: HookSessionKeySource; source: HookSessionKeySource;
sessionKey?: string; sessionKey?: string;
idFactory?: () => string; idFactory?: () => string;
}): { ok: true; value: string } | { ok: false; error: string } { }): Result<string, string> {
const requested = resolveSessionKey(params.sessionKey); const requested = resolveSessionKey(params.sessionKey);
if (requested) { if (requested) {
if ( if (
@@ -526,12 +523,9 @@ export function normalizeHookDispatchSessionKey(params: {
} }
/** Validate and normalize a hook agent payload before policy/session resolution. */ /** Validate and normalize a hook agent payload before policy/session resolution. */
export function normalizeAgentPayload(payload: Record<string, unknown>): export function normalizeAgentPayload(
| { payload: Record<string, unknown>,
ok: true; ): Result<HookAgentPayload, string> {
value: HookAgentPayload;
}
| { ok: false; error: string } {
const message = normalizeOptionalString(payload.message) ?? ""; const message = normalizeOptionalString(payload.message) ?? "";
if (!message) { if (!message) {
return { ok: false, error: "message required" }; return { ok: false, error: "message required" };
+1 -6
View File
@@ -1,4 +1,5 @@
/** Connected node-hosted plugin tools available to agent tool resolution. */ /** Connected node-hosted plugin tools available to agent tool resolution. */
import { asOptionalRecord as normalizeRecord } from "@openclaw/normalization-core/record-coerce";
import type { NodePluginToolDescriptor } from "../../packages/gateway-protocol/src/schema/nodes.js"; import type { NodePluginToolDescriptor } from "../../packages/gateway-protocol/src/schema/nodes.js";
import { NODE_MCP_TOOLS_CALL_COMMAND } from "../infra/node-commands.js"; import { NODE_MCP_TOOLS_CALL_COMMAND } from "../infra/node-commands.js";
import { createSubsystemLogger } from "../logging/subsystem.js"; import { createSubsystemLogger } from "../logging/subsystem.js";
@@ -44,12 +45,6 @@ function normalizeString(value: unknown): string {
return typeof value === "string" ? value.trim() : ""; return typeof value === "string" ? value.trim() : "";
} }
function normalizeRecord(value: unknown): Record<string, unknown> | undefined {
return value && typeof value === "object" && !Array.isArray(value)
? (value as Record<string, unknown>)
: undefined;
}
function defaultParameters(): Record<string, unknown> { function defaultParameters(): Record<string, unknown> {
return { type: "object", properties: {}, additionalProperties: true }; return { type: "object", properties: {}, additionalProperties: true };
} }
@@ -1,4 +1,5 @@
// Transcript persistence and source-reply rewrites shared by chat send and abort. // Transcript persistence and source-reply rewrites shared by chat send and abort.
import { asOptionalRecord as transcriptEventRecord } from "@openclaw/normalization-core/record-coerce";
import { getReplyPayloadMetadata } from "../../auto-reply/reply-payload.js"; import { getReplyPayloadMetadata } from "../../auto-reply/reply-payload.js";
import { import {
findTranscriptEvent, findTranscriptEvent,
@@ -67,12 +68,6 @@ export function assistantTranscriptScope(
}; };
} }
function transcriptEventRecord(event: TranscriptEvent): Record<string, unknown> | undefined {
return event && typeof event === "object" && !Array.isArray(event)
? (event as Record<string, unknown>)
: undefined;
}
function transcriptEventId(event: TranscriptEvent): string | undefined { function transcriptEventId(event: TranscriptEvent): string | undefined {
const id = transcriptEventRecord(event)?.id; const id = transcriptEventRecord(event)?.id;
return typeof id === "string" && id.trim().length > 0 ? id : undefined; return typeof id === "string" && id.trim().length > 0 ? id : undefined;
+2 -5
View File
@@ -1,6 +1,7 @@
// Host directory browsing for the new-session folder picker. operator.admin // Host directory browsing for the new-session folder picker. operator.admin
// only (see core-descriptors): listing arbitrary host paths carries the same // only (see core-descriptors): listing arbitrary host paths carries the same
// trust as starting a session with an explicit cwd. // trust as starting a session with an explicit cwd.
import { safeParseJson } from "@openclaw/normalization-core";
import { import {
ErrorCodes, ErrorCodes,
errorShape, errorShape,
@@ -14,11 +15,7 @@ import type { GatewayRequestHandlers } from "./types.js";
function parseNodePayload(payload: unknown, payloadJSON?: string | null): unknown { function parseNodePayload(payload: unknown, payloadJSON?: string | null): unknown {
if (payloadJSON) { if (payloadJSON) {
try { return safeParseJson(payloadJSON);
return JSON.parse(payloadJSON) as unknown;
} catch {
return undefined;
}
} }
return payload; return payload;
} }
@@ -1,6 +1,7 @@
// Model list result building resolves visible model catalogs for an agent and // Model list result building resolves visible model catalogs for an agent and
// strips runtime-only provider params before sending the browse API payload. // strips runtime-only provider params before sending the browse API payload.
import { normalizeProviderId } from "@openclaw/model-catalog-core/provider-id"; import { normalizeProviderId } from "@openclaw/model-catalog-core/provider-id";
import { asPositiveSafeInteger as resolvePositiveSafeInteger } from "@openclaw/normalization-core/number-coercion";
import { import {
resolveAgentEffectiveModelPrimary, resolveAgentEffectiveModelPrimary,
resolveAgentWorkspaceDir, resolveAgentWorkspaceDir,
@@ -79,10 +80,6 @@ function resolveModelsListView(params: Record<string, unknown>): ModelsListView
return view === "configured" || view === "provider-config" || view === "all" ? view : "default"; return view === "configured" || view === "provider-config" || view === "all" ? view : "default";
} }
function resolvePositiveSafeInteger(value: unknown): number | undefined {
return typeof value === "number" && Number.isSafeInteger(value) && value > 0 ? value : undefined;
}
// Project explicitly onto the public protocol shape. Concrete route, base URL, // Project explicitly onto the public protocol shape. Concrete route, base URL,
// auth, and cost facts stay private; runtime intent is attached separately. // auth, and cost facts stay private; runtime intent is attached separately.
function buildPublicModelProjection(entry: ModelCatalogEntry): ModelsListEntry { function buildPublicModelProjection(entry: ModelCatalogEntry): ModelsListEntry {
+2 -5
View File
@@ -1,3 +1,4 @@
import { safeParseJson } from "@openclaw/normalization-core";
import { import {
GATEWAY_CLIENT_CAPS, GATEWAY_CLIENT_CAPS,
hasGatewayClientCap, hasGatewayClientCap,
@@ -68,11 +69,7 @@ function parseNodePayload(payload: unknown, payloadJSON?: string | null): unknow
if (!payloadJSON) { if (!payloadJSON) {
return payload; return payload;
} }
try { return safeParseJson(payloadJSON);
return JSON.parse(payloadJSON) as unknown;
} catch {
return undefined;
}
} }
async function stageNodeTerminalUpload( async function stageNodeTerminalUpload(
+1 -4
View File
@@ -1,5 +1,6 @@
// Gateway session lifecycle state projection. // Gateway session lifecycle state projection.
// Converts agent run lifecycle events into session row/store status updates. // Converts agent run lifecycle events into session row/store status updates.
import { normalizeOptionalString as normalizeLifecycleRunId } from "@openclaw/normalization-core/string-coerce";
import { truncateUtf16Safe } from "@openclaw/normalization-core/utf16-slice"; import { truncateUtf16Safe } from "@openclaw/normalization-core/utf16-slice";
import type { SessionRunStatus } from "../../packages/gateway-protocol/src/schema/sessions-row.js"; import type { SessionRunStatus } from "../../packages/gateway-protocol/src/schema/sessions-row.js";
import { isAgentLifecycleYieldedWaiting } from "../agents/agent-lifecycle-parent-state.js"; import { isAgentLifecycleYieldedWaiting } from "../agents/agent-lifecycle-parent-state.js";
@@ -245,10 +246,6 @@ export function deriveGatewaySessionLifecycleProjectionPatch(params: {
return patch; return patch;
} }
function normalizeLifecycleRunId(value: unknown): string | undefined {
return typeof value === "string" && value.trim() ? value.trim() : undefined;
}
export function isRestartRecoveryLifecycleEvent(params: { export function isRestartRecoveryLifecycleEvent(params: {
entry?: Pick<SessionEntry, "restartRecoveryRuns"> | null; entry?: Pick<SessionEntry, "restartRecoveryRuns"> | null;
event: Pick<LifecycleEventLike, "runId" | "lifecycleGeneration" | "data">; event: Pick<LifecycleEventLike, "runId" | "lifecycleGeneration" | "data">;
+1 -4
View File
@@ -2,6 +2,7 @@
// Apple Watch cannot use generic WebSockets on-device, so node events use bounded HTTPS polls. // Apple Watch cannot use generic WebSockets on-device, so node events use bounded HTTPS polls.
import { randomBytes, randomUUID } from "node:crypto"; import { randomBytes, randomUUID } from "node:crypto";
import type { IncomingMessage, ServerResponse } from "node:http"; import type { IncomingMessage, ServerResponse } from "node:http";
import { isRecord as isStringRecord } from "@openclaw/normalization-core/record-coerce";
import { import {
GATEWAY_CLIENT_IDS, GATEWAY_CLIENT_IDS,
GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_MODES,
@@ -174,10 +175,6 @@ function resolveWatchClientAddress(
}; };
} }
function isStringRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
function trackResponseLifecycle(res: ServerResponse): ResponseLifecycle { function trackResponseLifecycle(res: ServerResponse): ResponseLifecycle {
let aborted = false; let aborted = false;
let settled = false; let settled = false;
+2 -5
View File
@@ -1,6 +1,7 @@
// Hook workspace helpers resolve hook roots and workspace-local hook files. // Hook workspace helpers resolve hook roots and workspace-local hook files.
import fs from "node:fs"; import fs from "node:fs";
import path from "node:path"; import path from "node:path";
import { safeParseJson } from "@openclaw/normalization-core";
import { normalizeTrimmedStringList } from "@openclaw/normalization-core/string-normalization"; import { normalizeTrimmedStringList } from "@openclaw/normalization-core/string-normalization";
import { MANIFEST_KEY } from "../compat/legacy-names.js"; import { MANIFEST_KEY } from "../compat/legacy-names.js";
import type { OpenClawConfig } from "../config/types.openclaw.js"; import type { OpenClawConfig } from "../config/types.openclaw.js";
@@ -44,11 +45,7 @@ function readHookPackageManifest(dir: string): HookPackageManifest | null {
if (raw === null) { if (raw === null) {
return null; return null;
} }
try { return (safeParseJson(raw) as HookPackageManifest | undefined) ?? null;
return JSON.parse(raw) as HookPackageManifest;
} catch {
return null;
}
} }
function resolvePackageHooks(manifest: HookPackageManifest): string[] { function resolvePackageHooks(manifest: HookPackageManifest): string[] {
+1 -4
View File
@@ -1,5 +1,6 @@
// Resolves the LAN host OpenClaw should advertise to nearby devices. // Resolves the LAN host OpenClaw should advertise to nearby devices.
import { isRfc1918Ipv4Address } from "@openclaw/net-policy/ip"; import { isRfc1918Ipv4Address } from "@openclaw/net-policy/ip";
import { normalizeLowercaseStringOrEmpty as normalizeInterfaceName } from "@openclaw/normalization-core/string-coerce";
import { runCommandWithTimeout as defaultRunCommandWithTimeout } from "../process/exec.js"; import { runCommandWithTimeout as defaultRunCommandWithTimeout } from "../process/exec.js";
import { import {
listExternalInterfaceAddresses, listExternalInterfaceAddresses,
@@ -56,10 +57,6 @@ type RankedWindowsRouteRow = {
order: number; order: number;
}; };
function normalizeInterfaceName(name: unknown): string {
return typeof name === "string" ? name.trim().toLowerCase() : "";
}
function normalizeMetric(value: unknown): number { function normalizeMetric(value: unknown): number {
if (typeof value === "number" && Number.isFinite(value)) { if (typeof value === "number" && Number.isFinite(value)) {
return value; return value;
+1 -6
View File
@@ -1,4 +1,5 @@
// Shared owner-qualified ClawHub security verdict resolution. // Shared owner-qualified ClawHub security verdict resolution.
import { asOptionalRecord as readObject } from "@openclaw/normalization-core/record-coerce";
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce"; import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
import pLimit from "p-limit"; import pLimit from "p-limit";
import { import {
@@ -76,12 +77,6 @@ function partitionCompatibleBatches(
return batches.map((batch) => batch.items); return batches.map((batch) => batch.items);
} }
function readObject(value: unknown): Record<string, unknown> | undefined {
return value && typeof value === "object" && !Array.isArray(value)
? (value as Record<string, unknown>)
: undefined;
}
function readOptionalStringField(value: unknown, field: string): string | undefined { function readOptionalStringField(value: unknown, field: string): string | undefined {
return normalizeOptionalString(readObject(value)?.[field]); return normalizeOptionalString(readObject(value)?.[field]);
} }
+1 -4
View File
@@ -4,6 +4,7 @@ import fs from "node:fs/promises";
import os from "node:os"; import os from "node:os";
import path from "node:path"; import path from "node:path";
import { resolveTimerTimeoutMs } from "@openclaw/normalization-core/number-coercion"; import { resolveTimerTimeoutMs } from "@openclaw/normalization-core/number-coercion";
import { isRecord as isJsonObject } from "@openclaw/normalization-core/record-coerce";
import { import {
normalizeLowercaseStringOrEmpty, normalizeLowercaseStringOrEmpty,
normalizeOptionalString, normalizeOptionalString,
@@ -852,10 +853,6 @@ function createClawHubBodyLimitError(
); );
} }
function isJsonObject(value: unknown): value is Record<string, unknown> {
return Boolean(value && typeof value === "object" && !Array.isArray(value));
}
function optionalStringField( function optionalStringField(
source: Record<string, unknown>, source: Record<string, unknown>,
field: string, field: string,
+32
View File
@@ -2,6 +2,7 @@
import { MAX_TIMER_TIMEOUT_MS } from "@openclaw/normalization-core/number-coercion"; import { MAX_TIMER_TIMEOUT_MS } from "@openclaw/normalization-core/number-coercion";
import { beforeEach, describe, expect, it, vi } from "vitest"; import { beforeEach, describe, expect, it, vi } from "vitest";
import { import {
cancelUnreadResponseBody,
readResponseTextPrefix, readResponseTextPrefix,
readResponseTextSnippet, readResponseTextSnippet,
readResponseWithLimit, readResponseWithLimit,
@@ -103,6 +104,37 @@ async function expectReadResponseWithLimitFailureCase(params: {
).rejects.toThrow(params.expectedError); ).rejects.toThrow(params.expectedError);
} }
describe("cancelUnreadResponseBody", () => {
it("cancels unread bodies and ignores cancellation failures", async () => {
const cancel = vi.fn(() => {
throw new Error("already closed");
});
const response = new Response(makeStallingStream([], cancel));
await expect(cancelUnreadResponseBody(response)).resolves.toBeUndefined();
expect(cancel).toHaveBeenCalledOnce();
});
it("leaves consumed and absent bodies alone", async () => {
const cancel = vi.fn();
const response = new Response(
new ReadableStream<Uint8Array>({
start(controller) {
controller.enqueue(new TextEncoder().encode("done"));
controller.close();
},
cancel,
}),
);
await response.text();
await cancelUnreadResponseBody(response);
await cancelUnreadResponseBody(undefined);
expect(cancel).not.toHaveBeenCalled();
});
});
describe("readResponseWithLimit", () => { describe("readResponseWithLimit", () => {
beforeEach(() => { beforeEach(() => {
vi.useRealTimers(); vi.useRealTimers();
+7
View File
@@ -10,6 +10,13 @@ import { parseStrictNonNegativeInteger } from "./parse-finite-number.js";
export { readChunkWithIdleTimeout } from "./http-response-body-timeout.js"; export { readChunkWithIdleTimeout } from "./http-response-body-timeout.js";
/** Cancels a response body only when no consumer has started reading it. */
export async function cancelUnreadResponseBody(response: Response | undefined): Promise<void> {
if (response && !response.bodyUsed) {
await response.body?.cancel().catch(() => undefined);
}
}
export const DEFAULT_WEBHOOK_MAX_BODY_BYTES = 1024 * 1024; export const DEFAULT_WEBHOOK_MAX_BODY_BYTES = 1024 * 1024;
export const DEFAULT_WEBHOOK_BODY_TIMEOUT_MS = 30_000; export const DEFAULT_WEBHOOK_BODY_TIMEOUT_MS = 30_000;
@@ -3,6 +3,7 @@
import fs from "node:fs/promises"; import fs from "node:fs/promises";
import path from "node:path"; import path from "node:path";
import { isPassThroughRemoteMediaSource } from "@openclaw/media-core/media-source-url"; import { isPassThroughRemoteMediaSource } from "@openclaw/media-core/media-source-url";
import { hasNonEmptyString as isNonEmptyMediaSource } from "@openclaw/normalization-core/string-coerce";
import type { ReplyPayload } from "../../auto-reply/types.js"; import type { ReplyPayload } from "../../auto-reply/types.js";
import { resolveDeliveryQueueMediaDir } from "../../config/paths.js"; import { resolveDeliveryQueueMediaDir } from "../../config/paths.js";
import { import {
@@ -38,10 +39,6 @@ function resolveArtifactExtension(source: string): string {
return ARTIFACT_EXT_RE.test(extension) ? extension.toLowerCase() : ""; return ARTIFACT_EXT_RE.test(extension) ? extension.toLowerCase() : "";
} }
function isNonEmptyMediaSource(source: unknown): source is string {
return typeof source === "string" && Boolean(source.trim());
}
function payloadMediaSources(payload: ReplyPayload): string[] { function payloadMediaSources(payload: ReplyPayload): string[] {
const sources: string[] = []; const sources: string[] = [];
if (isNonEmptyMediaSource(payload.mediaUrl)) { if (isNonEmptyMediaSource(payload.mediaUrl)) {
+1 -6
View File
@@ -1,5 +1,6 @@
// Message-action runner normalizes tool params, resolves channel/target/media, // Message-action runner normalizes tool params, resolves channel/target/media,
// applies policies, and dispatches send/poll/plugin actions. // applies policies, and dispatches send/poll/plugin actions.
import { asOptionalRecord as asResultRecord } from "@openclaw/normalization-core/record-coerce";
import { import {
normalizeOptionalLowercaseString, normalizeOptionalLowercaseString,
normalizeOptionalString, normalizeOptionalString,
@@ -284,12 +285,6 @@ export function getToolResult(
return "toolResult" in result ? result.toolResult : undefined; return "toolResult" in result ? result.toolResult : undefined;
} }
function asResultRecord(value: unknown): Record<string, unknown> | undefined {
return value && typeof value === "object" && !Array.isArray(value)
? (value as Record<string, unknown>)
: undefined;
}
function withSendNormalization( function withSendNormalization(
result: MessageActionRunResult, result: MessageActionRunResult,
normalization?: MessageActionNormalization, normalization?: MessageActionNormalization,
+3 -3
View File
@@ -3,9 +3,9 @@ import { asFiniteNumber } from "@openclaw/normalization-core/number-coercion";
import { isRecord } from "@openclaw/normalization-core/record-coerce"; import { isRecord } from "@openclaw/normalization-core/record-coerce";
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce"; import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
import { readProviderJsonResponse } from "../agents/provider-http-errors.js"; import { readProviderJsonResponse } from "../agents/provider-http-errors.js";
import { cancelUnreadResponseBody } from "./http-body.js";
import { import {
buildUsageHttpErrorSnapshot, buildUsageHttpErrorSnapshot,
discardUsageResponseBody,
fetchJson, fetchJson,
parseUsageResetAt, parseUsageResetAt,
readUsageJson, readUsageJson,
@@ -159,7 +159,7 @@ async function fetchClaudeWebUsage(
fetchFn, fetchFn,
); );
if (!orgRes.ok) { if (!orgRes.ok) {
await discardUsageResponseBody(orgRes); await cancelUnreadResponseBody(orgRes);
return null; return null;
} }
@@ -180,7 +180,7 @@ async function fetchClaudeWebUsage(
fetchFn, fetchFn,
); );
if (!usageRes.ok) { if (!usageRes.ok) {
await discardUsageResponseBody(usageRes); await cancelUnreadResponseBody(usageRes);
return null; return null;
} }
+2 -2
View File
@@ -1,9 +1,9 @@
// Fetches Codex provider usage windows. // Fetches Codex provider usage windows.
import { resolveProviderRequestHeaders } from "../agents/provider-request-config.js"; import { resolveProviderRequestHeaders } from "../agents/provider-request-config.js";
import { cancelUnreadResponseBody } from "./http-body.js";
import { parseStrictFiniteNumber } from "./parse-finite-number.js"; import { parseStrictFiniteNumber } from "./parse-finite-number.js";
import { import {
buildUsageHttpErrorSnapshot, buildUsageHttpErrorSnapshot,
discardUsageResponseBody,
fetchJson, fetchJson,
readUsageJson, readUsageJson,
} from "./provider-usage.fetch.shared.js"; } from "./provider-usage.fetch.shared.js";
@@ -89,7 +89,7 @@ export async function fetchCodexUsage(
); );
if (!res.ok) { if (!res.ok) {
await discardUsageResponseBody(res); await cancelUnreadResponseBody(res);
return buildUsageHttpErrorSnapshot({ return buildUsageHttpErrorSnapshot({
provider: "openai", provider: "openai",
status: res.status, status: res.status,
+2 -2
View File
@@ -1,8 +1,8 @@
// Fetches and normalizes DeepSeek provider usage records. // Fetches and normalizes DeepSeek provider usage records.
import { isRecord } from "@openclaw/normalization-core/record-coerce"; import { isRecord } from "@openclaw/normalization-core/record-coerce";
import { cancelUnreadResponseBody } from "./http-body.js";
import { import {
buildUsageHttpErrorSnapshot, buildUsageHttpErrorSnapshot,
discardUsageResponseBody,
fetchJson, fetchJson,
parseFiniteNumber, parseFiniteNumber,
readUsageJson, readUsageJson,
@@ -75,7 +75,7 @@ export async function fetchDeepSeekUsage(
); );
if (!res.ok) { if (!res.ok) {
await discardUsageResponseBody(res); await cancelUnreadResponseBody(res);
return buildUsageHttpErrorSnapshot({ return buildUsageHttpErrorSnapshot({
provider: "deepseek", provider: "deepseek",
status: res.status, status: res.status,
+2 -2
View File
@@ -2,9 +2,9 @@ import { expectDefined } from "@openclaw/normalization-core";
import { isRecord } from "@openclaw/normalization-core/record-coerce"; import { isRecord } from "@openclaw/normalization-core/record-coerce";
// Fetches Gemini provider usage windows. // Fetches Gemini provider usage windows.
import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce"; import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce";
import { cancelUnreadResponseBody } from "./http-body.js";
import { import {
buildUsageHttpErrorSnapshot, buildUsageHttpErrorSnapshot,
discardUsageResponseBody,
fetchJson, fetchJson,
readUsageJson, readUsageJson,
} from "./provider-usage.fetch.shared.js"; } from "./provider-usage.fetch.shared.js";
@@ -36,7 +36,7 @@ export async function fetchGeminiUsage(
); );
if (!res.ok) { if (!res.ok) {
await discardUsageResponseBody(res); await cancelUnreadResponseBody(res);
return buildUsageHttpErrorSnapshot({ return buildUsageHttpErrorSnapshot({
provider, provider,
status: res.status, status: res.status,
+2 -2
View File
@@ -4,9 +4,9 @@ import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/st
import { readProviderJsonResponse } from "../agents/provider-http-errors.js"; import { readProviderJsonResponse } from "../agents/provider-http-errors.js";
import { isRecord } from "../utils.js"; import { isRecord } from "../utils.js";
import { readTrimmedStringAlias } from "../utils/string-readers.js"; import { readTrimmedStringAlias } from "../utils/string-readers.js";
import { cancelUnreadResponseBody } from "./http-body.js";
import { import {
buildUsageHttpErrorSnapshot, buildUsageHttpErrorSnapshot,
discardUsageResponseBody,
fetchJson, fetchJson,
parseFiniteNumber, parseFiniteNumber,
} from "./provider-usage.fetch.shared.js"; } from "./provider-usage.fetch.shared.js";
@@ -544,7 +544,7 @@ export async function fetchMinimaxUsage(
); );
if (!res.ok) { if (!res.ok) {
await discardUsageResponseBody(res); await cancelUnreadResponseBody(res);
return buildUsageHttpErrorSnapshot({ return buildUsageHttpErrorSnapshot({
provider: "minimax", provider: "minimax",
status: res.status, status: res.status,
@@ -5,7 +5,6 @@ import { withFetchPreconnect } from "../test-utils/fetch-mock.js";
import { import {
buildUsageErrorSnapshot, buildUsageErrorSnapshot,
buildUsageHttpErrorSnapshot, buildUsageHttpErrorSnapshot,
discardUsageResponseBody,
fetchJson, fetchJson,
parseFiniteNumber, parseFiniteNumber,
readUsageJson, readUsageJson,
@@ -159,15 +158,6 @@ describe("provider usage fetch shared helpers", () => {
expect(timeoutSpy).toHaveBeenCalledWith(MAX_TIMER_TIMEOUT_MS); expect(timeoutSpy).toHaveBeenCalledWith(MAX_TIMER_TIMEOUT_MS);
}); });
it("cancels unread response bodies when discarding usage responses", async () => {
const response = new Response("not needed", { status: 429 });
const cancel = vi.spyOn(response.body!, "cancel").mockResolvedValue(undefined);
await discardUsageResponseBody(response);
expect(cancel).toHaveBeenCalledOnce();
});
it("maps configured status codes to token expired", () => { it("maps configured status codes to token expired", () => {
const snapshot = buildUsageHttpErrorSnapshot({ const snapshot = buildUsageHttpErrorSnapshot({
provider: "openai", provider: "openai",
-6
View File
@@ -23,12 +23,6 @@ export async function fetchJson(
return await fetchFn(url, { ...init, signal }); return await fetchFn(url, { ...init, signal });
} }
export async function discardUsageResponseBody(response: Response): Promise<void> {
if (!response.bodyUsed) {
await response.body?.cancel().catch(() => undefined);
}
}
export function parseFiniteNumber(value: unknown): number | undefined { export function parseFiniteNumber(value: unknown): number | undefined {
return parseFiniteNumberish(value); return parseFiniteNumberish(value);
} }
+2 -2
View File
@@ -2,9 +2,9 @@
import { asFiniteNumber } from "@openclaw/normalization-core/number-coercion"; import { asFiniteNumber } from "@openclaw/normalization-core/number-coercion";
import { isRecord } from "@openclaw/normalization-core/record-coerce"; import { isRecord } from "@openclaw/normalization-core/record-coerce";
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce"; import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
import { cancelUnreadResponseBody } from "./http-body.js";
import { import {
buildUsageHttpErrorSnapshot, buildUsageHttpErrorSnapshot,
discardUsageResponseBody,
fetchJson, fetchJson,
parseUsageResetAt, parseUsageResetAt,
readUsageJson, readUsageJson,
@@ -80,7 +80,7 @@ export async function fetchZaiUsage(
); );
if (!res.ok) { if (!res.ok) {
await discardUsageResponseBody(res); await cancelUnreadResponseBody(res);
return buildUsageHttpErrorSnapshot({ return buildUsageHttpErrorSnapshot({
provider: "zai", provider: "zai",
status: res.status, status: res.status,
+2 -5
View File
@@ -1,4 +1,5 @@
import type { DatabaseSync } from "node:sqlite"; import type { DatabaseSync } from "node:sqlite";
import { safeParseJson } from "@openclaw/normalization-core";
import { isRecord as isPlainRecord } from "@openclaw/normalization-core/record-coerce"; import { isRecord as isPlainRecord } from "@openclaw/normalization-core/record-coerce";
import type { DB as OpenClawStateKyselyDatabase } from "../state/openclaw-state-db.generated.js"; import type { DB as OpenClawStateKyselyDatabase } from "../state/openclaw-state-db.generated.js";
import { import {
@@ -356,11 +357,7 @@ function parseRequiredJson(value: string | null): unknown {
if (value === null) { if (value === null) {
return undefined; return undefined;
} }
try { return safeParseJson(value);
return JSON.parse(value) as unknown;
} catch {
return undefined;
}
} }
function decodeRestartSentinelRow(row: { function decodeRestartSentinelRow(row: {

Some files were not shown because too many files have changed in this diff Show More