From aaa509b26e43e2d98303e399a671692604bb0ef3 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 15 Aug 2026 01:32:04 -0700 Subject: [PATCH] refactor(types): remove chained type assertions in core and ui (#124073) * refactor(types): remove chained assertions in core and ui * fix(types): preserve legacy cron migration identity * fix(types): preserve settings patch key types --- packages/acp-core/src/runtime/errors.ts | 6 +- packages/agent-core/src/agent-loop.ts | 18 ++++-- packages/ai/src/providers/anthropic.ts | 5 +- .../transports/anthropic-compaction-replay.ts | 62 +++++++++---------- .../openai-completions-transport.ts | 5 +- .../src/transports/openai-responses-client.ts | 5 +- .../openai-responses-compaction-replay.ts | 38 +++++++----- ...enai-responses-replay-messages-internal.ts | 5 +- .../openai-responses-stream-internal.ts | 6 +- packages/markdown-core/src/ir.ts | 2 +- src/cli/cron-cli/shared.ts | 13 ++-- src/cli/devices-cli.runtime.ts | 4 +- src/commands/doctor-session-sqlite-readers.ts | 26 +++++--- .../doctor-session-state-providers.ts | 18 +++--- .../doctor/cron/legacy-store-migration.ts | 29 ++++----- src/commands/doctor/cron/repair-plan.ts | 6 +- .../doctor/cron/runtime-policy-migration.ts | 4 +- src/config/sessions/plugin-host-cleanup.ts | 6 +- .../session-accessor.entry-mutation.ts | 6 +- .../session-accessor.sqlite-session-row.ts | 7 +-- src/config/sessions/store-migrations.ts | 26 ++++---- src/config/sessions/transcript.ts | 15 ++--- src/plugin-sdk/agent-core.ts | 6 +- src/plugin-sdk/webhook-targets.ts | 4 +- ui/src/app/server-prefs-state.ts | 23 ++++--- ui/src/app/settings.ts | 3 +- ui/src/pages/chat/chat-state-refresh.ts | 8 +-- ui/src/pages/chat/components/widget-card.ts | 8 +-- ui/src/pages/plugins/icon-loader.ts | 4 +- 29 files changed, 205 insertions(+), 163 deletions(-) diff --git a/packages/acp-core/src/runtime/errors.ts b/packages/acp-core/src/runtime/errors.ts index 6691fc85f347..5a22385859c2 100644 --- a/packages/acp-core/src/runtime/errors.ts +++ b/packages/acp-core/src/runtime/errors.ts @@ -128,12 +128,12 @@ export function formatAcpErrorChain(error: unknown): string { return redactSensitiveText(String(error)); } const segments: string[] = [renderSingleError(error)]; - let current: unknown = (error as unknown as { cause?: unknown }).cause; + let current: unknown = error.cause; let depth = 0; while (current !== undefined && current !== null && depth < 8) { if (current instanceof Error) { segments.push(renderSingleError(current)); - current = (current as unknown as { cause?: unknown }).cause; + current = current.cause; } else { segments.push(stringifyNonErrorCause(current)); current = undefined; @@ -144,7 +144,7 @@ export function formatAcpErrorChain(error: unknown): string { } function renderSingleError(error: Error): string { - const codeValue = (error as unknown as { code?: unknown }).code; + const codeValue = "code" in error ? error.code : undefined; const codeSuffix = typeof codeValue === "string" || typeof codeValue === "number" ? ` [${codeValue}]` : ""; return `${error.name}${codeSuffix}: ${error.message}`; diff --git a/packages/agent-core/src/agent-loop.ts b/packages/agent-core/src/agent-loop.ts index 53c3844eae0b..ddeff9f37431 100644 --- a/packages/agent-core/src/agent-loop.ts +++ b/packages/agent-core/src/agent-loop.ts @@ -1873,8 +1873,17 @@ type TurnTaintMetadata = { }; function readTurnTaintMetadata(message: AgentMessage): TurnTaintMetadata | undefined { - const metadata = (message as unknown as Record)["__openclaw"]; - return asOptionalRecord(metadata) as TurnTaintMetadata | undefined; + const metadata = Reflect.get(message, "__openclaw"); + const record = asOptionalRecord(metadata); + if (!record) { + return undefined; + } + return { + ...(record.resultContentSource === "network" + ? { resultContentSource: record.resultContentSource } + : {}), + ...(record.turnTainted === true ? { turnTainted: true } : {}), + }; } function toolResultTaintsTurn(message: ToolResultMessage): boolean { @@ -1898,10 +1907,11 @@ function withAssistantTurnTaint(message: AssistantMessage, tainted: boolean): As if (!tainted) { return message; } - return { + const taintedMessage = { ...message, __openclaw: { ...readTurnTaintMetadata(message), turnTainted: true }, - } as unknown as AssistantMessage; + } satisfies AssistantMessage & { __openclaw: TurnTaintMetadata }; + return taintedMessage; } function withToolResultContentSource( diff --git a/packages/ai/src/providers/anthropic.ts b/packages/ai/src/providers/anthropic.ts index 01f2ac529be9..7363f96e161e 100644 --- a/packages/ai/src/providers/anthropic.ts +++ b/packages/ai/src/providers/anthropic.ts @@ -9,6 +9,7 @@ import type { RawMessageStreamEvent, TextBlockParam, } from "@anthropic-ai/sdk/resources/messages.js"; +import { isRecord } from "@openclaw/normalization-core/record-coerce"; import { getEnvApiKey } from "../env-api-keys.js"; import { getAiTransportHost, resolveAiTransportHeaderSentinels } from "../host.js"; import { @@ -448,7 +449,7 @@ export const streamAnthropic: StreamFunction<"anthropic-messages", AnthropicComp // and allowing the thinking-block recovery retry to fire. eventSink.push({ type: "start", partial: output }); } else if (event.type === "content_block_start") { - const rawContentBlock = event.content_block as unknown as Record; + const rawContentBlock = isRecord(event.content_block) ? event.content_block : undefined; if ( requestOptions?.anthropicServerCompaction === true && compactionCapture.begin(event.index, rawContentBlock, output.content.length) @@ -566,7 +567,7 @@ export const streamAnthropic: StreamFunction<"anthropic-messages", AnthropicComp }); } } else if (event.type === "content_block_delta") { - const rawDelta = event.delta as unknown as Record; + const rawDelta = isRecord(event.delta) ? event.delta : undefined; if (compactionCapture.delta(event.index, rawDelta)) { continue; } else if (event.delta.type === "text_delta") { diff --git a/packages/ai/src/transports/anthropic-compaction-replay.ts b/packages/ai/src/transports/anthropic-compaction-replay.ts index 90e967466bf3..bcfbc4f7d86c 100644 --- a/packages/ai/src/transports/anthropic-compaction-replay.ts +++ b/packages/ai/src/transports/anthropic-compaction-replay.ts @@ -1,4 +1,5 @@ import type { AssistantMessage, Context, Model, ProviderReplayState } from "@openclaw/llm-core"; +import { isRecord } from "@openclaw/normalization-core/record-coerce"; import { shortHash } from "../utils/hash.js"; const ANTHROPIC_COMPACTION_REPLAY_TYPE = "anthropic-compaction"; @@ -85,40 +86,39 @@ function buildAnthropicReplayContext(model: Model, options?: ReplayOpts): Anthro }; } +function isAnthropicCompactionState( + state: Record, +): state is Record & + (AnthropicCompactionReplayState | AnthropicCompactionSuppressionState) { + if ( + state.v !== 1 || + typeof state.data !== "string" || + typeof state.provider !== "string" || + typeof state.api !== "string" || + typeof state.model !== "string" || + typeof state.baseUrlHash !== "string" || + (state.sessionHash !== undefined && typeof state.sessionHash !== "string") || + (state.authProfileHash !== undefined && typeof state.authProfileHash !== "string") + ) { + return false; + } + if (state.type === ANTHROPIC_COMPACTION_SUPPRESSION_TYPE) { + return state.data === ANTHROPIC_COMPACTION_SUPPRESSION_DATA; + } + return ( + state.type === ANTHROPIC_COMPACTION_REPLAY_TYPE && + state.data.length > 0 && + (state.replayIndex === undefined || + (typeof state.replayIndex === "number" && + Number.isSafeInteger(state.replayIndex) && + state.replayIndex >= 0)) + ); +} + function readAnthropicCompactionState( value: unknown, ): AnthropicCompactionReplayState | AnthropicCompactionSuppressionState | undefined { - if (!value || typeof value !== "object" || Array.isArray(value)) { - return undefined; - } - const state = value as Record; - const validEnvelope = - state.v === 1 && - typeof state.data === "string" && - typeof state.provider === "string" && - typeof state.api === "string" && - typeof state.model === "string" && - typeof state.baseUrlHash === "string" && - (state.sessionHash === undefined || typeof state.sessionHash === "string") && - (state.authProfileHash === undefined || typeof state.authProfileHash === "string"); - if (!validEnvelope) { - return undefined; - } - const replayState = state as unknown as - | AnthropicCompactionReplayState - | AnthropicCompactionSuppressionState; - if (replayState.type === ANTHROPIC_COMPACTION_SUPPRESSION_TYPE) { - return replayState.data === ANTHROPIC_COMPACTION_SUPPRESSION_DATA ? replayState : undefined; - } - if ( - replayState.type !== ANTHROPIC_COMPACTION_REPLAY_TYPE || - replayState.data.length === 0 || - (replayState.replayIndex !== undefined && - (!Number.isSafeInteger(replayState.replayIndex) || replayState.replayIndex < 0)) - ) { - return undefined; - } - return replayState; + return isRecord(value) && isAnthropicCompactionState(value) ? value : undefined; } function replayContextMatches( diff --git a/packages/ai/src/transports/openai-completions-transport.ts b/packages/ai/src/transports/openai-completions-transport.ts index 1e7ae4f48cc8..e93ff87a2c41 100644 --- a/packages/ai/src/transports/openai-completions-transport.ts +++ b/packages/ai/src/transports/openai-completions-transport.ts @@ -34,6 +34,7 @@ import { import { failTransportStream, finalizeTransportStream, + type WritableTransportStream, withProviderResponseHook, } from "./transport-stream-shared.js"; @@ -173,7 +174,7 @@ function buildOpenAICompletionsClientConfig( export function createOpenAICompletionsTransportStreamFn(): StreamFn { return (model, context, options) => { const eventStream = createAssistantMessageEventStream(); - const stream = eventStream as unknown as { push(event: unknown): void; end(): void }; + const stream = eventStream as unknown as WritableTransportStream; void (async () => { const output: MutableAssistantOutput = { role: "assistant" as const, @@ -293,6 +294,6 @@ export function createOpenAICompletionsTransportStreamFn(): StreamFn { firstEventAbort?.dispose(); } })(); - return eventStream as unknown as ReturnType; + return eventStream; }; } diff --git a/packages/ai/src/transports/openai-responses-client.ts b/packages/ai/src/transports/openai-responses-client.ts index 39c3159ce84b..c0b75c8465a1 100644 --- a/packages/ai/src/transports/openai-responses-client.ts +++ b/packages/ai/src/transports/openai-responses-client.ts @@ -76,6 +76,7 @@ import { sanitizeResponsesImagePayload } from "./responses-image-payload-sanitiz import { mergeTransportMetadata, transportAbortError, + type WritableTransportStream, withProviderResponseHook, } from "./transport-stream-shared.js"; import { redactIdentifier } from "./transport-utils.js"; @@ -240,7 +241,7 @@ function createResponsesTransportExecutor(config: ResponsesTransportExecutorOpti const responsesOptions = options as OpenAIResponsesOptions | undefined; const compactRequest = claimResponsesCompactRequest(responsesOptions); const eventStream = createAssistantMessageEventStream(); - const stream = eventStream as unknown as { push(event: unknown): void; end(): void }; + const stream = eventStream as unknown as WritableTransportStream; void (async () => { const output = createOpenAIResponsesAssistantOutput(model, config.outputApi); let firstEventAbort: ReturnType | undefined; @@ -590,7 +591,7 @@ function createResponsesTransportExecutor(config: ResponsesTransportExecutorOpti firstEventAbort?.dispose(); } })(); - return eventStream as unknown as ReturnType; + return eventStream; }; } diff --git a/packages/ai/src/transports/openai-responses-compaction-replay.ts b/packages/ai/src/transports/openai-responses-compaction-replay.ts index 9a17ff11d0f4..76c3ce9615ad 100644 --- a/packages/ai/src/transports/openai-responses-compaction-replay.ts +++ b/packages/ai/src/transports/openai-responses-compaction-replay.ts @@ -62,29 +62,35 @@ export function isOpenAIResponsesReplayContext( ); } -function readOpenAIResponsesCompactionReplayState( - value: unknown, -): OpenAIResponsesCompactionReplayState | OpenAIResponsesCompactionSuppressionState | undefined { - if ( - !isOpenAIResponsesReplayContext(value) || - typeof value.baseUrlHash !== "string" || - (value as Record).v !== 1 - ) { - return undefined; +function isOpenAIResponsesCompactionState( + state: OpenAIResponsesReplayContext & Record, +): state is Record & + (OpenAIResponsesCompactionReplayState | OpenAIResponsesCompactionSuppressionState) { + if (typeof state.baseUrlHash !== "string" || state.v !== 1) { + return false; } - const state = value as OpenAIResponsesReplayContext & Record; if (state.type === OPENAI_RESPONSES_COMPACTION_SUPPRESSION_TYPE) { - return state.data === OPENAI_RESPONSES_COMPACTION_SUPPRESSION_DATA - ? (state as unknown as OpenAIResponsesCompactionSuppressionState) - : undefined; + return state.data === OPENAI_RESPONSES_COMPACTION_SUPPRESSION_DATA; } - return state.type === OPENAI_RESPONSES_COMPACTION_REPLAY_TYPE && + return ( + state.type === OPENAI_RESPONSES_COMPACTION_REPLAY_TYPE && typeof state.data === "string" && state.data.length > 0 && (state.id === undefined || typeof state.id === "string") && (state.replayIndex === undefined || - (Number.isSafeInteger(state.replayIndex) && (state.replayIndex as number) >= 0)) - ? (state as unknown as OpenAIResponsesCompactionReplayState) + (typeof state.replayIndex === "number" && + Number.isSafeInteger(state.replayIndex) && + state.replayIndex >= 0)) + ); +} + +function readOpenAIResponsesCompactionReplayState( + value: unknown, +): OpenAIResponsesCompactionReplayState | OpenAIResponsesCompactionSuppressionState | undefined { + return isRecord(value) && + isOpenAIResponsesReplayContext(value) && + isOpenAIResponsesCompactionState(value) + ? value : undefined; } diff --git a/packages/ai/src/transports/openai-responses-replay-messages-internal.ts b/packages/ai/src/transports/openai-responses-replay-messages-internal.ts index 5d5f4d9ea147..64d0d5cb68f5 100644 --- a/packages/ai/src/transports/openai-responses-replay-messages-internal.ts +++ b/packages/ai/src/transports/openai-responses-replay-messages-internal.ts @@ -1,4 +1,5 @@ import type { Api, AssistantMessage, Context, Model } from "@openclaw/llm-core"; +import { isRecord } from "@openclaw/normalization-core/record-coerce"; import type { ResponseFunctionCallOutputItemList, ResponseInput, @@ -369,9 +370,7 @@ function convertResponsesMessagesWithStyle( const replayableReasoningItem = prepareOpenAIResponsesReasoningItemForReplay( reasoningItem, replayContext, - readOpenAIResponsesReasoningReplayBlockMetadata( - block as unknown as Record, - ), + readOpenAIResponsesReasoningReplayBlockMetadata(isRecord(block) ? block : {}), providerStyle ? { preserveUnattributedEncryptedContent: true } : undefined, ); if (!shouldReplayResponsesItemIds) { diff --git a/packages/ai/src/transports/openai-responses-stream-internal.ts b/packages/ai/src/transports/openai-responses-stream-internal.ts index 64b42d3b0fa8..9d61d6f7f2d0 100644 --- a/packages/ai/src/transports/openai-responses-stream-internal.ts +++ b/packages/ai/src/transports/openai-responses-stream-internal.ts @@ -1,3 +1,4 @@ +import { isRecord } from "@openclaw/normalization-core/record-coerce"; import type { ResponseCreateParamsStreaming, ResponseOutputItem, @@ -705,10 +706,7 @@ export async function processResponsesStream( event.message ? `Error Code ${event.code}: ${event.message}` : "Unknown error", ); } else if (event.type === "response.failed") { - const failure = normalizeResponsesFailedEvent( - event as unknown as Record, - model, - ); + const failure = normalizeResponsesFailedEvent(isRecord(event) ? event : {}, model); finalizeFailedResponse(event.response, failure.responseId); throw new ResponsesStreamFailure(failure, event.response); } diff --git a/packages/markdown-core/src/ir.ts b/packages/markdown-core/src/ir.ts index 6e9b5f8ca922..c5d53c5eb36e 100644 --- a/packages/markdown-core/src/ir.ts +++ b/packages/markdown-core/src/ir.ts @@ -1578,7 +1578,7 @@ export function markdownToIRWithMeta( assistantTranscriptRolePreserveLinks: options.assistantTranscriptRoleHeaders === true, }; const md = createMarkdownIt(options); - const tokens = md.parse(source, env as unknown as object); + const tokens = md.parse(source, env); const tableMode = options.tableMode ?? "off"; diff --git a/src/cli/cron-cli/shared.ts b/src/cli/cron-cli/shared.ts index 7108cb0d5ab8..73b253898b44 100644 --- a/src/cli/cron-cli/shared.ts +++ b/src/cli/cron-cli/shared.ts @@ -3,6 +3,7 @@ import { resolveExpiresAtMsFromDurationMs, timestampMsToIsoString, } from "@openclaw/normalization-core/number-coercion"; +import { asOptionalRecord } from "@openclaw/normalization-core/record-coerce"; import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce"; import { truncateToVisibleWidth, visibleWidth } from "../../../packages/terminal-core/src/ansi.js"; import { sanitizeTerminalText } from "../../../packages/terminal-core/src/safe-text.js"; @@ -148,7 +149,7 @@ export function enrichCronJsonWithStatus(value: unknown): unknown { // Single job object (has 'state' and 'enabled') if ("state" in obj && "enabled" in obj) { - return { ...obj, status: computeStatus(obj as unknown as CronJob) }; + return { ...obj, status: computeStatus(obj) }; } // List response (has 'jobs' array) @@ -163,15 +164,19 @@ export function enrichCronJsonWithStatus(value: unknown): unknown { return value; } -function computeStatus(job: CronJob): string { +function computeStatus(job: { enabled?: unknown; state?: unknown }): string { if (!job.enabled) { return "disabled"; } - const state = job.state ?? {}; + const state = asOptionalRecord(job.state) ?? {}; if (state.runningAtMs) { return "running"; } - return state.lastRunStatus ?? state.lastStatus ?? "idle"; + return typeof state.lastRunStatus === "string" + ? state.lastRunStatus + : typeof state.lastStatus === "string" + ? state.lastStatus + : "idle"; } // Human-facing decoration only: enrichCronJsonWithStatus() emits computeStatus() diff --git a/src/cli/devices-cli.runtime.ts b/src/cli/devices-cli.runtime.ts index ae9c78d60786..385aa240f805 100644 --- a/src/cli/devices-cli.runtime.ts +++ b/src/cli/devices-cli.runtime.ts @@ -361,8 +361,8 @@ function assertLocalFallbackMatchesGatewayRequest( function redactLocalPairedDevice(device: InfraPairedDevice): PairedDevice { const { tokens, ...rest } = device; return { - ...(rest as unknown as PairedDevice), - tokens: summarizeDeviceTokens(tokens) as DeviceTokenSummary[] | undefined, + ...rest, + tokens: summarizeDeviceTokens(tokens), }; } diff --git a/src/commands/doctor-session-sqlite-readers.ts b/src/commands/doctor-session-sqlite-readers.ts index ed181c46025f..6a16021b5985 100644 --- a/src/commands/doctor-session-sqlite-readers.ts +++ b/src/commands/doctor-session-sqlite-readers.ts @@ -117,7 +117,9 @@ function readTranscriptEventsForImport( id: sessionId, type: "session", version: plan.sourceVersion, - } as unknown as FileEntry; + timestamp: "", + cwd: "", + } satisfies FileEntry; // V1 compactions refer to original row indexes. Stable index-derived IDs let // the second pass resolve those links without retaining the transcript. const idPrefix = createHash("sha256") @@ -144,14 +146,15 @@ function readTranscriptEventsForImport( let event = loadedEvent; let recognizedEvent: FileEntry | undefined; if (originalIndex === plan.headerIndex) { - const legacyHeader = event as unknown as Record; - const canonicalHeader: Record = { - ...legacyHeader, + const canonicalHeader = { + ...event, id: sessionId, - type: "session", + type: "session" as const, + timestamp: typeof event.timestamp === "string" ? event.timestamp : "", + cwd: "cwd" in event && typeof event.cwd === "string" ? event.cwd : "", }; - delete canonicalHeader.sessionId; - event = canonicalHeader as unknown as FileEntry; + Reflect.deleteProperty(canonicalHeader, "sessionId"); + event = canonicalHeader; recognizedEvent = event; } else { // Reuse the runtime partition contract one row at a time. The @@ -482,7 +485,14 @@ function parseSqliteSessionEntry(entryJson: string): SessionEntry | undefined { try { const parsed = JSON.parse(entryJson) as unknown; return isRecord(parsed) && typeof parsed.sessionId === "string" - ? (parsed as unknown as SessionEntry) + ? { + ...parsed, + sessionId: parsed.sessionId, + updatedAt: + typeof parsed.updatedAt === "number" && Number.isFinite(parsed.updatedAt) + ? parsed.updatedAt + : 0, + } : undefined; } catch { return undefined; diff --git a/src/commands/doctor-session-state-providers.ts b/src/commands/doctor-session-state-providers.ts index ba5992ba97fd..72cc18fe6bba 100644 --- a/src/commands/doctor-session-state-providers.ts +++ b/src/commands/doctor-session-state-providers.ts @@ -1,3 +1,4 @@ +import { isRecord } from "@openclaw/normalization-core/record-coerce"; /** Doctor repair for stale plugin-owned routing state persisted in session entries. */ import { normalizeOptionalString as normalizeString } from "@openclaw/normalization-core/string-coerce"; import { normalizeStringEntriesLower } from "@openclaw/normalization-core/string-normalization"; @@ -113,7 +114,10 @@ function entryMayContainPluginSessionRouteState(sessionKey: string, entry: Sessi if (isValidAgentHarnessSessionStoreEntry(sessionKey, entry)) { return false; } - const record = entry as unknown as Record; + if (!isRecord(entry)) { + return false; + } + const record = entry; return ( normalizeString(record.providerOverride) !== undefined || normalizeString(record.modelOverride) !== undefined || @@ -511,7 +515,9 @@ export async function runPluginSessionStateDoctorRepairs(params: { } routeByAgentId.set(agentId, route); } - scanStore[sessionKey] = entry as unknown as Record; + if (isRecord(entry)) { + scanStore[sessionKey] = entry; + } routes[sessionKey] = route; } if (Object.keys(scanStore).length === 0) { @@ -537,14 +543,10 @@ export async function runPluginSessionStateDoctorRepairs(params: { const repairedAt = Date.now(); const repairsByKey = new Map(repairs.map((repair) => [repair.key, repair])); await updateLegacySessionStore(params.absoluteStorePath, (currentStore) => { - const currentMutableStore = currentStore as unknown as Record< - string, - Record - >; for (const [key, repair] of repairsByKey) { - const current = currentMutableStore[key]; + const current = currentStore[key]; if ( - current && + isRecord(current) && applySessionRouteStateRepair({ sessionKey: key, entry: current, diff --git a/src/commands/doctor/cron/legacy-store-migration.ts b/src/commands/doctor/cron/legacy-store-migration.ts index 765f88136660..b61019cd4c79 100644 --- a/src/commands/doctor/cron/legacy-store-migration.ts +++ b/src/commands/doctor/cron/legacy-store-migration.ts @@ -434,20 +434,20 @@ function hasInlineState(jobs: Array | null | undefined>) ); } -function ensureJobStateObject(job: CronStoreFile["jobs"][number]): void { +function ensureJobStateObject(job: Record): void { if (!isRecord(job.state)) { - job.state = {} as never; + job.state = {}; } } -function backfillMissingRuntimeFields(job: CronStoreFile["jobs"][number]): void { +function backfillMissingRuntimeFields(job: Record): void { ensureJobStateObject(job); if (typeof job.updatedAtMs !== "number") { job.updatedAtMs = typeof job.createdAtMs === "number" ? job.createdAtMs : Date.now(); } } -function resolveUpdatedAtMs(job: CronStoreFile["jobs"][number], updatedAtMs: unknown): number { +function resolveUpdatedAtMs(job: Record, updatedAtMs: unknown): number { if (typeof updatedAtMs === "number" && Number.isFinite(updatedAtMs)) { return updatedAtMs; } @@ -459,20 +459,21 @@ function resolveUpdatedAtMs(job: CronStoreFile["jobs"][number], updatedAtMs: unk : Date.now(); } -function mergeStateFileEntry(job: CronStoreFile["jobs"][number], entry: unknown): void { +function mergeStateFileEntry(job: Record, entry: unknown): void { if (!isRecord(entry)) { backfillMissingRuntimeFields(job); return; } job.updatedAtMs = resolveUpdatedAtMs(job, entry.updatedAtMs); - job.state = isRecord(entry.state) ? (entry.state as never) : ({} as never); + job.state = isRecord(entry.state) ? entry.state : {}; if ( typeof entry.scheduleIdentity === "string" && - entry.scheduleIdentity !== - tryLegacyCronScheduleIdentity(job as unknown as Record) + entry.scheduleIdentity !== tryLegacyCronScheduleIdentity(job) ) { ensureJobStateObject(job); - job.state.nextRunAtMs = undefined; + if (isRecord(job.state)) { + job.state.nextRunAtMs = undefined; + } } } @@ -595,7 +596,7 @@ export async function loadLegacyCronStoreForMigration( version: 1, jobs: configRows as never as CronStoreFile["jobs"], }; - const jobs = store.jobs as unknown as Array>; + const jobs = configRows; const configJobs = cloneConfigJobs(configRows); const statePath = resolveLegacyCronStatePath(resolvedStorePath); @@ -604,8 +605,8 @@ export async function loadLegacyCronStoreForMigration( const hasLegacyInlineState = !stateFile && hasInlineState(jobs); if (stateFile) { - for (const job of store.jobs) { - const stateId = resolveCronStateId(job as unknown as Record); + for (const job of jobs) { + const stateId = resolveCronStateId(job); const entry = stateId ? stateFile.jobs[stateId] : undefined; configJobRuntimeEntries.push(isRecord(entry) ? structuredClone(entry) : {}); if (entry) { @@ -615,12 +616,12 @@ export async function loadLegacyCronStoreForMigration( } } } else if (!hasLegacyInlineState) { - for (const job of store.jobs) { + for (const job of jobs) { backfillMissingRuntimeFields(job); } } - for (const job of store.jobs) { + for (const job of jobs) { ensureJobStateObject(job); } diff --git a/src/commands/doctor/cron/repair-plan.ts b/src/commands/doctor/cron/repair-plan.ts index a55f19470552..6fe6c2e27750 100644 --- a/src/commands/doctor/cron/repair-plan.ts +++ b/src/commands/doctor/cron/repair-plan.ts @@ -1,5 +1,6 @@ // Cron doctor repair planning helpers for previewing and merging legacy rows. import { isDeepStrictEqual } from "node:util"; +import { isRecord } from "@openclaw/normalization-core/record-coerce"; import { normalizeOptionalStringifiedId } from "../../../../packages/normalization-core/src/string-coerce.js"; import { normalizeCronJobInput } from "../../../cron/normalize.js"; import type { CronJob } from "../../../cron/types.js"; @@ -231,7 +232,10 @@ export function needsSqliteProjectionBackfill(params: { if (!normalizedConfig) { return true; } - const projected = params.projectedJob as unknown as Record; + if (!isRecord(params.projectedJob)) { + return true; + } + const projected = params.projectedJob; for (const field of [ "agentId", "deleteAfterRun", diff --git a/src/commands/doctor/cron/runtime-policy-migration.ts b/src/commands/doctor/cron/runtime-policy-migration.ts index 8e3f1e9fd451..e2e536b06b5e 100644 --- a/src/commands/doctor/cron/runtime-policy-migration.ts +++ b/src/commands/doctor/cron/runtime-policy-migration.ts @@ -1,5 +1,5 @@ // Doctor-only runtime policy repair for migrated cron Codex model refs. -import { asOptionalRecord } from "@openclaw/normalization-core/record-coerce"; +import { asOptionalRecord, isRecord } from "@openclaw/normalization-core/record-coerce"; import { tryResolveDefaultAgentId } from "../../../agents/agent-scope-config.js"; import type { OpenClawConfig } from "../../../config/types.openclaw.js"; import { normalizeAgentId } from "../../../routing/session-key.js"; @@ -26,7 +26,7 @@ function resolvePolicyOwner(params: { cfg: OpenClawConfig; target: CronCodexRuntimePolicyTarget; }): { owner: MutableRecord; path: string } | undefined { - const root = params.cfg as unknown as MutableRecord; + const root = isRecord(params.cfg) ? params.cfg : {}; const agents = ensureRecord(root, "agents"); const requestedAgentId = params.target.agentId ? normalizeAgentId(params.target.agentId) diff --git a/src/config/sessions/plugin-host-cleanup.ts b/src/config/sessions/plugin-host-cleanup.ts index 0db1dd49c2e9..df56a7e55f66 100644 --- a/src/config/sessions/plugin-host-cleanup.ts +++ b/src/config/sessions/plugin-host-cleanup.ts @@ -71,9 +71,8 @@ function clearPromotedSessionEntrySlots( options.includeStoredSlotKeys === false && sessionEntrySlotKeys ? new Set(sessionEntrySlotKeys) : collectPromotedSessionEntrySlotKeys(entry, pluginId, sessionEntrySlotKeys); - const entryRecord = entry as unknown as Record; for (const slotKey of slotKeys) { - delete entryRecord[slotKey]; + Reflect.deleteProperty(entry, slotKey); } if (!options.pruneSlotOwnership || !entry.pluginExtensionSlotKeys) { return; @@ -152,9 +151,8 @@ function hasPromotedSessionEntrySlot( if (slotKeys.size === 0) { return false; } - const entryRecord = entry as unknown as Record; for (const slotKey of slotKeys) { - if (Object.hasOwn(entryRecord, slotKey)) { + if (Object.hasOwn(entry, slotKey)) { return true; } } diff --git a/src/config/sessions/session-accessor.entry-mutation.ts b/src/config/sessions/session-accessor.entry-mutation.ts index c8f4e92cb795..21de5f594e6e 100644 --- a/src/config/sessions/session-accessor.entry-mutation.ts +++ b/src/config/sessions/session-accessor.entry-mutation.ts @@ -328,13 +328,13 @@ export async function markSessionAbortTarget(params: { scope: SessionAccessScope; now?: () => number; }): Promise { - let resolvedTarget: SessionAbortTargetResult | null = null; + const resolution: { target: SessionAbortTargetResult | null } = { target: null }; try { const sessionKey = normalizeStoreSessionKey(params.scope.sessionKey); const updated = await patchSessionEntryCore( params.scope, (currentEntry) => { - resolvedTarget = { + resolution.target = { entry: { ...currentEntry }, persisted: false, sessionId: currentEntry.sessionId, @@ -368,7 +368,7 @@ export async function markSessionAbortTarget(params: { } : null; } catch (error) { - const fallbackTarget = resolvedTarget as unknown as SessionAbortTargetResult | null; + const fallbackTarget = resolution.target; if (fallbackTarget) { return { entry: fallbackTarget.entry, diff --git a/src/config/sessions/session-accessor.sqlite-session-row.ts b/src/config/sessions/session-accessor.sqlite-session-row.ts index b41451db460d..e88c23e4eaa3 100644 --- a/src/config/sessions/session-accessor.sqlite-session-row.ts +++ b/src/config/sessions/session-accessor.sqlite-session-row.ts @@ -10,7 +10,6 @@ import { projectCanonicalSessionEntryShape } from "./store-entry-shape.js"; import type { SessionEntry } from "./types.js"; export function normalizeSessionEntryTimestamp(entry: SessionEntry): SessionEntry { - const raw = entry as unknown as Record; const hasLegacyDeliveryFields = [ "route", "deliveryContext", @@ -20,7 +19,7 @@ export function normalizeSessionEntryTimestamp(entry: SessionEntry): SessionEntr "lastTo", "lastAccountId", "lastThreadId", - ].some((key) => key in raw); + ].some((key) => key in entry); const delivery = entry.delivery ?? (hasLegacyDeliveryFields ? undefined : { kind: "none" as const }); if (typeof entry.updatedAt === "number" && Number.isFinite(entry.updatedAt)) { @@ -84,9 +83,7 @@ export function bindSessionNode(params: { sessionKey: string; updatedAt: number; }) { - const canonicalEntry = projectCanonicalSessionEntryShape( - params.entry as unknown as Record, - ); + const canonicalEntry = projectCanonicalSessionEntryShape({ ...params.entry }); const actor = params.entry.createdActor; const legacyActorId = normalizeText( (params.entry as SessionEntry & { createdBy?: { id?: unknown } }).createdBy?.id, diff --git a/src/config/sessions/store-migrations.ts b/src/config/sessions/store-migrations.ts index 9c24fa6b8f22..e1c9482a5cfa 100644 --- a/src/config/sessions/store-migrations.ts +++ b/src/config/sessions/store-migrations.ts @@ -1,4 +1,5 @@ // Session store migrations repair legacy field names during load/save normalization. +import { asOptionalRecord } from "@openclaw/normalization-core/record-coerce"; import type { SessionEntry } from "./types.js"; /** Applies best-effort in-place migrations for legacy session store entry fields. */ @@ -9,25 +10,28 @@ export function applySessionStoreMigrations(store: Record) if (!entry || typeof entry !== "object") { continue; } - const rec = entry as unknown as Record; - if (typeof rec.channel !== "string" && typeof rec.provider === "string") { - rec.channel = rec.provider; - delete rec.provider; + const rec = asOptionalRecord(entry); + if (!rec) { + continue; + } + if (typeof rec["channel"] !== "string" && typeof rec["provider"] === "string") { + rec["channel"] = rec["provider"]; + delete rec["provider"]; changed = true; } - if (typeof rec.lastChannel !== "string" && typeof rec.lastProvider === "string") { - rec.lastChannel = rec.lastProvider; - delete rec.lastProvider; + if (typeof rec["lastChannel"] !== "string" && typeof rec["lastProvider"] === "string") { + rec["lastChannel"] = rec["lastProvider"]; + delete rec["lastProvider"]; changed = true; } // Best-effort migration: legacy `room` field → `groupChannel` (keep value, prune old key). - if (typeof rec.groupChannel !== "string" && typeof rec.room === "string") { - rec.groupChannel = rec.room; - delete rec.room; + if (typeof rec.groupChannel !== "string" && typeof rec["room"] === "string") { + rec.groupChannel = rec["room"]; + delete rec["room"]; changed = true; } else if ("room" in rec) { - delete rec.room; + delete rec["room"]; changed = true; } } diff --git a/src/config/sessions/transcript.ts b/src/config/sessions/transcript.ts index d627f96abc42..16fefb0a3dae 100644 --- a/src/config/sessions/transcript.ts +++ b/src/config/sessions/transcript.ts @@ -740,8 +740,8 @@ function isIdentifiedDeliveryMirror(message: SessionTranscriptAssistantMessage): ); } -function extractAssistantMessageText(message: SessionTranscriptAssistantMessage): string | null { - if (!Array.isArray(message.content)) { +function extractAssistantMessageText(message: AgentMessage): string | null { + if (message.role !== "assistant" || !Array.isArray(message.content)) { return null; } @@ -764,9 +764,7 @@ async function findLatestEquivalentAssistantMessageId( message: SessionTranscriptAssistantMessage, config?: OpenClawConfig, ): Promise { - const expectedText = extractAssistantMessageText( - redactTranscriptMessage(message, config) as unknown as SessionTranscriptAssistantMessage, - ); + const expectedText = extractAssistantMessageText(redactTranscriptMessage(message, config)); if (!expectedText) { return undefined; } @@ -783,12 +781,7 @@ async function findLatestEquivalentAssistantMessageId( return undefined; } const candidateText = latest - ? extractAssistantMessageText( - redactTranscriptMessage( - latest.message as AgentMessage, - config, - ) as unknown as SessionTranscriptAssistantMessage, - ) + ? extractAssistantMessageText(redactTranscriptMessage(latest.message as AgentMessage, config)) : undefined; return candidateText === expectedText ? latest?.id : undefined; } diff --git a/src/plugin-sdk/agent-core.ts b/src/plugin-sdk/agent-core.ts index f908f9e76781..a61c15452a16 100644 --- a/src/plugin-sdk/agent-core.ts +++ b/src/plugin-sdk/agent-core.ts @@ -9,8 +9,10 @@ import { completeSimple, streamSimple } from "./llm.js"; /** Runtime adapter that lets the package agent-core use OpenClaw LLM helpers. */ export const openClawAgentCoreRuntime = { - completeSimple: completeSimple as unknown as CompleteSimpleFn, - streamSimple: streamSimple as unknown as StreamFn, + completeSimple: ((model, context, options) => + completeSimple(model, context, options)) satisfies CompleteSimpleFn, + streamSimple: ((model, context, options) => + streamSimple(model, context, options)) satisfies StreamFn, } satisfies AgentCoreRuntimeDeps; /** Agent-core class preconfigured with OpenClaw runtime dependencies. */ diff --git a/src/plugin-sdk/webhook-targets.ts b/src/plugin-sdk/webhook-targets.ts index 2f155c4d8118..e5420b0b57a6 100644 --- a/src/plugin-sdk/webhook-targets.ts +++ b/src/plugin-sdk/webhook-targets.ts @@ -97,10 +97,10 @@ export function registerWebhookTargetWithPluginRoute }); } -const pathTeardownByTargetMap = new WeakMap, Map void>>(); +const pathTeardownByTargetMap = new WeakMap void>>(); function getPathTeardownMap(targetsByPath: Map): Map void> { - const mapKey = targetsByPath as unknown as Map; + const mapKey = targetsByPath; const existing = pathTeardownByTargetMap.get(mapKey); if (existing) { return existing; diff --git a/ui/src/app/server-prefs-state.ts b/ui/src/app/server-prefs-state.ts index 0034bb60f269..159be2b7b6cd 100644 --- a/ui/src/app/server-prefs-state.ts +++ b/ui/src/app/server-prefs-state.ts @@ -118,6 +118,21 @@ export function prefValuesEqual(left: unknown, right: unknown): boolean { return left === right; } +function applyChangedSettingsPatch( + target: Partial, + settings: UiSettings, + source: Partial, +): void { + const applyKey = (key: K, value: UiSettings[K] | undefined) => { + if (!prefValuesEqual(settings[key], value)) { + target[key] = value; + } + }; + for (const key of Object.keys(source) as Array) { + applyKey(key, source[key]); + } +} + export function extractServerUiPrefs(configObject: unknown): ServerUiPrefs { const prefs = asRecord(asRecord(asRecord(configObject)?.ui)?.prefs); if (!prefs) { @@ -232,13 +247,7 @@ export function serverPrefsLocalPatch( if (serverValue === null) { const resetPatch = specification.clearable ? specification.reset?.(settings) : undefined; if (resetPatch) { - for (const [resetKey, resetValue] of Object.entries(resetPatch)) { - if ( - !prefValuesEqual((settings as unknown as Record)[resetKey], resetValue) - ) { - (patch as Record)[resetKey] = resetValue; - } - } + applyChangedSettingsPatch(patch, settings, resetPatch); } continue; } diff --git a/ui/src/app/settings.ts b/ui/src/app/settings.ts index a25e54a504f5..921c80ea94d8 100644 --- a/ui/src/app/settings.ts +++ b/ui/src/app/settings.ts @@ -1,5 +1,6 @@ import { gatewayOriginScope } from "@openclaw/gateway-client/browser"; import { safeParseJson } from "@openclaw/normalization-core"; +import { asOptionalRecord } from "@openclaw/normalization-core/record-coerce"; import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce"; import { normalizeUniqueTrimmedStringList } from "@openclaw/normalization-core/string-normalization"; import { @@ -468,7 +469,7 @@ export function loadSettings(): UiSettings { (parsed as { theme?: unknown }).theme, (parsed as { themeMode?: unknown }).themeMode, ); - const parsedRecord = parsed as unknown as Record; + const parsedRecord = asOptionalRecord(parsed) ?? {}; const hasSidebarEntries = Object.hasOwn(parsedRecord, "sidebarEntries"); // One-time read of the retired route-only shape; all writes use sidebarEntries. const migratedSidebarEntries = hasSidebarEntries diff --git a/ui/src/pages/chat/chat-state-refresh.ts b/ui/src/pages/chat/chat-state-refresh.ts index 02f906f9d7ce..82f72569d607 100644 --- a/ui/src/pages/chat/chat-state-refresh.ts +++ b/ui/src/pages/chat/chat-state-refresh.ts @@ -6,7 +6,7 @@ import { isSessionRunActive } from "../../lib/session-run-state.ts"; import { areUiSessionKeysEquivalent } from "../../lib/sessions/session-key.ts"; import { refreshChatAvatar, resolveAgentIdForSession } from "./chat-avatar.ts"; import { applyRemoteSlashCommandsResult, refreshSlashCommands } from "./chat-commands.ts"; -import { loadChatHistory, type ChatMetadataResult, type ChatState } from "./chat-history.ts"; +import { loadChatHistory, type ChatMetadataResult } from "./chat-history.ts"; import { flushChatQueueForEvent } from "./chat-send-actions.ts"; import { flushChatQueueAfterIdleSessionReconciliation } from "./chat-session.ts"; import type { ChatPageHost } from "./chat-state-host.ts"; @@ -241,7 +241,7 @@ export async function refreshChatMetadata( const request = { host, client, agentId, version: requestVersion }; host.chatModelsLoading = true; try { - if (isGatewayMethodAdvertised(host as unknown as ChatState, "chat.metadata") === false) { + if (isGatewayMethodAdvertised(host, "chat.metadata") === false) { await refreshMissingChatMetadata(request, EMPTY_CHAT_METADATA_APPLY_RESULT, opts); return EMPTY_CHAT_METADATA_APPLY_RESULT; } @@ -341,7 +341,7 @@ async function refreshChat( const refreshedAgentId = resolveAgentIdForSession(host); const requestUpdate = () => host.requestUpdate?.(); const previousSessionsResult = host.sessionsResult; - const historyLoad = loadChatHistory(host as unknown as ChatState, { + const historyLoad = loadChatHistory(host, { deferBranches: opts?.deferBranches === true, startup: opts?.startup === true, }); @@ -442,7 +442,7 @@ export function refreshPageChat(host: ChatPageHost, opts?: ChatRefreshOptions) { opts?.startup && host.client && host.connected && - isGatewayMethodAdvertised(host as unknown as ChatState, "chat.startup") !== false, + isGatewayMethodAdvertised(host, "chat.startup") !== false, ); const startupMetadataRequestVersion = ownsStartupMetadata ? ++host.chatMetadataRequestVersion diff --git a/ui/src/pages/chat/components/widget-card.ts b/ui/src/pages/chat/components/widget-card.ts index 2c9a6fc26488..b03c8ccb8f4b 100644 --- a/ui/src/pages/chat/components/widget-card.ts +++ b/ui/src/pages/chat/components/widget-card.ts @@ -200,7 +200,7 @@ const adoptedWidgetPromptFrames = new WeakSet(); const widgetPromptOfferListenerWindows = new WeakSet(); function tryAdoptWidgetPromptPort(frame: HTMLIFrameElement) { - const source = frame.contentWindow as unknown as object | null; + const source = frame.contentWindow; if (adoptedWidgetPromptFrames.has(frame) || !promptEligibleFrames.has(frame) || !source) { return; } @@ -236,14 +236,14 @@ function installWidgetPromptOfferListener() { if (!source || !port || event.origin !== "null") { return; } - if (offeredWidgetPromptSources.has(source as unknown as object)) { + if (offeredWidgetPromptSources.has(source)) { // Only the first offer per content window can win; a replacement // document's offer must never displace the genuine bridge's. port.close(); return; } - offeredWidgetPromptSources.add(source as unknown as object); - pendingWidgetPromptPorts.set(source as unknown as object, port); + offeredWidgetPromptSources.add(source); + pendingWidgetPromptPorts.set(source, port); // Posted-message and iframe-load tasks have no guaranteed cross-source // ordering, so the offer may arrive after the eligible frame's load; // adopt for it now instead of stranding the widget without a channel. diff --git a/ui/src/pages/plugins/icon-loader.ts b/ui/src/pages/plugins/icon-loader.ts index c5525a0dbf32..2d82022c0fd9 100644 --- a/ui/src/pages/plugins/icon-loader.ts +++ b/ui/src/pages/plugins/icon-loader.ts @@ -194,7 +194,7 @@ async function loadSvgImage(url: string): Promise { return image; } -function parseSvgDimensions(root: SVGSVGElement): { width: number; height: number } | null { +function parseSvgDimensions(root: Element): { width: number; height: number } | null { const viewBox = root.getAttribute("viewBox"); if (viewBox) { const values = viewBox @@ -278,7 +278,7 @@ async function sanitizeSvgForRasterization( if (pathCommands > PLUGIN_ICON_SVG_MAX_PATH_COMMANDS) { return null; } - const dimensions = parseSvgDimensions(root as unknown as SVGSVGElement); + const dimensions = parseSvgDimensions(root); if (!dimensions) { return null; }