refactor: reuse canonical record guards (#115810)

* docs(agents): standardize canonical record guards

* refactor(model-catalog): use canonical record guard

* refactor(scripts): use canonical record guard

* refactor(ui): use canonical record guard

* refactor(codex): use canonical record guard

* refactor(agents): use canonical record guard

* refactor(sessions): use canonical record guard

* refactor(doctor): use canonical record guard

* refactor(infra): use canonical record guard
This commit is contained in:
Vincent Koc
2026-07-29 11:43:36 +02:00
committed by GitHub
parent 1464da8edd
commit 25c375e47f
11 changed files with 15 additions and 42 deletions
+1
View File
@@ -73,6 +73,7 @@ Skills own workflows; root owns hard policy and routing.
- Tests may use observed examples, but prod literals need a short contract reason.
- Compatibility is opt-in. "Shipped" means reachable from a release Git tag; main/GitHub/PR/unreleased code is not shipped.
- Refactor default: one canonical path. Delete the old path unless user explicitly wants compat or the shipped public contract is obvious and cited.
- Reuse the canonical non-array record guard instead of adding local `isRecord` copies. Core, UI, scripts with workspace package resolution, and packages that already depend on normalization-core import `@openclaw/normalization-core/record-coerce`; plugins import `openclaw/plugin-sdk/string-coerce-runtime`. Keep a local guard only when the semantics intentionally differ or the file must remain dependency-free, browser-serialized, generated, or runnable outside workspace package resolution.
- Core runtime consumes only current canonical shapes/config/data. Legacy or retired shapes normalize only in doctor/migration code before runtime; no runtime shims, aliases, or fallback readers.
- State/storage migrations are database-first. Runtime reads/writes the canonical store only. Old file stores, sidecars, aliases, and fallback readers belong in `openclaw doctor --fix` migration code only, never steady-state runtime.
- Storage default: SQLite only. Do not add JSON/JSONL/TXT/sidecar files for OpenClaw-owned runtime state, caches, queues, registries, indexes, cursors, checkpoints, or plugin scratch data.
@@ -1,5 +1,6 @@
import { Buffer } from "node:buffer";
import type { AgentMessage } from "openclaw/plugin-sdk/agent-harness-runtime";
import { isRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
import type { JsonValue } from "./protocol.js";
import { readUpstreamUserText } from "./upstream-prompt-provenance.js";
@@ -17,10 +18,6 @@ type ProjectedMessageGroup = {
bytes: number;
};
function isRecord(value: unknown): value is Record<string, unknown> {
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
}
function readNonEmptyString(value: unknown): string | undefined {
return typeof value === "string" ? value.trim() || undefined : undefined;
}
@@ -1,9 +1,5 @@
// Collects configured model references from OpenClaw config-shaped objects.
/** Narrow unknown values to plain records. */
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
import { isRecord } from "@openclaw/normalization-core/record-coerce";
/** One configured model reference plus its config path. */
export type ConfiguredModelRef = {
@@ -1,4 +1,5 @@
// Model Catalog Core helper module supports model catalog normalize behavior.
import { isRecord } from "@openclaw/normalization-core/record-coerce";
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
import {
normalizeOptionalTrimmedStringList,
@@ -43,11 +44,6 @@ const MODEL_CATALOG_API_SET = new Set<string>(MODEL_CATALOG_APIS);
const DEFAULT_MODEL_INPUT: ModelCatalogInput[] = ["text"];
const DEFAULT_MODEL_STATUS: ModelCatalogStatus = "available";
/** Narrow unknown catalog payloads to plain records. */
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
/** Reject object keys that can mutate prototypes when copied into records. */
function isBlockedObjectKey(key: string): boolean {
return key === "__proto__" || key === "prototype" || key === "constructor";
+1 -4
View File
@@ -11,6 +11,7 @@ import {
clampTimerTimeoutMs,
finiteSecondsToTimerSafeMilliseconds,
} from "@openclaw/normalization-core/number-coercion";
import { isRecord } from "@openclaw/normalization-core/record-coerce";
import prettyMilliseconds from "pretty-ms";
import {
die,
@@ -568,10 +569,6 @@ function readString(value: unknown): string {
return typeof value === "string" ? value : "";
}
function isRecord(value: unknown): value is Record<string, unknown> {
return value !== null && typeof value === "object" && !Array.isArray(value);
}
export function parseRegistryPackageMetadata(raw: string): {
gitHead: string;
tarball: string;
+1 -4
View File
@@ -1,4 +1,5 @@
/** Pure repair rules for OpenClaw-generated plugin model catalogs. */
import { isRecord } from "@openclaw/normalization-core/record-coerce";
export const PLUGIN_MODEL_CATALOG_GENERATED_BY = "openclaw-plugin-model-catalog-v1";
@@ -7,10 +8,6 @@ type PluginModelCatalogRepair = {
removedModelCount: number;
};
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
function hasCatalogApi(value: unknown): boolean {
return typeof value === "string" && value.length > 0;
}
+1 -4
View File
@@ -1,6 +1,7 @@
// Doctor migration for config and state left by the retired Phone Control lease model.
import fs from "node:fs/promises";
import path from "node:path";
import { isRecord } from "@openclaw/normalization-core/record-coerce";
import { resolveStateDir } from "../config/paths.js";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import { createPluginStateKeyedStore } from "../plugin-state/plugin-state-store.js";
@@ -77,10 +78,6 @@ async function inspectStatePath(filePath: string, label: string): Promise<StateP
}
}
function isRecord(value: unknown): value is Record<string, unknown> {
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
}
function isStringArray(value: unknown): boolean {
return (
Array.isArray(value) && value.every((entry) => typeof entry === "string" && entry.trim() !== "")
+2 -4
View File
@@ -1,4 +1,6 @@
// Transcript tree helpers keep append-only leaf controls consistent across readers.
import { isRecord } from "@openclaw/normalization-core/record-coerce";
type TranscriptRecord = Record<string, unknown>;
type SessionTranscriptTreeEntry = {
@@ -25,10 +27,6 @@ export type SessionTranscriptTree<T> = {
hasInvalidLeafControl: boolean;
};
function isRecord(value: unknown): value is TranscriptRecord {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
function readNonEmptyString(value: unknown): string | undefined {
return typeof value === "string" && value.trim().length > 0 ? value : undefined;
}
@@ -2,6 +2,7 @@ import { createHash } from "node:crypto";
import fs from "node:fs";
import path from "node:path";
import type { DatabaseSync } from "node:sqlite";
import { isRecord } from "@openclaw/normalization-core/record-coerce";
import {
decodeSessionArchiveBytes,
encodeSessionArchiveContent,
@@ -84,10 +85,6 @@ type ArchiveSourceSnapshot = {
size: number;
};
function isRecord(value: unknown): value is Record<string, unknown> {
return Boolean(value && typeof value === "object" && !Array.isArray(value));
}
function transformTranscriptEvent(event: TranscriptEvent): {
changed: boolean;
event: TranscriptEvent;
+1 -4
View File
@@ -1,4 +1,5 @@
// Control UI module owns transient operator question state.
import { isRecord } from "@openclaw/normalization-core/record-coerce";
import type {
Question,
QuestionAnswers,
@@ -55,10 +56,6 @@ type QuestionAnswerValues = Record<string, string[]>;
const REFRESH_RETRY_DELAYS_MS = [1_000, 2_000, 4_000] as const;
function isRecord(value: unknown): value is Record<string, unknown> {
return Boolean(value && typeof value === "object" && !Array.isArray(value));
}
function readNonEmptyString(value: unknown): string | null {
if (typeof value !== "string") {
return null;
+4 -4
View File
@@ -1,3 +1,7 @@
import { isRecord } from "@openclaw/normalization-core/record-coerce";
export { isRecord };
export function formatError(error: unknown): string {
if (error instanceof Error && error.message.trim()) {
return error.message;
@@ -10,7 +14,3 @@ export function formatError(error: unknown): string {
}
return "Unknown workboard error.";
}
export function isRecord(value: unknown): value is Record<string, unknown> {
return Boolean(value && typeof value === "object" && !Array.isArray(value));
}