diff --git a/config/knip.config.ts b/config/knip.config.ts index 0e771c2e537b..7c030b16239b 100644 --- a/config/knip.config.ts +++ b/config/knip.config.ts @@ -571,6 +571,7 @@ const config = { "src/boolean-coercion.ts!", "src/error-coercion.ts!", "src/expect.ts!", + "src/json-coercion.ts!", "src/number-coercion.ts!", "src/phone-presentation.ts!", "src/record-coerce.ts!", diff --git a/docs/.generated/plugin-sdk-api-baseline/agent-harness-runtime.json b/docs/.generated/plugin-sdk-api-baseline/agent-harness-runtime.json index 0050b37f06bd..f0a6dffa1572 100644 --- a/docs/.generated/plugin-sdk-api-baseline/agent-harness-runtime.json +++ b/docs/.generated/plugin-sdk-api-baseline/agent-harness-runtime.json @@ -1 +1 @@ -{"contentHash":"4aa3e760b353d1df519118c3695c9b2421b6f8d003ac2ffbb093630abe238360","entrypoint":"agent-harness-runtime","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime"} +{"contentHash":"a5aa9055bb7a6505df117fd57bc2f37d7aa9b20b1cff3c2531dae7fe0fc41f0b","entrypoint":"agent-harness-runtime","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime"} diff --git a/docs/.generated/plugin-sdk-api-baseline/agent-harness.json b/docs/.generated/plugin-sdk-api-baseline/agent-harness.json index f24a9400e9f9..3c7b040f865f 100644 --- a/docs/.generated/plugin-sdk-api-baseline/agent-harness.json +++ b/docs/.generated/plugin-sdk-api-baseline/agent-harness.json @@ -1 +1 @@ -{"contentHash":"dbaf8a557d0aba2a387280f134f76b5dfc156aaaa27abef5ff2dbfd21d96830b","entrypoint":"agent-harness","importSpecifier":"openclaw/plugin-sdk/agent-harness"} +{"contentHash":"9d3b87f5ecf49e5d95ac0359ae35bd029238b11e34a88d452c05d50ffc7a60a4","entrypoint":"agent-harness","importSpecifier":"openclaw/plugin-sdk/agent-harness"} diff --git a/docs/.generated/plugin-sdk-api-baseline/channel-core.json b/docs/.generated/plugin-sdk-api-baseline/channel-core.json index ae382cd08378..db8c9032fded 100644 --- a/docs/.generated/plugin-sdk-api-baseline/channel-core.json +++ b/docs/.generated/plugin-sdk-api-baseline/channel-core.json @@ -1 +1 @@ -{"contentHash":"aa63172e6cc1bb34ba6fcc47c0c6cb20ab9c6efb061939b23351a54ec563c933","entrypoint":"channel-core","importSpecifier":"openclaw/plugin-sdk/channel-core"} +{"contentHash":"fa576f6d45ac6077714529a5d8d496800191ed5e28015e4744dada75c8a22e2c","entrypoint":"channel-core","importSpecifier":"openclaw/plugin-sdk/channel-core"} diff --git a/docs/.generated/plugin-sdk-api-baseline/channel-entry-contract.json b/docs/.generated/plugin-sdk-api-baseline/channel-entry-contract.json index 52cd76c5c66b..aa20b15ee3f8 100644 --- a/docs/.generated/plugin-sdk-api-baseline/channel-entry-contract.json +++ b/docs/.generated/plugin-sdk-api-baseline/channel-entry-contract.json @@ -1 +1 @@ -{"contentHash":"9dbf56ae53ce48e962a0a2acaa28786968111ade8d3b6ec9112d8797dc18b0ef","entrypoint":"channel-entry-contract","importSpecifier":"openclaw/plugin-sdk/channel-entry-contract"} +{"contentHash":"b29f2c2f0e8a9b13870fac3919deb611a45f307ef317baf1916e8c7621547ca1","entrypoint":"channel-entry-contract","importSpecifier":"openclaw/plugin-sdk/channel-entry-contract"} diff --git a/docs/.generated/plugin-sdk-api-baseline/channel-message.json b/docs/.generated/plugin-sdk-api-baseline/channel-message.json index 58adc5dfe30c..3b365bb5815a 100644 --- a/docs/.generated/plugin-sdk-api-baseline/channel-message.json +++ b/docs/.generated/plugin-sdk-api-baseline/channel-message.json @@ -1 +1 @@ -{"contentHash":"345577990d31114e6e94bb9ee4b4c1d382446c713b7d5743b80688fd906ae36d","entrypoint":"channel-message","importSpecifier":"openclaw/plugin-sdk/channel-message"} +{"contentHash":"08b9e89e4603dde1b13ce98bc534f5578fc66f8e9d7236f799954a7baaca39cb","entrypoint":"channel-message","importSpecifier":"openclaw/plugin-sdk/channel-message"} diff --git a/docs/.generated/plugin-sdk-api-baseline/channel-outbound.json b/docs/.generated/plugin-sdk-api-baseline/channel-outbound.json index 6f0487ccc4ca..f870a62503d6 100644 --- a/docs/.generated/plugin-sdk-api-baseline/channel-outbound.json +++ b/docs/.generated/plugin-sdk-api-baseline/channel-outbound.json @@ -1 +1 @@ -{"contentHash":"84c644ecaadfd74ec78bd5dfde78f92e6de1f12a05cfd156539f71797fb02ea8","entrypoint":"channel-outbound","importSpecifier":"openclaw/plugin-sdk/channel-outbound"} +{"contentHash":"0d20ad92fe85134d8dd0d4fbd144cba687a65d91930056f08eb8a03b791bf963","entrypoint":"channel-outbound","importSpecifier":"openclaw/plugin-sdk/channel-outbound"} diff --git a/docs/.generated/plugin-sdk-api-baseline/channel-plugin-common.json b/docs/.generated/plugin-sdk-api-baseline/channel-plugin-common.json index c42e9d16faa4..78f07009e40b 100644 --- a/docs/.generated/plugin-sdk-api-baseline/channel-plugin-common.json +++ b/docs/.generated/plugin-sdk-api-baseline/channel-plugin-common.json @@ -1 +1 @@ -{"contentHash":"269ff82ec3c64e671804ab242cae73d5a5031493cdd76da458c4fd7198b9351b","entrypoint":"channel-plugin-common","importSpecifier":"openclaw/plugin-sdk/channel-plugin-common"} +{"contentHash":"a3df582853be918f4893acf8501a6b7df14f7d04e27ff249823e243926278598","entrypoint":"channel-plugin-common","importSpecifier":"openclaw/plugin-sdk/channel-plugin-common"} diff --git a/docs/.generated/plugin-sdk-api-baseline/core.json b/docs/.generated/plugin-sdk-api-baseline/core.json index 172929603730..94bb7d199b03 100644 --- a/docs/.generated/plugin-sdk-api-baseline/core.json +++ b/docs/.generated/plugin-sdk-api-baseline/core.json @@ -1 +1 @@ -{"contentHash":"e141ebb54f0102829e7edcd2bd0d1e9b21cc71ae972284dcb916e3a62031324a","entrypoint":"core","importSpecifier":"openclaw/plugin-sdk/core"} +{"contentHash":"28f52a46ac1d7bb3c522d9f5ff62d9b3b6e10cdec2be9f23b7ca2b8d8e4ca12c","entrypoint":"core","importSpecifier":"openclaw/plugin-sdk/core"} diff --git a/docs/.generated/plugin-sdk-api-baseline/discord.json b/docs/.generated/plugin-sdk-api-baseline/discord.json index 9c9d0d8fe384..a27ba71e232c 100644 --- a/docs/.generated/plugin-sdk-api-baseline/discord.json +++ b/docs/.generated/plugin-sdk-api-baseline/discord.json @@ -1 +1 @@ -{"contentHash":"0e33b7a69d8c8a038922448b5ff5c049825d3872bb708b2c35bcd5472be98fb4","entrypoint":"discord","importSpecifier":"openclaw/plugin-sdk/discord"} +{"contentHash":"fb5d2a6e3e29e2a227b891b39e42462dab3dc62b804a2b7268d14346278fa1ea","entrypoint":"discord","importSpecifier":"openclaw/plugin-sdk/discord"} diff --git a/docs/.generated/plugin-sdk-api-baseline/inbound-reply-dispatch.json b/docs/.generated/plugin-sdk-api-baseline/inbound-reply-dispatch.json index 0c0a619b7d0c..d56ce4cb1289 100644 --- a/docs/.generated/plugin-sdk-api-baseline/inbound-reply-dispatch.json +++ b/docs/.generated/plugin-sdk-api-baseline/inbound-reply-dispatch.json @@ -1 +1 @@ -{"contentHash":"dd10bdfce14b5dc2ddd976448ff67ae9ad3ef396fe33efd9cb64a737ff279e45","entrypoint":"inbound-reply-dispatch","importSpecifier":"openclaw/plugin-sdk/inbound-reply-dispatch"} +{"contentHash":"bef4716ee06950504ba2dd90333092cb71e56ce10d97d8ba79c3372871f2dddc","entrypoint":"inbound-reply-dispatch","importSpecifier":"openclaw/plugin-sdk/inbound-reply-dispatch"} diff --git a/docs/.generated/plugin-sdk-api-baseline/meeting-runtime.json b/docs/.generated/plugin-sdk-api-baseline/meeting-runtime.json index c9b2ecec9404..e7ba90096a5c 100644 --- a/docs/.generated/plugin-sdk-api-baseline/meeting-runtime.json +++ b/docs/.generated/plugin-sdk-api-baseline/meeting-runtime.json @@ -1 +1 @@ -{"contentHash":"3d54eabf64f44c0597a4273fa3a993d91ce335bca196fc12c49b9a472a752f22","entrypoint":"meeting-runtime","importSpecifier":"openclaw/plugin-sdk/meeting-runtime"} +{"contentHash":"8ead34b0bfed6204ad25b2753b27dc6b239b19afc8afbd0f498379124227eb4a","entrypoint":"meeting-runtime","importSpecifier":"openclaw/plugin-sdk/meeting-runtime"} diff --git a/docs/.generated/plugin-sdk-api-baseline/plugin-entry.json b/docs/.generated/plugin-sdk-api-baseline/plugin-entry.json index 32877f489b11..0de20cd12cb1 100644 --- a/docs/.generated/plugin-sdk-api-baseline/plugin-entry.json +++ b/docs/.generated/plugin-sdk-api-baseline/plugin-entry.json @@ -1 +1 @@ -{"contentHash":"fbaffdb6c6ea7f5713950c64f984dd1c297f9e556d6889acd0f60dc75e65fd4c","entrypoint":"plugin-entry","importSpecifier":"openclaw/plugin-sdk/plugin-entry"} +{"contentHash":"03c4a48091e9d1db55ef24461f1e4a5fdb333a4be2d90574fea86fba01c5cc4f","entrypoint":"plugin-entry","importSpecifier":"openclaw/plugin-sdk/plugin-entry"} diff --git a/docs/.generated/plugin-sdk-api-baseline/plugin-runtime.json b/docs/.generated/plugin-sdk-api-baseline/plugin-runtime.json index 8f577add6dcb..39316fab78cc 100644 --- a/docs/.generated/plugin-sdk-api-baseline/plugin-runtime.json +++ b/docs/.generated/plugin-sdk-api-baseline/plugin-runtime.json @@ -1 +1 @@ -{"contentHash":"cf61011d1a62cfa55ada8245928bd6877b6bbd5fa9f9fea63837697853c99067","entrypoint":"plugin-runtime","importSpecifier":"openclaw/plugin-sdk/plugin-runtime"} +{"contentHash":"ca0acf4e21e1978c6f51dce666d0f92b025f3184423be50cd659c8ae3d3e3cc5","entrypoint":"plugin-runtime","importSpecifier":"openclaw/plugin-sdk/plugin-runtime"} diff --git a/docs/.generated/plugin-sdk-api-baseline/provider-catalog-runtime.json b/docs/.generated/plugin-sdk-api-baseline/provider-catalog-runtime.json index ff4f4fd408b2..00495635c9d1 100644 --- a/docs/.generated/plugin-sdk-api-baseline/provider-catalog-runtime.json +++ b/docs/.generated/plugin-sdk-api-baseline/provider-catalog-runtime.json @@ -1 +1 @@ -{"contentHash":"7b87709014d10313b7ade61594ad5e8ac032695ab97439abb2781f5dc16e09f8","entrypoint":"provider-catalog-runtime","importSpecifier":"openclaw/plugin-sdk/provider-catalog-runtime"} +{"contentHash":"fc10e0e479166eae7d9b7316c240d14b6c78ac9d78b2715f09f5f0d61fe99372","entrypoint":"provider-catalog-runtime","importSpecifier":"openclaw/plugin-sdk/provider-catalog-runtime"} diff --git a/docs/.generated/plugin-sdk-api-baseline/tool-plugin.json b/docs/.generated/plugin-sdk-api-baseline/tool-plugin.json index 4695226ef513..3cbbd761747a 100644 --- a/docs/.generated/plugin-sdk-api-baseline/tool-plugin.json +++ b/docs/.generated/plugin-sdk-api-baseline/tool-plugin.json @@ -1 +1 @@ -{"contentHash":"98ad69c4e05380e77b259132c66463b536675331ae77fea7465230b93c9daca6","entrypoint":"tool-plugin","importSpecifier":"openclaw/plugin-sdk/tool-plugin"} +{"contentHash":"d38378e903e47a17e5d3b86ae87c9e93e7513c883b2e6a49e1a6d2ea2f3b1514","entrypoint":"tool-plugin","importSpecifier":"openclaw/plugin-sdk/tool-plugin"} diff --git a/docs/.generated/plugin-sdk-api-baseline/webhook-ingress.json b/docs/.generated/plugin-sdk-api-baseline/webhook-ingress.json index 4e69fd335b3c..9ad9a30fbe2c 100644 --- a/docs/.generated/plugin-sdk-api-baseline/webhook-ingress.json +++ b/docs/.generated/plugin-sdk-api-baseline/webhook-ingress.json @@ -1 +1 @@ -{"contentHash":"0a5b8c7968e6761a2b73be6b88131758da62eb9cd3d00d20d58a18ba01b8ad87","entrypoint":"webhook-ingress","importSpecifier":"openclaw/plugin-sdk/webhook-ingress"} +{"contentHash":"e150792598fe5067b2eeaa80b4f3cf417c48ffe96f3c15cecced6fd3c0683992","entrypoint":"webhook-ingress","importSpecifier":"openclaw/plugin-sdk/webhook-ingress"} diff --git a/docs/plugins/sdk-migration.md b/docs/plugins/sdk-migration.md index 98f5be775480..6b0383e54d0c 100644 --- a/docs/plugins/sdk-migration.md +++ b/docs/plugins/sdk-migration.md @@ -202,23 +202,19 @@ Audit the current migration queue with `pnpm plugins:boundary-report`: | ------------------------------------------------------- | -------------------------------------------------------------------------- | | `--summary` (or `pnpm plugins:boundary-report:summary`) | Compact counts instead of full detail. | | `--json` | Machine-readable report. | -| `--owner ` | Filter to one plugin or compatibility owner. | -| `--fail-on-cross-owner` | Exit non-zero on cross-owner reserved SDK imports. | +| `--owner ` | Filter to one compatibility owner. | | `--fail-on-eligible-compat` | Exit non-zero on or after a deprecated compat record's `removeAfter` date. | -| `--fail-on-unclassified-unused-reserved` | Exit non-zero on unused reserved SDK shims. | -`pnpm plugins:boundary-report:ci` runs with all three fail flags. Deprecated -records normally have an explicit `removeAfter` date. A contract tied to a -version boundary instead declares a `removalGate`; `next-plugin-sdk-major` is an -approved major-version gate, not a pending owner decision, and is never -date-eligible. A record with neither field appears as `no-date` and remains -ineligible until its owner publishes a gate. The report displays either the date -or named gate, counts local code/doc references, lists `removal-pending` records -with their blockers and surface-token reader references, surfaces cross-owner -reserved SDK imports, and summarizes the private memory-host SDK bridge. Those -reader references are triage signals, not published-artifact proof. Reserved SDK -subpaths must have tracked owner usage; unused reserved exports should be removed -from the public SDK. +`pnpm plugins:boundary-report:ci` runs with the compatibility fail flag. +Deprecated records normally have an explicit `removeAfter` date. A contract +tied to a version boundary instead declares a `removalGate`; +`next-plugin-sdk-major` is an approved major-version gate, not a pending owner +decision, and is never date-eligible. A record with neither field appears as +`no-date` and remains ineligible until its owner publishes a gate. The report +displays either the date or named gate, counts local code/doc references, lists +`removal-pending` records with their blockers and surface-token reader +references, and summarizes the private memory-host SDK bridge. Those reader +references are triage signals, not published-artifact proof. ### Media legacy projection diff --git a/docs/plugins/sdk-subpaths.md b/docs/plugins/sdk-subpaths.md index c9d2a6765038..4384064b2f18 100644 --- a/docs/plugins/sdk-subpaths.md +++ b/docs/plugins/sdk-subpaths.md @@ -16,14 +16,11 @@ private-local entries explicitly. Three files define the boundary: excluded from the typed, documented SDK. Production entries remain available as JavaScript-only host runtime exports for separately published official plugins; test-only entries stay unexported. -- `src/plugin-sdk/entrypoints.ts`: classification metadata for deprecated - subpaths, reserved bundled helpers, supported bundled facades, and - plugin-owned public surfaces. +- `scripts/lib/plugin-sdk-entries.mts`: derived public/private export metadata, + supported bundled facades, and plugin-owned public surfaces. Maintainers audit the public export count with `pnpm plugin-sdk:surface` and -active reserved helper subpaths with `pnpm plugins:boundary-report:summary`; -unused reserved helper exports fail the CI report instead of staying in the -public SDK as dormant compatibility debt. +the compatibility queue with `pnpm plugins:boundary-report:summary`. For the plugin authoring guide, see [Plugin SDK overview](/plugins/sdk-overview). @@ -64,10 +61,10 @@ longer package exports: `agent-runtime-test-contracts`, ### Bundled plugin helper subpaths -Bundled-only helper modules are private-local after the July 2026 sweep. Cross-owner imports are blocked by package contract guardrails. `src/plugin-sdk/entrypoints.ts` separately tracks the supported bundled facades that remain public, SDK -entrypoints backed by their bundled plugin until generic contracts replace -`plugin-sdk/qa-runner-runtime`, `plugin-sdk/telegram-account`, -deprecated for new code; see the per-row notes below. +Bundled-only helper modules are private-local after the July 2026 sweep. +Package contract guardrails classify the supported bundled facades that remain +public until generic contracts replace them. Those facades are deprecated for +new code; see the per-row notes below. @@ -223,7 +220,7 @@ Use `isLoopbackHost(host)` when a plugin must accept only the local machine. It | `plugin-sdk/browser-config` | Private-local after July 2026; Supported browser config facade for normalized profile/defaults, CDP URL parsing, and browser-control auth helpers | | `plugin-sdk/agent-harness-task-runtime` | Private-local after July 2026; Generic task lifecycle and completion delivery helpers for harness-backed agents using a host-issued task scope | | `plugin-sdk/agent-harness-runtime` | Agent-harness runtime helpers. `acquireSessionWriteLock`, `resolveSessionWriteLockAcquireTimeoutMs`, `resolveSessionWriteLockOptions`, and `SessionWriteLockAcquireTimeoutConfig` are deprecated no-op compatibility exports scheduled for removal in the 2026.10 release train. They no longer block or create lock sidecars; harnesses should rely on OpenClaw's per-session lane plus the durable writer claim and in-transaction fence. | - | `plugin-sdk/codex-mcp-projection` | Private-local after July 2026; Reserved bundled Codex helper for projecting user MCP server config into Codex thread config; not for third-party plugins | + | `plugin-sdk/codex-mcp-projection` | Private-local after July 2026; Bundled Codex helper for projecting user MCP server config into Codex thread config; not for third-party plugins | | `plugin-sdk/codex-session-transcript-runtime` | Private-local bundled Codex helper for serializing transcript-mirror writes; not for third-party plugins | | `plugin-sdk/channel-runtime-context` | Generic channel runtime-context registration and lookup helpers | | `plugin-sdk/matrix` | Deprecated Matrix compatibility facade for older third-party channel packages; new plugins should import `plugin-sdk/run-command` directly | @@ -403,8 +400,8 @@ Use `isLoopbackHost(host)` when a plugin must accept only the local machine. It | Subpath | Owner and purpose | | --- | --- | - | `plugin-sdk/codex-mcp-projection` | Private-local after July 2026; Bundled Codex plugin helper for projecting user MCP server config into Codex app-server thread config (reserved package export) | - | `plugin-sdk/codex-session-transcript-runtime` | Private-local bundled Codex plugin helper for serializing transcript-mirror writes (reserved package export) | + | `plugin-sdk/codex-mcp-projection` | Private-local after July 2026; Bundled Codex plugin helper for projecting user MCP server config into Codex app-server thread config (default-only package export) | + | `plugin-sdk/codex-session-transcript-runtime` | Private-local bundled Codex plugin helper for serializing transcript-mirror writes (default-only package export) | diff --git a/extensions/active-memory/index.ts b/extensions/active-memory/index.ts index 71c79405c94f..8a4badd28db1 100644 --- a/extensions/active-memory/index.ts +++ b/extensions/active-memory/index.ts @@ -30,7 +30,7 @@ import { resetActiveRecallStateForTests, setCachedResult, shouldCacheResult, - toSingleLineLogValue, + toSingleLineErrorMessage, } from "./recall-state.js"; import { maybeResolveActiveRecall } from "./recall.js"; import { @@ -376,9 +376,7 @@ export default definePluginEntry({ runId: ctx.runId, }).catch((error: unknown) => { api.logger.debug?.( - `active-memory: lane-1 trigger recall failed: ${toSingleLineLogValue( - error instanceof Error ? error.message : String(error), - )}`, + `active-memory: lane-1 trigger recall failed: ${toSingleLineErrorMessage(error)}`, ); return { hasStrongHit: false, injectedCount: 0 }; }); @@ -483,9 +481,7 @@ export default definePluginEntry({ if (deadlineController.signal.aborted) { return undefined; } - const message = toSingleLineLogValue( - error instanceof Error ? error.message : String(error), - ); + const message = toSingleLineErrorMessage(error); api.logger.warn?.( `active-memory: before_prompt_build failed, skipping memory lookup: ${message}`, ); @@ -547,9 +543,7 @@ export default definePluginEntry({ runId: ctx.runId, }).catch((error: unknown) => { api.logger.debug?.( - `active-memory: lane-1 prewarm failed: ${toSingleLineLogValue( - error instanceof Error ? error.message : String(error), - )}`, + `active-memory: lane-1 prewarm failed: ${toSingleLineErrorMessage(error)}`, ); }); }); diff --git a/extensions/active-memory/recall-run.ts b/extensions/active-memory/recall-run.ts index 0452cab1c2a6..49e9f6a2c207 100644 --- a/extensions/active-memory/recall-run.ts +++ b/extensions/active-memory/recall-run.ts @@ -21,7 +21,7 @@ import { } from "./config.js"; import { buildRecallPrompt } from "./prompt.js"; import { getModelRef } from "./query.js"; -import { toSingleLineLogValue } from "./recall-state.js"; +import { toSingleLineErrorMessage } from "./recall-state.js"; import { resolveRecallRunChannelContext } from "./session.js"; import { attachPartialTimeoutData, @@ -383,7 +383,7 @@ async function runRecallSubagent(params: { return { rawReply: "NONE", resultStatus: "unavailable" }; } if (!params.abortSignal?.aborted) { - const message = toSingleLineLogValue(error instanceof Error ? error.message : String(error)); + const message = toSingleLineErrorMessage(error); params.api.logger.warn?.( `active-memory: memory sub-agent failed, skipping recall: ${message}`, ); @@ -398,9 +398,7 @@ async function runRecallSubagent(params: { sources: transcriptSources, sessionFile: artifactSessionFile, }).catch((error: unknown) => { - const message = toSingleLineLogValue( - error instanceof Error ? error.message : String(error), - ); + const message = toSingleLineErrorMessage(error); params.api.logger.debug?.( `active-memory: failed to persist recall transcript ${artifactSessionFile}: ${message}`, ); @@ -412,9 +410,7 @@ async function runRecallSubagent(params: { sessionKey: subagentSessionKey, storePath, }).catch((error: unknown) => { - const message = toSingleLineLogValue( - error instanceof Error ? error.message : String(error), - ); + const message = toSingleLineErrorMessage(error); params.api.logger.warn?.( `active-memory: failed to clean up recall session ${subagentSessionKey}: ${message}`, ); diff --git a/extensions/active-memory/recall-state.ts b/extensions/active-memory/recall-state.ts index 76af3383410a..4d7e71eb2d8c 100644 --- a/extensions/active-memory/recall-state.ts +++ b/extensions/active-memory/recall-state.ts @@ -1,4 +1,5 @@ import crypto from "node:crypto"; +import { coerceErrorMessage } from "openclaw/plugin-sdk/error-runtime"; import { closeActiveMemorySearchManager } from "openclaw/plugin-sdk/memory-host-search"; import { asDateTimestampMs, @@ -69,9 +70,7 @@ function scheduleMemorySearchCleanupAfterTimeout( api.logger.debug?.(`${logPrefix} released memory search managers after timeout`); }) .catch((error: unknown) => { - const message = toSingleLineLogValue( - error instanceof Error ? error.message : String(error), - ); + const message = toSingleLineErrorMessage(error); api.logger.warn?.( `${logPrefix} failed to release memory search managers after timeout: ${message}`, ); @@ -223,6 +222,10 @@ function toSingleLineLogValue(value: unknown): string { : singleLine; } +function toSingleLineErrorMessage(error: unknown): string { + return toSingleLineLogValue(coerceErrorMessage(error)); +} + function shouldCacheResult(result: ActiveRecallResult): boolean { return result.status === "ok" && result.summary.length > 0; } @@ -252,5 +255,6 @@ export { scheduleMemorySearchCleanupAfterTimeout, setCachedResult, shouldCacheResult, + toSingleLineErrorMessage, toSingleLineLogValue, }; diff --git a/extensions/active-memory/recall.ts b/extensions/active-memory/recall.ts index 4ff1ed59c55b..5914cb0d2f5a 100644 --- a/extensions/active-memory/recall.ts +++ b/extensions/active-memory/recall.ts @@ -15,6 +15,7 @@ import { scheduleMemorySearchCleanupAfterTimeout, setCachedResult, shouldCacheResult, + toSingleLineErrorMessage, toSingleLineLogValue, } from "./recall-state.js"; import { @@ -441,7 +442,7 @@ async function resolveActiveRecall( params.abortSignal?.throwIfAborted(); return result; } - const message = toSingleLineLogValue(error instanceof Error ? error.message : String(error)); + const message = toSingleLineErrorMessage(error); if (params.config.logging) { params.api.logger.warn?.(`${logPrefix} failed error=${message}; skipping recall`); } diff --git a/extensions/browser/src/browser/cdp-websocket.ts b/extensions/browser/src/browser/cdp-websocket.ts index a9f382a1368b..31c990a34c91 100644 --- a/extensions/browser/src/browser/cdp-websocket.ts +++ b/extensions/browser/src/browser/cdp-websocket.ts @@ -143,7 +143,7 @@ function createPinnedAgentForCdpUrl( assertPinnedAgentAuthority(url, connectionOptions); } catch (err) { const socket = new net.Socket(); - const error = err instanceof Error ? err : new Error(String(err)); + const error = toStringifiedError(err); process.nextTick(() => { callback?.(error, socket); socket.destroy(error); diff --git a/extensions/browser/src/browser/config.ts b/extensions/browser/src/browser/config.ts index 3a87a712e26c..0d12c6476122 100644 --- a/extensions/browser/src/browser/config.ts +++ b/extensions/browser/src/browser/config.ts @@ -19,8 +19,8 @@ import { deriveDefaultBrowserControlPort, } from "../config/port-defaults.js"; import type { SsrFPolicy } from "../infra/net/ssrf.js"; +import { parseBooleanValue } from "../sdk-config.js"; import { resolveUserPath } from "../utils.js"; -import { parseBooleanValue } from "../utils/boolean.js"; import { parseBrowserHttpUrl, redactCdpUrl, isLoopbackHost } from "./cdp.helpers.js"; import { DEFAULT_AI_SNAPSHOT_MAX_CHARS, diff --git a/extensions/browser/src/utils/boolean.ts b/extensions/browser/src/utils/boolean.ts deleted file mode 100644 index 4bb1915c29fb..000000000000 --- a/extensions/browser/src/utils/boolean.ts +++ /dev/null @@ -1,4 +0,0 @@ -/** - * Boolean parsing helper re-exported for Browser CLI/config code. - */ -export { parseBooleanValue } from "../sdk-config.js"; diff --git a/extensions/codex/src/app-server/approval-bridge.ts b/extensions/codex/src/app-server/approval-bridge.ts index 383fdd9ad8a9..51262c6d990a 100644 --- a/extensions/codex/src/app-server/approval-bridge.ts +++ b/extensions/codex/src/app-server/approval-bridge.ts @@ -13,7 +13,7 @@ import { type NativeHookRelayProcessResponse, type NativeHookRelayRegistrationHandle, } from "openclaw/plugin-sdk/agent-harness-runtime"; -import { coerceErrorMessage as formatErrorMessage } from "openclaw/plugin-sdk/error-runtime"; +import { coerceErrorMessage } from "openclaw/plugin-sdk/error-runtime"; import { normalizeTrimmedStringList, readStringField as readString, @@ -232,7 +232,7 @@ export async function handleCodexAppServerApprovalRequest(params: { message: cancelled ? "Codex app-server approval cancelled because the run stopped." : `Codex app-server approval route failed: ${formatCodexDisplayText( - formatErrorMessage(error), + coerceErrorMessage(error), )}`, }); return buildApprovalResponse( @@ -585,7 +585,7 @@ async function runNativeRelayToolPolicyForApprovalRequest(params: { handled: true, blocked: true, reason: `OpenClaw native hook relay unavailable for Codex app-server approval: ${formatCodexDisplayText( - formatErrorMessage(error), + coerceErrorMessage(error), )}`, failureDisposition: "failed", }; diff --git a/extensions/codex/src/app-server/client.ts b/extensions/codex/src/app-server/client.ts index 97447b7badb8..5cb184d97c78 100644 --- a/extensions/codex/src/app-server/client.ts +++ b/extensions/codex/src/app-server/client.ts @@ -5,6 +5,8 @@ import { randomUUID } from "node:crypto"; import { createInterface, type Interface as ReadlineInterface } from "node:readline"; import { embeddedAgentLog, OPENCLAW_VERSION } from "openclaw/plugin-sdk/agent-harness-runtime"; +import { coerceErrorMessage, toStringifiedError } from "openclaw/plugin-sdk/error-runtime"; +import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime"; import { sliceUtf16Safe, truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime"; import { resolveCodexAppServerRuntimeOptions, type CodexAppServerStartOptions } from "./config.js"; import { @@ -225,12 +227,8 @@ export class CodexAppServerClient { this.child = child; this.lines = createInterface({ input: child.stdout }); this.lines.on("line", (line) => this.handleLine(line)); - this.lines.on("error", (error) => - this.closeWithError(error instanceof Error ? error : new Error(String(error))), - ); - child.stdout.on("error", (error) => - this.closeWithError(error instanceof Error ? error : new Error(String(error))), - ); + this.lines.on("error", (error) => this.closeWithError(toStringifiedError(error))); + child.stdout.on("error", (error) => this.closeWithError(toStringifiedError(error))); child.stderr.setEncoding("utf8"); child.stderr.on("data", (text: string) => { this.stderrTail = appendBoundedTail(this.stderrTail, text, CODEX_APP_SERVER_STDERR_TAIL_MAX); @@ -244,9 +242,7 @@ export class CodexAppServerClient { child.stderr.on("error", (error) => { embeddedAgentLog.warn("codex app-server stderr stream failed", { error }); }); - child.once("error", (error) => - this.closeWithError(error instanceof Error ? error : new Error(String(error))), - ); + child.once("error", (error) => this.closeWithError(toStringifiedError(error))); child.once("exit", (code, signal) => { this.transportExited = true; this.closeWithError(buildCodexAppServerExitError(code, signal, this.stderrTail)); @@ -255,9 +251,7 @@ export class CodexAppServerClient { // stream. When the child process terminates abruptly the pipe can break // before the "exit" event fires, so a pending writeMessage() produces an // asynchronous error on stdin that would otherwise crash the gateway. - child.stdin.on?.("error", (error) => - this.closeWithError(error instanceof Error ? error : new Error(String(error))), - ); + child.stdin.on?.("error", (error) => this.closeWithError(toStringifiedError(error))); } /** Starts a new app-server client using resolved runtime start options. */ @@ -638,7 +632,7 @@ export class CodexAppServerClient { onWriteAttempt?.(); this.writeMessage(message, (error) => rejectPending(error)); } catch (error) { - rejectPending(error instanceof Error ? error : new Error(String(error))); + rejectPending(toStringifiedError(error)); } }); } @@ -829,7 +823,7 @@ export class CodexAppServerClient { } this.writeMessage({ id: request.id, result: defaultServerRequestResponse(request) }); } catch (error) { - const message = error instanceof Error ? error.message : String(error); + const message = coerceErrorMessage(error); embeddedAgentLog.warn("codex app-server server request handler failed", { id: request.id, method: request.method, @@ -1028,10 +1022,10 @@ function buildCodexAppServerRuntimeIdentity( response: CodexInitializeResponse, serverVersion: string, ): CodexAppServerRuntimeIdentity { - const userAgent = readNonEmptyInitializeString(response.userAgent); - const codexHome = readNonEmptyInitializeString(response.codexHome); - const platformFamily = readNonEmptyInitializeString(response.platformFamily); - const platformOs = readNonEmptyInitializeString(response.platformOs); + const userAgent = normalizeOptionalString(response.userAgent); + const codexHome = normalizeOptionalString(response.codexHome); + const platformFamily = normalizeOptionalString(response.platformFamily); + const platformOs = normalizeOptionalString(response.platformOs); return { serverVersion, ...(userAgent ? { userAgent } : {}), @@ -1041,11 +1035,6 @@ function buildCodexAppServerRuntimeIdentity( }; } -function readNonEmptyInitializeString(value: string | undefined): string | undefined { - const trimmed = value?.trim(); - return trimmed ? trimmed : undefined; -} - /** Extracts the Codex version from the app-server initialize user-agent field. */ function readCodexVersionFromUserAgent(userAgent: string | undefined): string | undefined { // Codex returns `/ ...`; the originator can be @@ -1096,7 +1085,7 @@ function shouldBufferCodexAppServerParseFailure(value: string, error: unknown): if (!value.startsWith("{") && !value.startsWith("[")) { return false; } - const message = error instanceof Error ? error.message : String(error); + const message = coerceErrorMessage(error); return ( message.includes("Unterminated string") || message.includes("Unexpected end of JSON input") ); @@ -1107,7 +1096,7 @@ function logCodexAppServerParseFailure(value: string, error: unknown, fragmentCo const suffix = fragmentCount > 1 ? ` fragments=${fragmentCount}` : ""; embeddedAgentLog.warn("failed to parse codex app-server message", { error, - errorMessage: error instanceof Error ? error.message : String(error), + errorMessage: coerceErrorMessage(error), fragmentCount, linePreview, consoleMessage: `failed to parse codex app-server message${suffix}: preview=${JSON.stringify( diff --git a/extensions/codex/src/app-server/compact.ts b/extensions/codex/src/app-server/compact.ts index 90b469f6f37a..62a4b975091f 100644 --- a/extensions/codex/src/app-server/compact.ts +++ b/extensions/codex/src/app-server/compact.ts @@ -123,7 +123,7 @@ function watchCodexNativeCompactionCompletion(params: { embeddedAgentLog.error("failed to retire unconfirmed codex app-server compaction", { threadId: params.threadId, turnId: compactionTurnId, - reason: formatCompactionError(error), + reason: coerceErrorMessage(error), }); // Keep the lifecycle fence held when neither terminal state nor thread // retirement can be proven. Releasing would permit same-thread overlap. @@ -160,7 +160,7 @@ function watchCodexNativeCompactionCompletion(params: { embeddedAgentLog.warn("codex app-server compaction interrupt request failed", { threadId: params.threadId, turnId: compactionTurnId, - reason: formatCompactionError(error), + reason: coerceErrorMessage(error), }); }); }; @@ -510,7 +510,7 @@ async function compactCodexNativeThread( return { ok: false, compacted: false, - reason: formatCompactionError(error), + reason: coerceErrorMessage(error), }; } const { appServer, usesSupervisionConnection } = connection; @@ -652,7 +652,7 @@ async function compactCodexNativeThread( // Transport errors after the write leave the server-side start // ambiguous. Retire or detach the thread before releasing its fence. await completionWatch.retireUnconfirmedRequest( - `codex app-server compaction start was unconfirmed: ${formatCompactionError(error)}`, + `codex app-server compaction start was unconfirmed: ${coerceErrorMessage(error)}`, ); } }; @@ -754,7 +754,7 @@ async function compactCodexNativeThread( if (isCodexThreadNotFoundError(error)) { return failedCodexThreadBindingCompactionResult(params, { threadId: binding.threadId, - reason: formatCompactionError(error), + reason: coerceErrorMessage(error), recovery: "stale_thread_binding", }); } @@ -762,12 +762,12 @@ async function compactCodexNativeThread( sessionId: params.sessionId, sessionKey: params.sessionKey, threadId: binding.threadId, - reason: formatCompactionError(error), + reason: coerceErrorMessage(error), }); return { ok: false, compacted: false, - reason: formatCompactionError(error), + reason: coerceErrorMessage(error), }; } finally { completionWatch.cancel(); @@ -967,7 +967,4 @@ function isCodexThreadNotFoundError(error: unknown): boolean { return coerceErrorMessage(error).toLowerCase().includes("thread not found"); } -function formatCompactionError(error: unknown): string { - return coerceErrorMessage(error); -} /* oxlint-disable max-lines -- TODO: split this grandfathered oversized file. */ diff --git a/extensions/codex/src/app-server/local-runtime-attribution.ts b/extensions/codex/src/app-server/local-runtime-attribution.ts index ad3bbaeb8ce0..9bea74edd8b2 100644 --- a/extensions/codex/src/app-server/local-runtime-attribution.ts +++ b/extensions/codex/src/app-server/local-runtime-attribution.ts @@ -3,6 +3,7 @@ * backed by OpenAI auth but should still report Codex Responses semantics. */ import type { AgentHarnessAttemptParamsV2 } from "openclaw/plugin-sdk/agent-harness-runtime"; +import { normalizeLowercaseStringOrEmpty as normalizeRuntimeId } from "openclaw/plugin-sdk/string-coerce-runtime"; export type CodexLocalRuntimeAttributionParams = Pick< AgentHarnessAttemptParamsV2, @@ -19,10 +20,6 @@ type CodexLocalRuntimeAttribution = { api?: string; }; -function normalizeRuntimeId(value: string | undefined): string { - return value?.trim().toLowerCase() ?? ""; -} - /** Maps local Codex runtime plans onto the provider/api pair exposed to event projection. */ export function resolveCodexLocalRuntimeAttribution( params: CodexLocalRuntimeAttributionParams, diff --git a/extensions/codex/src/app-server/native-subagent-task-mirror.ts b/extensions/codex/src/app-server/native-subagent-task-mirror.ts index c99ff754fb8a..5940f2ad051c 100644 --- a/extensions/codex/src/app-server/native-subagent-task-mirror.ts +++ b/extensions/codex/src/app-server/native-subagent-task-mirror.ts @@ -3,7 +3,10 @@ * runtime rows so parent sessions can observe child progress. */ import type { AgentHarnessTaskRuntime } from "openclaw/plugin-sdk/agent-harness-task-runtime"; -import { readStringField as readString } from "openclaw/plugin-sdk/string-coerce-runtime"; +import { + normalizeOptionalString, + readStringField as readString, +} from "openclaw/plugin-sdk/string-coerce-runtime"; import { CODEX_NATIVE_SUBAGENT_RUN_ID_PREFIX } from "./native-subagent-task-ids.js"; import type { CodexServerNotification, @@ -102,13 +105,13 @@ export class CodexNativeSubagentTaskMirror { } const threadId = thread.id.trim(); const label = - trimOptional(spawn.agent_nickname) ?? - trimOptional(thread.agentNickname) ?? - trimOptional(spawn.agent_role) ?? - trimOptional(thread.agentRole) ?? + normalizeOptionalString(spawn.agent_nickname) ?? + normalizeOptionalString(thread.agentNickname) ?? + normalizeOptionalString(spawn.agent_role) ?? + normalizeOptionalString(thread.agentRole) ?? "Codex subagent"; const task = - trimOptional(thread.preview) ?? + normalizeOptionalString(thread.preview) ?? `Codex native subagent${label === "Codex subagent" ? "" : ` ${label}`}`; const createdAt = secondsToMillis(thread.createdAt) ?? this.now(); if ( @@ -258,13 +261,16 @@ export class CodexNativeSubagentTaskMirror { ) { return; } - const threadId = trimOptional(readString(item, "agentThreadId")); + const threadId = normalizeOptionalString(readString(item, "agentThreadId")); const kind = normalizeSubagentActivityKind(readString(item, "kind")); if (!threadId || !kind) { return; } if (kind === "started") { - this.createTaskFromSubagentActivity(threadId, trimOptional(readString(item, "agentPath"))); + this.createTaskFromSubagentActivity( + threadId, + normalizeOptionalString(readString(item, "agentPath")), + ); return; } if (this.mirrorStateByThreadId.get(threadId) !== "mirrored") { @@ -293,7 +299,7 @@ export class CodexNativeSubagentTaskMirror { } private createTaskFromCollabSpawnItem(threadId: string, item: JsonObject): void { - const prompt = trimOptional(readString(item, "prompt")); + const prompt = normalizeOptionalString(readString(item, "prompt")); const createdAt = this.now(); this.createRunningTask({ threadId, @@ -366,7 +372,7 @@ export class CodexNativeSubagentTaskMirror { runId, lastEventAt: eventAt, progressSummary: - trimOptional(message) ?? + normalizeOptionalString(message) ?? (normalizedStatus === "pendingInit" ? "Codex native subagent is initializing." : normalizedStatus === "interrupted" @@ -377,7 +383,7 @@ export class CodexNativeSubagentTaskMirror { } if (normalizedStatus === "completed") { this.terminalRunIds.add(runId); - const summary = trimOptional(message) ?? "Codex native subagent completed."; + const summary = normalizeOptionalString(message) ?? "Codex native subagent completed."; if (this.expectedAuthoritativeRunIds.has(runId)) { this.runtime.recordTaskRunProgressByRunId({ runId, @@ -405,8 +411,8 @@ export class CodexNativeSubagentTaskMirror { status: "succeeded", endedAt: eventAt, lastEventAt: eventAt, - progressSummary: trimOptional(message) ?? "Codex native subagent blocked.", - terminalSummary: trimOptional(message) ?? "Codex native subagent blocked.", + progressSummary: normalizeOptionalString(message) ?? "Codex native subagent blocked.", + terminalSummary: normalizeOptionalString(message) ?? "Codex native subagent blocked.", terminalOutcome: "blocked", }); return; @@ -417,9 +423,12 @@ export class CodexNativeSubagentTaskMirror { status: normalizedStatus === "shutdown" ? "cancelled" : "failed", endedAt: eventAt, lastEventAt: eventAt, - error: trimOptional(message) ?? `Codex native subagent status: ${normalizedStatus}`, - progressSummary: trimOptional(message) ?? `Codex native subagent ${normalizedStatus}.`, - terminalSummary: trimOptional(message) ?? "Codex native subagent did not complete.", + error: + normalizeOptionalString(message) ?? `Codex native subagent status: ${normalizedStatus}`, + progressSummary: + normalizeOptionalString(message) ?? `Codex native subagent ${normalizedStatus}.`, + terminalSummary: + normalizeOptionalString(message) ?? "Codex native subagent did not complete.", }); } } @@ -580,8 +589,3 @@ function secondsToMillis(value: number | null | undefined): number | undefined { } return value * 1000; } - -function trimOptional(value: string | null | undefined): string | undefined { - const trimmed = value?.trim(); - return trimmed ? trimmed : undefined; -} diff --git a/extensions/codex/src/app-server/plugin-activation.ts b/extensions/codex/src/app-server/plugin-activation.ts index 1b096b19f70e..a8d5b5739fd7 100644 --- a/extensions/codex/src/app-server/plugin-activation.ts +++ b/extensions/codex/src/app-server/plugin-activation.ts @@ -2,6 +2,7 @@ * Activates curated Codex marketplace plugins and keeps require-active * marketplaces outside OpenClaw's install authority. */ +import { coerceErrorMessage } from "openclaw/plugin-sdk/error-runtime"; import type { CodexAppInventoryCache, CodexAppInventoryRequest } from "./app-inventory-cache.js"; import { CODEX_PLUGINS_MARKETPLACE_NAME, @@ -138,9 +139,7 @@ export async function ensureCodexPluginActivation( marketplace: resolved.marketplace, diagnostics: [ { - message: `Codex plugin install failed: ${ - error instanceof Error ? error.message : String(error) - }`, + message: `Codex plugin install failed: ${coerceErrorMessage(error)}`, }, ], }; @@ -163,9 +162,7 @@ export async function ensureCodexPluginActivation( } catch (error) { refreshFailed = true; refreshDiagnostics.push({ - message: `Codex plugin runtime refresh failed after install: ${ - error instanceof Error ? error.message : String(error) - }`, + message: `Codex plugin runtime refresh failed after install: ${coerceErrorMessage(error)}`, }); } const authRequired = installResponse.appsNeedingAuth.length > 0; @@ -212,7 +209,7 @@ async function refreshCodexPluginRuntimeState(params: { } satisfies v2.HooksListParams) as Promise); } catch (error) { diagnostics.push({ - message: `Codex hooks refresh skipped: ${error instanceof Error ? error.message : String(error)}`, + message: `Codex hooks refresh skipped: ${coerceErrorMessage(error)}`, }); } await params.request("config/mcpServer/reload", undefined); @@ -240,9 +237,7 @@ async function refreshCodexPluginRuntimeState(params: { }); } catch (error) { diagnostics.push({ - message: `Codex app inventory refresh skipped: ${ - error instanceof Error ? error.message : String(error) - }`, + message: `Codex app inventory refresh skipped: ${coerceErrorMessage(error)}`, }); } } diff --git a/extensions/codex/src/app-server/sandbox-exec-server/processes.ts b/extensions/codex/src/app-server/sandbox-exec-server/processes.ts index 045763432f5b..bea19fe6335f 100644 --- a/extensions/codex/src/app-server/sandbox-exec-server/processes.ts +++ b/extensions/codex/src/app-server/sandbox-exec-server/processes.ts @@ -4,6 +4,7 @@ */ import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process"; import { embeddedAgentLog } from "openclaw/plugin-sdk/agent-harness-runtime"; +import { coerceErrorMessage } from "openclaw/plugin-sdk/error-runtime"; import type { WebSocket } from "ws"; import type { JsonObject, JsonValue } from "../protocol.js"; import { requireObject, requireString, requireStringArray } from "./json-rpc.js"; @@ -69,7 +70,7 @@ export async function startProcess( await runProcess(execServer, managed, { argv, cwd, env }); } catch (error) { processes.delete(processId); - managed.failure = error instanceof Error ? error.message : String(error); + managed.failure = coerceErrorMessage(error); managed.exitCode = null; managed.exited = true; managed.closed = true; @@ -114,11 +115,11 @@ async function runProcess( stdio: ["pipe", "pipe", "pipe"], }); } catch (error) { - managed.failure = error instanceof Error ? error.message : String(error); + managed.failure = coerceErrorMessage(error); await finalizeProcess(managed).catch((finalizeError: unknown) => { embeddedAgentLog.warn("codex sandbox exec-server finalize after start failure failed", { processId: managed.processId, - error: finalizeError instanceof Error ? finalizeError.message : String(finalizeError), + error: coerceErrorMessage(finalizeError), }); }); throw error; @@ -209,7 +210,7 @@ function emitProcessClosed(managed: ManagedProcess, exitCode: number | null): vo }); } void finalizeProcess(managed).catch((error: unknown) => { - const message = error instanceof Error ? error.message : String(error); + const message = coerceErrorMessage(error); managed.failure ??= message; embeddedAgentLog.warn("codex sandbox exec-server finalize failed", { processId: managed.processId, diff --git a/extensions/codex/src/migration/apply.ts b/extensions/codex/src/migration/apply.ts index 55f54ad9c965..a2b0f40ddf8f 100644 --- a/extensions/codex/src/migration/apply.ts +++ b/extensions/codex/src/migration/apply.ts @@ -1,6 +1,6 @@ // Codex plugin module implements apply behavior. import path from "node:path"; -import { coerceErrorMessage as formatCodexMigrationError } from "openclaw/plugin-sdk/error-runtime"; +import { coerceErrorMessage } from "openclaw/plugin-sdk/error-runtime"; import { applyMigrationManualItem, markMigrationItemConflict, @@ -273,14 +273,14 @@ async function applyCodexPluginInstallItem( ...item.details, code: "plugin_inventory_unavailable", warningReason: CODEX_PLUGIN_LOAD_WARNING, - diagnostic: formatCodexMigrationError(error), + diagnostic: coerceErrorMessage(error), }, }; } return { ...item, status: "error", - reason: formatCodexMigrationError(error), + reason: coerceErrorMessage(error), details: { ...item.details, code: "plugin_install_failed", @@ -290,7 +290,7 @@ async function applyCodexPluginInstallItem( } function isCodexPluginInventoryLoadError(error: unknown): boolean { - const message = formatCodexMigrationError(error); + const message = coerceErrorMessage(error); return message.includes("codex app-server plugin/list timed out"); } @@ -473,7 +473,7 @@ async function applyCodexPluginConfigItem( if (error instanceof CodexPluginConfigConflictError) { return markMigrationItemConflict(item, error.reason); } - return markMigrationItemError(item, error instanceof Error ? error.message : String(error)); + return markMigrationItemError(item, coerceErrorMessage(error)); } } diff --git a/extensions/codex/src/migration/source.ts b/extensions/codex/src/migration/source.ts index 7a1af2b77561..4f28f606e72d 100644 --- a/extensions/codex/src/migration/source.ts +++ b/extensions/codex/src/migration/source.ts @@ -1,5 +1,6 @@ // Codex plugin module implements source behavior. import path from "node:path"; +import { coerceErrorMessage } from "openclaw/plugin-sdk/error-runtime"; import { isPathInside } from "openclaw/plugin-sdk/security-runtime"; import { defaultCodexAppInventoryCache, @@ -155,7 +156,7 @@ async function discoverInstalledCuratedPlugins( } catch (error) { return { plugins: [], - error: error instanceof Error ? error.message : String(error), + error: coerceErrorMessage(error), }; } } @@ -297,7 +298,7 @@ async function withPluginMigrationEligibility(params: { sourceAccountError = "Codex app-server did not report an authenticated source account."; } } catch (error) { - sourceAccountError = error instanceof Error ? error.message : String(error); + sourceAccountError = coerceErrorMessage(error); } if (sourceAccountError && !params.verifyPluginApps) { for (const { plugin, apps } of pending) { @@ -335,7 +336,7 @@ async function withPluginMigrationEligibility(params: { const snapshot = await refreshSourceAppInventory(params.requestOptions).catch( (error: unknown) => { - const message = error instanceof Error ? error.message : String(error); + const message = coerceErrorMessage(error); for (const { plugin, apps } of pending) { evaluated.push({ ...plugin, @@ -427,7 +428,7 @@ async function readPluginDetail( }); return { ok: true, detail: response.plugin }; } catch (error) { - return { ok: false, error: error instanceof Error ? error.message : String(error) }; + return { ok: false, error: coerceErrorMessage(error) }; } } diff --git a/extensions/discord/src/internal/commands.ts b/extensions/discord/src/internal/commands.ts index db75dbedc00b..e4790f128418 100644 --- a/extensions/discord/src/internal/commands.ts +++ b/extensions/discord/src/internal/commands.ts @@ -7,6 +7,7 @@ import { } from "discord-api-types/v10"; import type { BaseMessageInteractiveComponent } from "./components.js"; import type { AutocompleteInteraction, CommandInteraction } from "./interactions.js"; +import { stripUndefinedFields as clean } from "./undefined-fields.js"; type ConditionalCommandOption = (interaction: unknown) => boolean; type CommandOption = Record & { @@ -25,10 +26,6 @@ type RawSubcommandOption = { options?: RawSubcommandOption[]; }; -function clean>(value: T): T { - return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== undefined)) as T; -} - function resolveConditionalCommandOption( value: boolean | ConditionalCommandOption, interaction: unknown, diff --git a/extensions/discord/src/internal/components.base.ts b/extensions/discord/src/internal/components.base.ts index d8860ad7351d..c2304d578133 100644 --- a/extensions/discord/src/internal/components.base.ts +++ b/extensions/discord/src/internal/components.base.ts @@ -1,6 +1,7 @@ // Discord plugin module implements components.base behavior. import { expectDefined } from "openclaw/plugin-sdk/expect-runtime"; import type { BaseComponentInteraction } from "./interactions.js"; +export { stripUndefinedFields as clean } from "./undefined-fields.js"; export type ComponentParserResult = { key: string; @@ -35,10 +36,6 @@ export function parseCustomId(id: string): ComponentParserResult { return { key, data }; } -export function clean>(value: T): T { - return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== undefined)) as T; -} - export function colorToNumber(value: string | number | undefined): number | undefined { if (typeof value === "number") { return Number.isInteger(value) && value >= 0 && value <= 0xffffff ? value : undefined; diff --git a/extensions/discord/src/internal/embeds.ts b/extensions/discord/src/internal/embeds.ts index a9eef3757f51..a5ac252b5f88 100644 --- a/extensions/discord/src/internal/embeds.ts +++ b/extensions/discord/src/internal/embeds.ts @@ -1,9 +1,6 @@ // Discord plugin module implements embeds behavior. import type { APIEmbed } from "discord-api-types/v10"; - -function clean>(value: T): T { - return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== undefined)) as T; -} +import { stripUndefinedFields as clean } from "./undefined-fields.js"; export class Embed { title?: string; diff --git a/extensions/discord/src/internal/payload.ts b/extensions/discord/src/internal/payload.ts index 6b847a339d44..a13753c61533 100644 --- a/extensions/discord/src/internal/payload.ts +++ b/extensions/discord/src/internal/payload.ts @@ -1,6 +1,7 @@ // Discord plugin module implements payload behavior. import { MessageFlags, type APIEmbed } from "discord-api-types/v10"; import { Embed } from "./embeds.js"; +import { stripUndefinedFields as clean } from "./undefined-fields.js"; export type MessagePayloadFile = { name: string; @@ -29,10 +30,6 @@ export type TopLevelComponents = { serialize: () => unknown; }; -function clean>(value: T): T { - return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== undefined)) as T; -} - function serializeAnyComponent(component: { serialize: () => unknown }): unknown { return component.serialize(); } diff --git a/extensions/discord/src/internal/undefined-fields.ts b/extensions/discord/src/internal/undefined-fields.ts new file mode 100644 index 000000000000..b5b30a1d5eb0 --- /dev/null +++ b/extensions/discord/src/internal/undefined-fields.ts @@ -0,0 +1,4 @@ +// Discord plugin module implements undefined field filtering. +export function stripUndefinedFields(value: T): T { + return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== undefined)) as T; +} diff --git a/extensions/discord/src/send.message-request.ts b/extensions/discord/src/send.message-request.ts index 11fb4270b82c..6db1dccb80be 100644 --- a/extensions/discord/src/send.message-request.ts +++ b/extensions/discord/src/send.message-request.ts @@ -8,6 +8,9 @@ import { type MessagePayloadObject, type TopLevelComponents, } from "./internal/discord.js"; +import { stripUndefinedFields } from "./internal/undefined-fields.js"; + +export { stripUndefinedFields }; const SUPPRESS_EMBEDS_FLAG = MessageFlags.SuppressEmbeds; export const SUPPRESS_NOTIFICATIONS_FLAG = MessageFlags.SuppressNotifications; @@ -130,10 +133,6 @@ export function buildDiscordMessageRequest(params: DiscordMessageRequestParams) }); } -export function stripUndefinedFields(value: T): T { - return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== undefined)) as T; -} - function hasV2Components(components?: TopLevelComponents[]): boolean { return Boolean(components?.some((component) => "isV2" in component && component.isV2)); } diff --git a/extensions/feishu/src/agent-config.ts b/extensions/feishu/src/agent-config.ts index 65b096a842fa..3b987bb0d764 100644 --- a/extensions/feishu/src/agent-config.ts +++ b/extensions/feishu/src/agent-config.ts @@ -1,15 +1,9 @@ // Feishu helper module supports agent config behavior. +import { normalizeAgentId } from "openclaw/plugin-sdk/routing"; import type { ClawdbotConfig } from "./bot-runtime-api.js"; type ReasoningDefault = "on" | "stream" | "off"; -const DEFAULT_AGENT_ID = "main"; - -function normalizeAgentId(value: string | undefined | null): string { - const normalized = (value ?? "").trim().toLowerCase(); - return normalized || DEFAULT_AGENT_ID; -} - export function resolveFeishuConfigReasoningDefault( cfg: ClawdbotConfig, agentId: string, diff --git a/extensions/feishu/src/doctor.ts b/extensions/feishu/src/doctor.ts index 3125dae3ddce..12dea2661654 100644 --- a/extensions/feishu/src/doctor.ts +++ b/extensions/feishu/src/doctor.ts @@ -17,7 +17,10 @@ import { resolveStorePath, } from "openclaw/plugin-sdk/session-store-runtime"; import { resolveStateDir } from "openclaw/plugin-sdk/state-paths"; -import { isRecord } from "openclaw/plugin-sdk/string-coerce-runtime"; +import { + isRecord, + normalizeLowercaseStringOrEmpty, +} from "openclaw/plugin-sdk/string-coerce-runtime"; import { legacyConfigRules, normalizeCompatibilityConfig } from "./doctor-contract.js"; const FEISHU_STATE_DIR = "feishu"; @@ -175,10 +178,6 @@ function isFeishuAcpBindingSessionKey(key: string): boolean { return /^agent:[^:]+:acp:binding:feishu(?::|$)/.test(key.trim().toLowerCase()); } -function normalizeMetadataString(value: unknown): string { - return typeof value === "string" ? value.trim().toLowerCase() : ""; -} - function isFeishuSessionEntry(key: string, value: unknown): boolean { if (isFeishuAcpBindingSessionKey(key)) { return false; @@ -190,29 +189,29 @@ function isFeishuSessionEntry(key: string, value: unknown): boolean { return false; } if ( - normalizeMetadataString(value.channel) === "feishu" || - normalizeMetadataString(value.lastChannel) === "feishu" + normalizeLowercaseStringOrEmpty(value.channel) === "feishu" || + normalizeLowercaseStringOrEmpty(value.lastChannel) === "feishu" ) { return true; } const route = isRecord(value.route) ? value.route : null; - if (normalizeMetadataString(route?.channel) === "feishu") { + if (normalizeLowercaseStringOrEmpty(route?.channel) === "feishu") { return true; } const deliveryContext = isRecord(value.deliveryContext) ? value.deliveryContext : null; - if (normalizeMetadataString(deliveryContext?.channel) === "feishu") { + if (normalizeLowercaseStringOrEmpty(deliveryContext?.channel) === "feishu") { return true; } const pendingDeliveryContext = isRecord(value.pendingFinalDeliveryContext) ? value.pendingFinalDeliveryContext : null; - if (normalizeMetadataString(pendingDeliveryContext?.channel) === "feishu") { + if (normalizeLowercaseStringOrEmpty(pendingDeliveryContext?.channel) === "feishu") { return true; } const origin = isRecord(value.origin) ? value.origin : null; - const originProvider = normalizeMetadataString(origin?.provider); - const originSurface = normalizeMetadataString(origin?.surface); - const originFrom = normalizeMetadataString(origin?.from); + const originProvider = normalizeLowercaseStringOrEmpty(origin?.provider); + const originSurface = normalizeLowercaseStringOrEmpty(origin?.surface); + const originFrom = normalizeLowercaseStringOrEmpty(origin?.from); return ( originProvider === "feishu" || originSurface.startsWith("feishu") || diff --git a/extensions/google-meet/lazy-import.test.ts b/extensions/google-meet/lazy-import.test.ts index 5d2bfaf0935f..b0f1591022f4 100644 --- a/extensions/google-meet/lazy-import.test.ts +++ b/extensions/google-meet/lazy-import.test.ts @@ -108,7 +108,7 @@ describe("google-meet lazy imports", () => { vi.doMock("openclaw/plugin-sdk/routing", () => { routingImports += 1; return { - normalizeAgentId: (value: string) => value, + normalizeAgentId: vi.fn((value: string) => value), parseAgentSessionKey: () => ({ agentId: "main" }), }; }); diff --git a/extensions/imessage/src/monitor/monitor-provider.ts b/extensions/imessage/src/monitor/monitor-provider.ts index eb61d9ad255e..9a1c1e88a5c4 100644 --- a/extensions/imessage/src/monitor/monitor-provider.ts +++ b/extensions/imessage/src/monitor/monitor-provider.ts @@ -45,6 +45,7 @@ import { resolveStorePath, } from "openclaw/plugin-sdk/session-store-runtime"; import { openNodeSqliteDatabase } from "openclaw/plugin-sdk/sqlite-runtime"; +import { normalizeStringEntries } from "openclaw/plugin-sdk/string-coerce-runtime"; import { sliceUtf16Safe, truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime"; import { waitForTransportReady } from "openclaw/plugin-sdk/transport-ready-runtime"; import { resolveIMessageAccount } from "../accounts.js"; @@ -112,7 +113,7 @@ import { loadIMessageRecoveryCursor, resolveIMessageRecoveryCursorDbIdentity, } from "./recovery-cursor.js"; -import { normalizeAllowList, resolveRuntime } from "./runtime.js"; +import { resolveRuntime } from "./runtime.js"; import { createSelfChatCache } from "./self-chat-cache.js"; import type { IMessageAttachment, IMessagePayload, MonitorIMessageOpts } from "./types.js"; import { sanitizeIMessageWatchErrorPayload } from "./watch-error-log.js"; @@ -371,9 +372,9 @@ export async function monitorIMessageProvider(opts: MonitorIMessageOpts = {}): P const selfChatCache = createSelfChatCache(); const loopRateLimiter = createLoopRateLimiter(); const textLimit = resolveTextChunkLimit(cfg, "imessage", accountInfo.accountId); - const allowFrom = normalizeAllowList(opts.allowFrom ?? imessageCfg.allowFrom); + const allowFrom = normalizeStringEntries(opts.allowFrom ?? imessageCfg.allowFrom); const configuredGroupAllowFrom = opts.groupAllowFrom ?? imessageCfg.groupAllowFrom; - const groupAllowFrom = normalizeAllowList( + const groupAllowFrom = normalizeStringEntries( configuredGroupAllowFrom ?? (imessageCfg.allowFrom && imessageCfg.allowFrom.length > 0 ? imessageCfg.allowFrom : []), ); diff --git a/extensions/imessage/src/monitor/runtime.ts b/extensions/imessage/src/monitor/runtime.ts index b53d131577f4..2c155c699ef3 100644 --- a/extensions/imessage/src/monitor/runtime.ts +++ b/extensions/imessage/src/monitor/runtime.ts @@ -1,12 +1,7 @@ // Imessage plugin module implements runtime behavior. import { createNonExitingRuntime, type RuntimeEnv } from "openclaw/plugin-sdk/runtime-env"; -import { normalizeStringEntries } from "openclaw/plugin-sdk/string-coerce-runtime"; import type { MonitorIMessageOpts } from "./types.js"; export function resolveRuntime(opts: MonitorIMessageOpts): RuntimeEnv { return opts.runtime ?? createNonExitingRuntime(); } - -export function normalizeAllowList(list?: Array) { - return normalizeStringEntries(list); -} diff --git a/extensions/matrix/src/account-selection.ts b/extensions/matrix/src/account-selection.ts index 52d6d7fa14ca..ed8f751770e8 100644 --- a/extensions/matrix/src/account-selection.ts +++ b/extensions/matrix/src/account-selection.ts @@ -12,13 +12,12 @@ import { } from "openclaw/plugin-sdk/account-id"; import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts"; import { hasConfiguredSecretInput } from "openclaw/plugin-sdk/secret-input"; -import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime"; +import { isRecord, normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime"; import { resolveMatrixAccountStringValues, type MatrixResolvedStringField, } from "./auth-precedence.js"; import { getMatrixScopedEnvVarNames, listMatrixEnvAccountIds } from "./env-vars.js"; -import { isRecord } from "./record-shared.js"; type MatrixTopologyStringSources = Partial>; diff --git a/extensions/matrix/src/doctor-contract.ts b/extensions/matrix/src/doctor-contract.ts index c269f9c83cbf..a11ac60f9e8b 100644 --- a/extensions/matrix/src/doctor-contract.ts +++ b/extensions/matrix/src/doctor-contract.ts @@ -13,7 +13,7 @@ import { migrateLegacyFlatAllowPrivateNetworkAlias, stripRetiredChannelKeys, } from "openclaw/plugin-sdk/runtime-doctor-migrations"; -import { isRecord } from "./record-shared.js"; +import { isRecord } from "openclaw/plugin-sdk/string-coerce-runtime"; import type { MatrixStreamingMode } from "./types.js"; function parseMatrixStreamingMode(value: unknown): MatrixStreamingMode | null { diff --git a/extensions/matrix/src/matrix/client/file-sync-store.ts b/extensions/matrix/src/matrix/client/file-sync-store.ts index 7e611ac89fdc..39851a0e77b6 100644 --- a/extensions/matrix/src/matrix/client/file-sync-store.ts +++ b/extensions/matrix/src/matrix/client/file-sync-store.ts @@ -15,7 +15,7 @@ import type { PluginStateKeyedStore, PluginStateSyncKeyedStore, } from "openclaw/plugin-sdk/plugin-state-runtime"; -import { isRecord } from "../../record-shared.js"; +import { isRecord } from "openclaw/plugin-sdk/string-coerce-runtime"; import { getMatrixRuntime } from "../../runtime.js"; import { createAsyncLock } from "../async-lock.js"; import { LogService } from "../sdk/logger.js"; diff --git a/extensions/matrix/src/matrix/client/storage.ts b/extensions/matrix/src/matrix/client/storage.ts index c779c45d2f25..6007750ff3b3 100644 --- a/extensions/matrix/src/matrix/client/storage.ts +++ b/extensions/matrix/src/matrix/client/storage.ts @@ -8,7 +8,7 @@ import type { PluginStateKeyedStore, PluginStateSyncKeyedStore, } from "openclaw/plugin-sdk/plugin-state-runtime"; -import { isRecord } from "../../record-shared.js"; +import { isRecord } from "openclaw/plugin-sdk/string-coerce-runtime"; import { getMatrixRuntime } from "../../runtime.js"; import { resolveMatrixAccountStorageRoot } from "../../storage-paths.js"; import { diff --git a/extensions/matrix/src/matrix/crypto-state-store.ts b/extensions/matrix/src/matrix/crypto-state-store.ts index 1d50762e5ab9..dac7e5589f66 100644 --- a/extensions/matrix/src/matrix/crypto-state-store.ts +++ b/extensions/matrix/src/matrix/crypto-state-store.ts @@ -6,7 +6,7 @@ import type { PluginStateKeyedStore, PluginStateSyncKeyedStore, } from "openclaw/plugin-sdk/plugin-state-runtime"; -import { isRecord } from "../record-shared.js"; +import { isRecord } from "openclaw/plugin-sdk/string-coerce-runtime"; import { getMatrixRuntime } from "../runtime.js"; import type { MatrixStoredRecoveryKey } from "./sdk/types.js"; import { resolveMatrixSqliteStateEnv } from "./sqlite-state.js"; diff --git a/extensions/matrix/src/matrix/monitor/inbound-dedupe-migration.ts b/extensions/matrix/src/matrix/monitor/inbound-dedupe-migration.ts index 195021758b69..9973bc6b496b 100644 --- a/extensions/matrix/src/matrix/monitor/inbound-dedupe-migration.ts +++ b/extensions/matrix/src/matrix/monitor/inbound-dedupe-migration.ts @@ -19,7 +19,7 @@ import type { DatabaseSync } from "node:sqlite"; // plugin-state-store/kysely graph, so the value import stays lazy below. import type { PersistentDedupeEntry } from "openclaw/plugin-sdk/persistent-dedupe"; import type { PluginDoctorStateMigrationContext } from "openclaw/plugin-sdk/runtime-doctor-migrations"; -import { isRecord } from "../../record-shared.js"; +import { isRecord } from "openclaw/plugin-sdk/string-coerce-runtime"; import { normalizeMatrixStorageMetadata } from "../client/storage.js"; const LEGACY_SQLITE_NAMESPACE = "inbound-dedupe"; diff --git a/extensions/matrix/src/record-shared.ts b/extensions/matrix/src/record-shared.ts deleted file mode 100644 index 5dbea4ab20ea..000000000000 --- a/extensions/matrix/src/record-shared.ts +++ /dev/null @@ -1,4 +0,0 @@ -// Matrix plugin module implements record shared behavior. -import { isRecord } from "openclaw/plugin-sdk/string-coerce-runtime"; - -export { isRecord }; diff --git a/extensions/memory-core/src/migration/doctor-memory-sidecar.ts b/extensions/memory-core/src/migration/doctor-memory-sidecar.ts index a4dc410cc003..e5a351259137 100644 --- a/extensions/memory-core/src/migration/doctor-memory-sidecar.ts +++ b/extensions/memory-core/src/migration/doctor-memory-sidecar.ts @@ -11,6 +11,8 @@ import { legacyStateFileExists, type PluginDoctorStateMigration, } from "openclaw/plugin-sdk/runtime-doctor-migrations"; +// This doctor closure must stay dependency-light while accepting legacy array-backed objects. +import { asOptionalObjectRecord as readLegacyObjectRecord } from "openclaw/plugin-sdk/string-coerce-runtime"; // sqlite-runtime re-exports the agent-db/kysely graph; keep it lazy so doctor // enumeration does not cold-load it with this closure. import { @@ -26,13 +28,6 @@ function formatLegacyVectorRows(count: number | undefined): string { type MemoryFtsTokenizer = "unicode61" | "trigram"; -// This doctor closure must stay dependency-light while accepting legacy array-backed objects. -function readLegacyObjectRecord(value: unknown): Record | undefined { - return value !== null && typeof value === "object" - ? (value as Record) - : undefined; -} - function resolveConfiguredAgentIds(config: unknown): string[] { const cfg = config as { agents?: { entries?: unknown; list?: unknown } }; const entries = readLegacyObjectRecord(cfg.agents?.entries); diff --git a/extensions/msteams/src/attachments/graph.ts b/extensions/msteams/src/attachments/graph.ts index 1ff932787bdf..5be5b3412291 100644 --- a/extensions/msteams/src/attachments/graph.ts +++ b/extensions/msteams/src/attachments/graph.ts @@ -285,10 +285,10 @@ export async function downloadMSTeamsGraphMedia(params: { } catch (err) { params.logger?.debug?.("graph media token acquisition failed", { messageUrl, - error: err instanceof Error ? err.message : String(err), + error: coerceErrorMessage(err), }); params.logger?.warn?.("msteams graph token acquisition failed", { - error: err instanceof Error ? err.message : String(err), + error: coerceErrorMessage(err), }); return { media: [], messageUrl, tokenError: true }; } diff --git a/extensions/msteams/src/reply-stream-controller.ts b/extensions/msteams/src/reply-stream-controller.ts index fef311bc6f1f..3972b978bf5d 100644 --- a/extensions/msteams/src/reply-stream-controller.ts +++ b/extensions/msteams/src/reply-stream-controller.ts @@ -11,6 +11,7 @@ import { resolveChannelProgressDraftMaxLines, resolveChannelStreamingPreviewToolProgress, } from "openclaw/plugin-sdk/channel-outbound"; +import { coerceErrorMessage } from "openclaw/plugin-sdk/error-runtime"; import { normalizeOptionalLowercaseString } from "openclaw/plugin-sdk/string-coerce-runtime"; import type { MSTeamsConfig, ReplyPayload } from "../runtime-api.js"; import { extractMessageId } from "./media-helpers.js"; @@ -280,9 +281,7 @@ export function createTeamsReplyStreamController(params: { canceledLocally = true; return; } - params.log?.debug?.( - `stream informative update failed: ${err instanceof Error ? err.message : String(err)}`, - ); + params.log?.debug?.(`stream informative update failed: ${coerceErrorMessage(err)}`); } }; @@ -371,7 +370,7 @@ export function createTeamsReplyStreamController(params: { // cumulative prefix Teams accepted; failed emits prove no delivery. streamFailed = true; params.log?.warn?.( - `msteams stream emit failed, falling back to block delivery: ${err instanceof Error ? err.message : String(err)}`, + `msteams stream emit failed, falling back to block delivery: ${coerceErrorMessage(err)}`, ); } }, @@ -500,7 +499,7 @@ export function createTeamsReplyStreamController(params: { streamFailed = true; replacementEmitFailed = true; params.log?.warn?.( - `msteams stream replacement failed, falling back to block delivery: ${err instanceof Error ? err.message : String(err)}`, + `msteams stream replacement failed, falling back to block delivery: ${coerceErrorMessage(err)}`, ); // Retain ownership until finalize so payloads held before this // failed replacement cannot be overtaken by this or later blocks. @@ -549,9 +548,7 @@ export function createTeamsReplyStreamController(params: { } // Non-cancel emit failure: fall through to block delivery as a // safety net so the user still sees the final reply. - params.log?.debug?.( - `progress-mode finalize failed: ${err instanceof Error ? err.message : String(err)}`, - ); + params.log?.debug?.(`progress-mode finalize failed: ${coerceErrorMessage(err)}`); } } return payload; @@ -669,9 +666,7 @@ export function createTeamsReplyStreamController(params: { // the reply pipeline after the user already saw the response. streamFailed = true; streamFinalizationPending = false; - params.log?.warn?.( - `msteams stream finalize failed: ${err instanceof Error ? err.message : String(err)}`, - ); + params.log?.warn?.(`msteams stream finalize failed: ${coerceErrorMessage(err)}`); const fallback = pendingFinalPayload; pendingFinalPayload = undefined; const replacementFallback = diff --git a/extensions/ollama/src/ollama-json.ts b/extensions/ollama/src/ollama-json.ts deleted file mode 100644 index ad988d216df6..000000000000 --- a/extensions/ollama/src/ollama-json.ts +++ /dev/null @@ -1,5 +0,0 @@ -// Ollama plugin module implements ollama json behavior. -export { - parseJsonObjectPreservingUnsafeIntegers, - parseJsonPreservingUnsafeIntegers, -} from "openclaw/plugin-sdk/json-unsafe-integers"; diff --git a/extensions/ollama/src/stream.runtime.ts b/extensions/ollama/src/stream.runtime.ts index a3dd7a0f7475..9989fd8d26f9 100644 --- a/extensions/ollama/src/stream.runtime.ts +++ b/extensions/ollama/src/stream.runtime.ts @@ -2,6 +2,10 @@ import { randomUUID } from "node:crypto"; import type { StreamFn } from "openclaw/plugin-sdk/agent-core"; import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime"; +import { + parseJsonObjectPreservingUnsafeIntegers, + parseJsonPreservingUnsafeIntegers, +} from "openclaw/plugin-sdk/json-unsafe-integers"; import type { AssistantMessage, StopReason, @@ -26,10 +30,6 @@ import { import { estimateStringChars, truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime"; import { OLLAMA_CLOUD_BASE_URL, OLLAMA_DEFAULT_BASE_URL } from "./defaults.js"; import { normalizeOllamaWireModelId } from "./model-id.js"; -import { - parseJsonObjectPreservingUnsafeIntegers, - parseJsonPreservingUnsafeIntegers, -} from "./ollama-json.js"; import { buildOllamaBaseUrlSsrFPolicy, isOllamaCloudModel } from "./provider-models.js"; import { createOllamaVisibleContentSanitizer, diff --git a/extensions/openai/openai-provider.ts b/extensions/openai/openai-provider.ts index c2c797028193..4f1541c18c8e 100644 --- a/extensions/openai/openai-provider.ts +++ b/extensions/openai/openai-provider.ts @@ -7,6 +7,9 @@ import { createProviderApiKeyAuthMethod } from "openclaw/plugin-sdk/provider-aut import { getCachedLiveProviderModelRows, LiveModelCatalogHttpError, + readLiveModelCatalogBooleanField, + readLiveModelCatalogPositiveSafeIntegerField, + readLiveModelCatalogStringField, type LiveModelCatalogFetchGuard, } from "openclaw/plugin-sdk/provider-catalog-live-runtime"; import { @@ -342,28 +345,6 @@ async function buildOpenAILiveProviderConfig( } } -function readCodexModelString(row: unknown, key: string): string | undefined { - if (!row || typeof row !== "object" || Array.isArray(row)) { - return undefined; - } - const value = (row as Record)[key]; - return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined; -} - -function readCodexModelPositiveInteger(row: unknown, keys: readonly string[]): number | undefined { - if (!row || typeof row !== "object" || Array.isArray(row)) { - return undefined; - } - const record = row as Record; - for (const key of keys) { - const value = record[key]; - if (typeof value === "number" && Number.isSafeInteger(value) && value > 0) { - return value; - } - } - return undefined; -} - function readCodexModelStringArray(row: unknown, keys: readonly string[]): readonly string[] { if (!row || typeof row !== "object" || Array.isArray(row)) { return []; @@ -399,14 +380,6 @@ function readCodexReasoningLevels(row: unknown): readonly string[] | undefined { }); } -function readCodexModelBoolean(row: unknown, key: string): boolean | undefined { - if (!row || typeof row !== "object" || Array.isArray(row)) { - return undefined; - } - const value = (row as Record)[key]; - return typeof value === "boolean" ? value : undefined; -} - function readCodexModelRows(body: unknown): readonly unknown[] { if (!body || typeof body !== "object" || Array.isArray(body)) { throw new Error("OpenAI Codex model discovery response must be { models: [] }"); @@ -419,12 +392,15 @@ function readCodexModelRows(body: unknown): readonly unknown[] { } function shouldIncludeCodexModelRow(row: unknown): boolean { - const visibility = normalizeLowercaseStringOrEmpty(readCodexModelString(row, "visibility") ?? ""); + const visibility = normalizeLowercaseStringOrEmpty( + readLiveModelCatalogStringField(row, "visibility") ?? "", + ); if (visibility && visibility !== "list") { return false; } const showInPicker = - readCodexModelBoolean(row, "show_in_picker") ?? readCodexModelBoolean(row, "showInPicker"); + readLiveModelCatalogBooleanField(row, "show_in_picker") ?? + readLiveModelCatalogBooleanField(row, "showInPicker"); return showInPicker !== false; } @@ -496,7 +472,8 @@ function buildOpenAICodexModelFromLiveRow(row: unknown): ModelDefinitionConfig | if (!shouldIncludeCodexModelRow(row)) { return undefined; } - const modelId = readCodexModelString(row, "slug") ?? readCodexModelString(row, "id"); + const modelId = + readLiveModelCatalogStringField(row, "slug") ?? readLiveModelCatalogStringField(row, "id"); if (!modelId) { return undefined; } @@ -506,7 +483,7 @@ function buildOpenAICodexModelFromLiveRow(row: unknown): ModelDefinitionConfig | const normalizedModelId = normalizeLowercaseStringOrEmpty(modelId); const fallback = resolveCodexModelFallback(modelId); const reasoningLevels = readCodexReasoningLevels(row); - const observedContextTokens = readCodexModelPositiveInteger(row, [ + const observedContextTokens = readLiveModelCatalogPositiveSafeIntegerField(row, [ "context_window", "contextWindow", ]); @@ -518,12 +495,12 @@ function buildOpenAICodexModelFromLiveRow(row: unknown): ModelDefinitionConfig | ) : observedContextTokens; const contextWindow = - readCodexModelPositiveInteger(row, ["max_context_window", "maxContextWindow"]) ?? + readLiveModelCatalogPositiveSafeIntegerField(row, ["max_context_window", "maxContextWindow"]) ?? fallback?.contextWindow ?? observedContextTokens ?? DEFAULT_CONTEXT_TOKENS; const maxTokens = - readCodexModelPositiveInteger(row, [ + readLiveModelCatalogPositiveSafeIntegerField(row, [ "max_output_tokens", "maxOutputTokens", "max_completion_tokens", @@ -548,7 +525,7 @@ function buildOpenAICodexModelFromLiveRow(row: unknown): ModelDefinitionConfig | return normalizeOpenAICodexCatalogModel({ id: modelId, - name: readCodexModelString(row, "display_name") ?? fallback?.name ?? modelId, + name: readLiveModelCatalogStringField(row, "display_name") ?? fallback?.name ?? modelId, api: "openai-chatgpt-responses", baseUrl: OPENAI_CODEX_RESPONSES_BASE_URL, reasoning: (reasoningLevels?.length ?? 0) > 0 || fallback?.reasoning || false, diff --git a/extensions/openai/realtime-voice-bridge.ts b/extensions/openai/realtime-voice-bridge.ts index fbcee5b08044..83c972b2c033 100644 --- a/extensions/openai/realtime-voice-bridge.ts +++ b/extensions/openai/realtime-voice-bridge.ts @@ -1,5 +1,5 @@ import { randomUUID } from "node:crypto"; -import { toStringifiedError } from "openclaw/plugin-sdk/error-runtime"; +import { coerceErrorMessage, toStringifiedError } from "openclaw/plugin-sdk/error-runtime"; import { resolveProviderRequestHeaders } from "openclaw/plugin-sdk/provider-http"; import { captureWsEvent, @@ -324,7 +324,7 @@ export class OpenAIRealtimeBridge extends OpenAIRealtimeEvents implements Realti direction: "local", kind: "error", flowId: this.flowId, - errorText: error instanceof Error ? error.message : String(error), + errorText: coerceErrorMessage(error), meta: { provider: "openai", capability: "realtime-voice", diff --git a/extensions/openai/thinking-policy.ts b/extensions/openai/thinking-policy.ts index c05ecc25ea56..648ef4feccaf 100644 --- a/extensions/openai/thinking-policy.ts +++ b/extensions/openai/thinking-policy.ts @@ -3,6 +3,7 @@ import type { ProviderDefaultThinkingPolicyContext, ProviderThinkingProfile, } from "openclaw/plugin-sdk/plugin-entry"; +import { normalizeLowercaseStringOrEmpty as normalizeModelId } from "openclaw/plugin-sdk/string-coerce-runtime"; import { OPENAI_GPT_53_CODEX_SPARK_MODEL_ID, OPENAI_GPT_54_MINI_MODEL_ID, @@ -53,10 +54,6 @@ const OPENAI_UNIFIED_XHIGH_MODEL_IDS = [ OPENAI_GPT_54_NANO_MODEL_ID, ] as const; -function normalizeModelId(value: string): string { - return value.trim().toLowerCase(); -} - function matchesExactOrPrefix(id: string, values: readonly string[]): boolean { const normalizedId = normalizeModelId(id); return values.some((value) => { diff --git a/extensions/parallel/src/parallel-search-normalize.ts b/extensions/parallel/src/parallel-search-normalize.ts index 0039237c29f7..219fb07f67d4 100644 --- a/extensions/parallel/src/parallel-search-normalize.ts +++ b/extensions/parallel/src/parallel-search-normalize.ts @@ -12,7 +12,10 @@ import { resolveSiteName, wrapWebContent, } from "openclaw/plugin-sdk/provider-web-search"; -import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime"; +import { + normalizeBoundedOptionalString, + normalizeOptionalString, +} from "openclaw/plugin-sdk/string-coerce-runtime"; import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime"; // Internal-only bounds (the model-facing tool schema declares its own copies). @@ -30,6 +33,11 @@ export const PARALLEL_SESSION_ID_MAX_LENGTH = 1000; export const PARALLEL_FREE_SESSION_ID_MAX_LENGTH = 100; const PARALLEL_CLIENT_MODEL_MAX_LENGTH = 100; +export const normalizeParallelSessionId: ( + value: string | undefined, + maxLength: number, +) => string | undefined = normalizeBoundedOptionalString; + type ParallelSearchResult = { title?: unknown; url?: unknown; @@ -93,14 +101,6 @@ export function resolveParallelSearchCount( }); } -export function normalizeParallelSessionId( - value: string | undefined, - maxLength: number, -): string | undefined { - const trimmed = normalizeOptionalString(value); - return trimmed && trimmed.length <= maxLength ? trimmed : undefined; -} - export function normalizeParallelObjective(value: string | undefined): string | undefined { const trimmed = normalizeOptionalString(value); if (!trimmed) { diff --git a/extensions/policy/src/doctor/policy-runtime.ts b/extensions/policy/src/doctor/policy-runtime.ts index 97292ff22612..d2cca4d617b5 100644 --- a/extensions/policy/src/doctor/policy-runtime.ts +++ b/extensions/policy/src/doctor/policy-runtime.ts @@ -6,7 +6,10 @@ import { } from "openclaw/plugin-sdk/exec-approvals-runtime"; import type { HealthCheckContext, HealthFinding } from "openclaw/plugin-sdk/health"; import { createLazyRuntimeModule } from "openclaw/plugin-sdk/lazy-runtime"; -import { isRecord } from "openclaw/plugin-sdk/string-coerce-runtime"; +import { + isRecord, + normalizeLowercaseStringOrEmpty, +} from "openclaw/plugin-sdk/string-coerce-runtime"; import { EXEC_APPROVALS_POLICY_DOCUMENT_NAME } from "../exec-approvals-uri.js"; import type { PolicyAuthProfileEvidence } from "../policy-state.js"; import { CHECK_IDS } from "./check-ids.js"; @@ -16,6 +19,8 @@ import { } from "./policy-constants.js"; import { readPolicyStringArray } from "./utils.js"; +export const normalizePolicyChannelId: (value: string) => string = normalizeLowercaseStringOrEmpty; + const loadFsPromisesModule = createLazyRuntimeModule(() => import("node:fs/promises")); export async function readPolicyFile( @@ -287,10 +292,6 @@ export function authProfileHasMetadata( ); } -export function normalizePolicyChannelId(value: string): string { - return value.trim().toLowerCase(); -} - export function execApprovalsDisplayName(): string { return resolveExecApprovalsDisplayPath(); } diff --git a/extensions/policy/src/doctor/utils.ts b/extensions/policy/src/doctor/utils.ts index 11b644a7743d..333b5fc54cf1 100644 --- a/extensions/policy/src/doctor/utils.ts +++ b/extensions/policy/src/doctor/utils.ts @@ -1,5 +1,6 @@ // Shared policy doctor value readers. import { isRecord } from "openclaw/plugin-sdk/string-coerce-runtime"; +export { readBooleanPath as readPolicyBoolean } from "../policy-state-helpers.js"; export function readPolicyStringArray( policy: unknown, @@ -50,14 +51,3 @@ export function ocPathSegment(value: string): string { } return JSON.stringify(value); } - -export function readPolicyBoolean(policy: unknown, path: readonly string[]): boolean | undefined { - let current: unknown = policy; - for (const part of path) { - if (!isRecord(current)) { - return undefined; - } - current = current[part]; - } - return typeof current === "boolean" ? current : undefined; -} diff --git a/extensions/qa-lab/src/errors.ts b/extensions/qa-lab/src/errors.ts index ee087eea8044..9f4b0cec3c19 100644 --- a/extensions/qa-lab/src/errors.ts +++ b/extensions/qa-lab/src/errors.ts @@ -1,4 +1,10 @@ // Qa Lab plugin module defines shared suite errors. +import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime"; + +export function toQaError(value: unknown): Error { + return value instanceof Error ? value : new Error(formatErrorMessage(value)); +} + type QaSuiteArtifactErrorCode = | "evidence_missing" | "report_missing" diff --git a/extensions/qa-lab/src/lab-server.ts b/extensions/qa-lab/src/lab-server.ts index 0e09481b7392..bfcf836741e5 100644 --- a/extensions/qa-lab/src/lab-server.ts +++ b/extensions/qa-lab/src/lab-server.ts @@ -18,6 +18,7 @@ import { writeQaRequestBodyLimitError, } from "./bus-server.js"; import { createQaBusState, type QaBusState } from "./bus-state.js"; +import { toQaError } from "./errors.js"; import { QaEvidenceGalleryError, buildQaEvidenceGalleryModel, @@ -220,10 +221,6 @@ function createQaLabConfig(baseUrl: string): OpenClawConfig { return createQaChannelGatewayConfig({ baseUrl }); } -function normalizeQaLabCleanupError(error: unknown): Error { - return error instanceof Error ? error : new Error(formatErrorMessage(error)); -} - function detectQaEvidenceArtifactContentType(filePath: string): string { const lower = filePath.toLowerCase(); if (lower.endsWith(".png")) { @@ -589,7 +586,7 @@ export async function startQaLabServer( return; } fs.createReadStream(artifactFile) - .on("error", (error) => res.destroy(normalizeQaLabCleanupError(error))) + .on("error", (error) => res.destroy(toQaError(error))) .pipe(res); return; } @@ -957,14 +954,14 @@ export async function startQaLabServer( try { await gateway?.stop(); } catch (error) { - cleanupError = normalizeQaLabCleanupError(error); + cleanupError = toQaError(error); } const results = await Promise.allSettled([ Promise.resolve().then(() => (serverListening ? closeQaHttpServer(server) : undefined)), Promise.resolve().then(releaseCaptureStore), ]); const failed = results.find((result) => result.status === "rejected"); - return cleanupError ?? (failed ? normalizeQaLabCleanupError(failed.reason) : undefined); + return cleanupError ?? (failed ? toQaError(failed.reason) : undefined); }; try { diff --git a/extensions/qa-lab/src/live-transports/whatsapp/whatsapp-live.contracts.ts b/extensions/qa-lab/src/live-transports/whatsapp/whatsapp-live.contracts.ts index cd7020a8aae0..c11cbf28c00b 100644 --- a/extensions/qa-lab/src/live-transports/whatsapp/whatsapp-live.contracts.ts +++ b/extensions/qa-lab/src/live-transports/whatsapp/whatsapp-live.contracts.ts @@ -3,8 +3,8 @@ import type { WhatsAppQaDriverObservedMessage, WhatsAppQaDriverSession, } from "@openclaw/whatsapp/api.js"; -import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime"; import type { startQaGatewayChild } from "../../gateway-child.js"; +export { toQaError as toWhatsAppQaError } from "../../errors.js"; export type WhatsAppQaRuntimeEnv = { driverAuthArchiveBase64: string; @@ -19,10 +19,6 @@ export type WhatsAppQaApprovalDecision = "allow-once" | "deny"; type WhatsAppQaApprovalDecisionMode = "reaction" | "rpc"; type WhatsAppQaScenarioPosture = "direct-gateway" | "native-approval" | "user-path"; -export function toWhatsAppQaError(error: unknown): Error { - return error instanceof Error ? error : new Error(formatErrorMessage(error)); -} - type WhatsAppQaMessageSendMode = | { kind?: "text"; diff --git a/extensions/qa-lab/src/mantis/slack-desktop-smoke.runtime.ts b/extensions/qa-lab/src/mantis/slack-desktop-smoke.runtime.ts index 88c16fa242f3..a403f072dafb 100644 --- a/extensions/qa-lab/src/mantis/slack-desktop-smoke.runtime.ts +++ b/extensions/qa-lab/src/mantis/slack-desktop-smoke.runtime.ts @@ -4,6 +4,7 @@ import path from "node:path"; import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime"; import { pathExists } from "openclaw/plugin-sdk/security-runtime"; import { ensureRepoBoundDirectory, resolveRepoRelativeOutputDir } from "../cli-paths.js"; +import { toQaError } from "../errors.js"; import { acquireQaCredentialLease, startQaCredentialLeaseHeartbeat, @@ -1464,7 +1465,7 @@ export async function runMantisSlackDesktopSmoke( timer.updatePhaseStatus("crabbox.remote_run", "accepted"); } if (remoteRunError && !gatewaySetupCompleted && !slackQaCompleted) { - throw toMantisError(remoteRunError); + throw toQaError(remoteRunError); } if (gatewaySetup && !gatewaySetupCompleted) { throw new Error("Slack desktop gateway setup did not report a live OpenClaw gateway."); @@ -1580,7 +1581,4 @@ export async function runMantisSlackDesktopSmoke( } } -function toMantisError(error: unknown): Error { - return error instanceof Error ? error : new Error(formatErrorMessage(error)); -} /* oxlint-disable max-lines -- TODO: split this grandfathered oversized file. */ diff --git a/extensions/qa-lab/src/mantis/telegram-desktop-builder.runtime.ts b/extensions/qa-lab/src/mantis/telegram-desktop-builder.runtime.ts index c1d7bee10337..54638b1e23ba 100644 --- a/extensions/qa-lab/src/mantis/telegram-desktop-builder.runtime.ts +++ b/extensions/qa-lab/src/mantis/telegram-desktop-builder.runtime.ts @@ -4,6 +4,7 @@ import path from "node:path"; import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime"; import { pathExists } from "openclaw/plugin-sdk/security-runtime"; import { ensureRepoBoundDirectory, resolveRepoRelativeOutputDir } from "../cli-paths.js"; +import { toQaError } from "../errors.js"; import { acquireQaCredentialLease, startQaCredentialLeaseHeartbeat, @@ -727,7 +728,7 @@ export async function runMantisTelegramDesktopBuilder( timer.updatePhaseStatus("crabbox.remote_run", "accepted"); } if (remoteRunError && !gatewaySetupCompleted) { - throw toMantisError(remoteRunError); + throw toQaError(remoteRunError); } if (gatewaySetup && !gatewaySetupCompleted) { throw new Error("Telegram desktop builder did not report a live OpenClaw gateway."); @@ -832,7 +833,4 @@ export async function runMantisTelegramDesktopBuilder( } } -function toMantisError(error: unknown): Error { - return error instanceof Error ? error : new Error(formatErrorMessage(error)); -} /* oxlint-disable max-lines -- TODO: split this grandfathered oversized file. */ diff --git a/extensions/qa-lab/src/mantis/visual-task.runtime.ts b/extensions/qa-lab/src/mantis/visual-task.runtime.ts index 32afa022709b..c5d96ff4e556 100644 --- a/extensions/qa-lab/src/mantis/visual-task.runtime.ts +++ b/extensions/qa-lab/src/mantis/visual-task.runtime.ts @@ -4,6 +4,7 @@ import path from "node:path"; import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime"; import { pathExists, writeExternalFileWithinRoot } from "openclaw/plugin-sdk/security-runtime"; import { ensureRepoBoundDirectory, resolveRepoRelativeOutputDir } from "../cli-paths.js"; +import { toQaError } from "../errors.js"; import { isTruthyOptIn, trimToValue } from "../mantis-options.runtime.js"; import { type CommandRunner, @@ -229,14 +230,10 @@ async function runCommandWithExternalOutput(params: { }, }); if (deferredError) { - throw toMantisError(deferredError); + throw toQaError(deferredError); } } -function toMantisError(error: unknown): Error { - return error instanceof Error ? error : new Error(formatErrorMessage(error)); -} - function buildVisualDriverArgs(params: { browserUrl: string; crabboxBin: string; diff --git a/extensions/qa-lab/src/manual-lane.runtime.ts b/extensions/qa-lab/src/manual-lane.runtime.ts index dfe258df7752..00aa810e78fb 100644 --- a/extensions/qa-lab/src/manual-lane.runtime.ts +++ b/extensions/qa-lab/src/manual-lane.runtime.ts @@ -2,6 +2,7 @@ import { randomUUID } from "node:crypto"; import { setTimeout as sleep } from "node:timers/promises"; import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime"; +import { toQaError } from "./errors.js"; import { startQaGatewayChild } from "./gateway-child.js"; import { startQaLabServer } from "./lab-server.js"; import { resolveQaLiveTurnTimeoutMs } from "./live-timeout.js"; @@ -31,10 +32,6 @@ type ManualLaneResult = { watchUrl: string; }; -function normalizeManualLaneCleanupError(error: unknown): Error { - return error instanceof Error ? error : new Error(formatErrorMessage(error)); -} - async function stopManualLaneResource( resource: { stop: () => Promise | void } | null | undefined, ): Promise { @@ -45,7 +42,7 @@ async function stopManualLaneResource( await resource.stop(); return undefined; } catch (error) { - return normalizeManualLaneCleanupError(error); + return toQaError(error); } } @@ -58,7 +55,7 @@ async function stopManualLaneAuxiliaryResources(resources: { .map((resource) => Promise.resolve().then(() => resource.stop())); const results = await Promise.allSettled(stopTasks); const failed = results.find((result) => result.status === "rejected"); - return failed ? normalizeManualLaneCleanupError(failed.reason) : undefined; + return failed ? toQaError(failed.reason) : undefined; } function resolveManualLaneTimeoutMs(params: { @@ -188,13 +185,13 @@ export async function runQaManualLane(params: QaManualLaneParams) { } finally { let transportCleanupBeforeError: Error | undefined; await transportCleanupBeforeGatewayStop?.().catch((error: unknown) => { - transportCleanupBeforeError = normalizeManualLaneCleanupError(error); + transportCleanupBeforeError = toQaError(error); }); const gatewayCleanupError = await stopManualLaneResource(gateway); let transportCleanupAfterError: Error | undefined; if (!gatewayCleanupError) { await transportCleanupAfterGatewayStop?.().catch((error: unknown) => { - transportCleanupAfterError = normalizeManualLaneCleanupError(error); + transportCleanupAfterError = toQaError(error); }); } const auxiliaryCleanupError = await stopManualLaneAuxiliaryResources({ lab, mock }); diff --git a/extensions/qa-lab/src/multipass.runtime.ts b/extensions/qa-lab/src/multipass.runtime.ts index 2075ffb2d033..6b4480056125 100644 --- a/extensions/qa-lab/src/multipass.runtime.ts +++ b/extensions/qa-lab/src/multipass.runtime.ts @@ -4,6 +4,7 @@ import fs from "node:fs"; import { access, mkdir, writeFile } from "node:fs/promises"; import path from "node:path"; import type { OpenClawCrablineChannelDriverSelection } from "@openclaw/crabline"; +import { coerceErrorMessage, toStringifiedError } from "openclaw/plugin-sdk/error-runtime"; import { runExec } from "openclaw/plugin-sdk/process-runtime"; import { sleep } from "openclaw/plugin-sdk/runtime-env"; import { appendRegularFile } from "openclaw/plugin-sdk/security-runtime"; @@ -481,14 +482,14 @@ async function waitForGuestReady(logPath: string, vmName: string) { lastError = error; await appendMultipassLog( logPath, - `guest-ready retry ${attempt}/12: ${error instanceof Error ? error.message : String(error)}\n\n`, + `guest-ready retry ${attempt}/12: ${coerceErrorMessage(error)}\n\n`, ); if (attempt < 12) { await sleep(2_000); } } } - throw lastError instanceof Error ? lastError : new Error(String(lastError)); + throw toStringifiedError(lastError); } async function mountRepo(logPath: string, repoRoot: string, vmName: string) { @@ -505,14 +506,14 @@ async function mountRepo(logPath: string, repoRoot: string, vmName: string) { lastError = error; await appendMultipassLog( logPath, - `mount retry ${attempt}/5: ${error instanceof Error ? error.message : String(error)}\n\n`, + `mount retry ${attempt}/5: ${coerceErrorMessage(error)}\n\n`, ); if (attempt < 5) { await sleep(2_000); } } } - throw lastError instanceof Error ? lastError : new Error(String(lastError)); + throw toStringifiedError(lastError); } async function mountCodexHome(logPath: string, hostCodexHomePath: string, vmName: string) { @@ -529,14 +530,14 @@ async function mountCodexHome(logPath: string, hostCodexHomePath: string, vmName lastError = error; await appendMultipassLog( logPath, - `codex-home mount retry ${attempt}/5: ${error instanceof Error ? error.message : String(error)}\n\n`, + `codex-home mount retry ${attempt}/5: ${coerceErrorMessage(error)}\n\n`, ); if (attempt < 5) { await sleep(2_000); } } } - throw lastError instanceof Error ? lastError : new Error(String(lastError)); + throw toStringifiedError(lastError); } async function transferLiveProviderConfig(plan: QaMultipassPlan) { @@ -560,7 +561,7 @@ async function tryCopyGuestBootstrapLog(plan: QaMultipassPlan) { } catch (error) { await appendMultipassLog( plan.hostLogPath, - `bootstrap log transfer skipped: ${error instanceof Error ? error.message : String(error)}\n\n`, + `bootstrap log transfer skipped: ${coerceErrorMessage(error)}\n\n`, ); } } @@ -605,10 +606,9 @@ export async function runQaMultipass(params: { await execFileAsync("multipass", ["version"]); } catch (error) { if ((error as ExecFileError).code !== "ENOENT") { - throw new Error( - `Unable to verify Multipass availability: ${error instanceof Error ? error.message : String(error)}.`, - { cause: error }, - ); + throw new Error(`Unable to verify Multipass availability: ${coerceErrorMessage(error)}.`, { + cause: error, + }); } throw new Error( `Multipass is not installed on this host. Install it with '${resolveMultipassInstallHint()}', then rerun 'pnpm openclaw qa suite --runner multipass'.`, @@ -668,7 +668,7 @@ export async function runQaMultipass(params: { await tryCopyGuestBootstrapLog(plan); } throw new Error( - `QA Multipass run failed: ${error instanceof Error ? error.message : String(error)}. See ${plan.hostLogPath}.`, + `QA Multipass run failed: ${coerceErrorMessage(error)}. See ${plan.hostLogPath}.`, { cause: error }, ); } finally { @@ -679,7 +679,7 @@ export async function runQaMultipass(params: { } catch (error) { await appendMultipassLog( plan.hostLogPath, - `cleanup error: ${error instanceof Error ? error.message : String(error)}\n\n`, + `cleanup error: ${coerceErrorMessage(error)}\n\n`, ); } } diff --git a/extensions/qa-lab/src/progress-format.ts b/extensions/qa-lab/src/progress-format.ts index f3c3d6fb576f..948a2c9b141e 100644 --- a/extensions/qa-lab/src/progress-format.ts +++ b/extensions/qa-lab/src/progress-format.ts @@ -1,9 +1,3 @@ -import { parseBooleanValue } from "openclaw/plugin-sdk/string-coerce-runtime"; - -export function parseQaProgressBooleanEnv(value: string | undefined): boolean | undefined { - return parseBooleanValue(value); -} - export function sanitizeQaProgressValue(value: string): string { let normalized = ""; for (const char of value) { diff --git a/extensions/qa-lab/src/suite-support.ts b/extensions/qa-lab/src/suite-support.ts index 122c8921c2d6..a7d447f63e26 100644 --- a/extensions/qa-lab/src/suite-support.ts +++ b/extensions/qa-lab/src/suite-support.ts @@ -1,7 +1,7 @@ import type { OpenClawCrablineChannelDriverSelection } from "@openclaw/crabline"; +import { parseBooleanValue } from "openclaw/plugin-sdk/string-coerce-runtime"; import type { QaSuiteChannelDriverSelection } from "./crabline-artifacts.js"; import type { QaProviderMode } from "./model-selection.js"; -import { parseQaProgressBooleanEnv as parseQaSuiteBooleanEnv } from "./progress-format.js"; import type { QaTransportId } from "./qa-transport-registry.js"; import type { QaTransportAdapter } from "./qa-transport.js"; import type { RuntimeId } from "./runtime-parity.js"; @@ -127,7 +127,7 @@ export function appendNodeOption(raw: string | undefined, option: string) { } export function shouldCaptureGatewayHeapCheckpoints(env: NodeJS.ProcessEnv = process.env) { - return parseQaSuiteBooleanEnv(env.OPENCLAW_QA_GATEWAY_HEAP_CHECKPOINTS) === true; + return parseBooleanValue(env.OPENCLAW_QA_GATEWAY_HEAP_CHECKPOINTS) === true; } export function buildQaGatewayHeapCheckpointRuntimeEnvPatch( diff --git a/extensions/qa-lab/src/suite.test.ts b/extensions/qa-lab/src/suite.test.ts index ed51a6c8f4a6..bc25954eb8fb 100644 --- a/extensions/qa-lab/src/suite.test.ts +++ b/extensions/qa-lab/src/suite.test.ts @@ -301,14 +301,6 @@ describe("qa suite", () => { ); }); - it("parses progress env booleans", () => { - expect(qaSuiteProgressTesting.parseQaSuiteBooleanEnv("true")).toBe(true); - expect(qaSuiteProgressTesting.parseQaSuiteBooleanEnv("on")).toBe(true); - expect(qaSuiteProgressTesting.parseQaSuiteBooleanEnv("false")).toBe(false); - expect(qaSuiteProgressTesting.parseQaSuiteBooleanEnv("off")).toBe(false); - expect(qaSuiteProgressTesting.parseQaSuiteBooleanEnv("maybe")).toBeUndefined(); - }); - it("stops an owned lab when readiness never becomes healthy", async () => { const stop = vi.fn(async () => {}); fetchWithSsrFGuardMock.mockResolvedValue({ diff --git a/extensions/qa-lab/src/suite.ts b/extensions/qa-lab/src/suite.ts index 886d9ed8efc7..61cccf29f185 100644 --- a/extensions/qa-lab/src/suite.ts +++ b/extensions/qa-lab/src/suite.ts @@ -7,6 +7,7 @@ import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts"; import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime"; import { parseStrictPositiveInteger } from "openclaw/plugin-sdk/number-runtime"; import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/ssrf-runtime"; +import { parseBooleanValue } from "openclaw/plugin-sdk/string-coerce-runtime"; import type { QaEvidenceTiming, QaEvidenceSummaryJson } from "./evidence-summary.js"; import type { QaCliBackendAuthMode, QaGatewayChildCommand } from "./gateway-child.js"; import { startQaGatewayChild } from "./gateway-child.js"; @@ -14,10 +15,7 @@ import { discardIgnoredResponseBody } from "./ignored-response-body.js"; import type { QaLabServerHandle, QaLabServerStartParams } from "./lab-server.types.js"; import { resolveQaLiveTurnTimeoutMs } from "./live-timeout.js"; import type { QaProviderMode } from "./model-selection.js"; -import { - parseQaProgressBooleanEnv as parseQaSuiteBooleanEnv, - sanitizeQaProgressValue as sanitizeQaSuiteProgressValue, -} from "./progress-format.js"; +import { sanitizeQaProgressValue as sanitizeQaSuiteProgressValue } from "./progress-format.js"; import type { QaThinkingLevel } from "./qa-gateway-config.js"; import { createQaTransportAdapter, @@ -169,11 +167,11 @@ export type QaSuiteRunParams = { }; export function shouldLogQaSuiteProgress(env: NodeJS.ProcessEnv = process.env) { - const override = parseQaSuiteBooleanEnv(env.OPENCLAW_QA_SUITE_PROGRESS); + const override = parseBooleanValue(env.OPENCLAW_QA_SUITE_PROGRESS); if (override !== undefined) { return override; } - return parseQaSuiteBooleanEnv(env.CI) === true; + return parseBooleanValue(env.CI) === true; } export function resolveQaSuiteTransportReadyTimeoutMs( @@ -606,7 +604,6 @@ export const qaSuiteProgressTesting = { createScenarioStepRunner: createQaSuiteScenarioStepRunner, formatQaSuiteRunStartProgress, mergeQaRuntimeEnvPatches, - parseQaSuiteBooleanEnv, remapModelRefForForcedRuntime, runQaFlowSuiteCleanupPlan, runQaSuiteCleanupSteps, diff --git a/extensions/qwen/index.ts b/extensions/qwen/index.ts index e7fc65347342..3f38d478b664 100644 --- a/extensions/qwen/index.ts +++ b/extensions/qwen/index.ts @@ -2,6 +2,7 @@ import { createProviderApiKeyAuthMethod } from "openclaw/plugin-sdk/provider-auth-api-key"; import { buildOpenAICompatibleLiveModelProviderConfig } from "openclaw/plugin-sdk/provider-catalog-live-runtime"; import { defineSingleProviderPluginEntry } from "openclaw/plugin-sdk/provider-entry"; +import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/string-coerce-runtime"; import { applyQwenNativeStreamingUsageCompat } from "./api.js"; import { buildQwenMediaUnderstandingProvider } from "./media-understanding-provider.js"; import { @@ -43,10 +44,6 @@ const QWEN_TOKEN_PLAN_GLM_NO_MAX_THINKING_LEVEL_IDS = QWEN_TOKEN_PLAN_THINKING_L (id) => id !== "max", ); -function normalizeProviderId(value: string): string { - return value.trim().toLowerCase(); -} - function resolveConfiguredQwenBaseUrl( config: { models?: { providers?: Record } } | undefined, ): string | undefined { @@ -55,7 +52,7 @@ function resolveConfiguredQwenBaseUrl( return undefined; } for (const [providerId, provider] of Object.entries(providers)) { - const normalized = normalizeProviderId(providerId); + const normalized = normalizeLowercaseStringOrEmpty(providerId); if (normalized !== PROVIDER_ID && normalized !== LEGACY_PROVIDER_ID) { continue; } @@ -75,7 +72,7 @@ function resolveConfiguredQwenTokenPlanBaseUrl( return undefined; } for (const [providerId, provider] of Object.entries(providers)) { - const normalized = normalizeProviderId(providerId); + const normalized = normalizeLowercaseStringOrEmpty(providerId); if (normalized !== QWEN_TOKEN_PLAN_PROVIDER_ID) { continue; } diff --git a/extensions/signal/src/client-container.ts b/extensions/signal/src/client-container.ts index ad45fe7c8d39..8652bb1872bd 100644 --- a/extensions/signal/src/client-container.ts +++ b/extensions/signal/src/client-container.ts @@ -6,7 +6,7 @@ * to keep the two modes cleanly isolated. */ -import { toErrorObject } from "openclaw/plugin-sdk/error-runtime"; +import { coerceErrorMessage, toErrorObject } from "openclaw/plugin-sdk/error-runtime"; import { resolveFetch } from "openclaw/plugin-sdk/fetch-runtime"; import { detectMime, @@ -243,7 +243,7 @@ export async function containerCheck( return { ok: false, status: null, - error: err instanceof Error ? err.message : String(err), + error: coerceErrorMessage(err), }; } finally { await releaseUnreadResponseBody(res); @@ -279,7 +279,7 @@ function containerReceiveCheck( settle({ ok: false, status: null, - error: err instanceof Error ? err.message : String(err), + error: coerceErrorMessage(err), }); return; } @@ -301,7 +301,7 @@ function containerReceiveCheck( settle({ ok: false, status: null, - error: err instanceof Error ? err.message : String(err), + error: coerceErrorMessage(err), }); }); ws.once("close", (code, reason) => { @@ -478,9 +478,7 @@ export async function streamContainerEvents(params: { try { ws = new WebSocket(wsUrl, { maxPayload: WS_MAX_PAYLOAD, handshakeTimeout: WS_HANDSHAKE_MS }); } catch (err) { - logError( - `[signal-ws] failed to create WebSocket: ${err instanceof Error ? err.message : String(err)}`, - ); + logError(`[signal-ws] failed to create WebSocket: ${coerceErrorMessage(err)}`); reject(toErrorObject(err, "Non-Error rejection")); return; } @@ -504,20 +502,18 @@ export async function streamContainerEvents(params: { await params.onEvent(envelope); }); void eventChain.catch((err: unknown) => { - logError( - `[signal-ws] receive handler failed: ${err instanceof Error ? err.message : String(err)}`, - ); + logError(`[signal-ws] receive handler failed: ${coerceErrorMessage(err)}`); rejectOnce(err); ws.close(); }); } } catch (err) { - logError(`[signal-ws] parse error: ${err instanceof Error ? err.message : String(err)}`); + logError(`[signal-ws] parse error: ${coerceErrorMessage(err)}`); } }); ws.on("error", (err) => { - logError(`[signal-ws] error: ${err instanceof Error ? err.message : String(err)}`); + logError(`[signal-ws] error: ${coerceErrorMessage(err)}`); // Don't resolve here - the close event will fire next }); diff --git a/extensions/slack/src/actions.ts b/extensions/slack/src/actions.ts index 04c7fbb5dfdb..1729889d93b6 100644 --- a/extensions/slack/src/actions.ts +++ b/extensions/slack/src/actions.ts @@ -4,6 +4,7 @@ import { normalizeAccountId } from "openclaw/plugin-sdk/account-resolution"; import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts"; import { requireRuntimeConfig } from "openclaw/plugin-sdk/plugin-config-runtime"; import { logVerbose } from "openclaw/plugin-sdk/runtime-env"; +import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime"; import { z } from "zod"; import { resolveSlackAccount } from "./accounts.js"; import type { SlackAuthoredTextPlacement } from "./authored-text.js"; @@ -609,11 +610,6 @@ type SlackFileThreadShare = { threadTs?: string; }; -function normalizeSlackScopeValue(value: string | undefined): string | undefined { - const trimmed = value?.trim(); - return trimmed ? trimmed : undefined; -} - function collectSlackDirectShareChannelIds(file: SlackFileInfoSummary): Set { const ids = new Set(); for (const group of [file.channels, file.groups, file.ims]) { @@ -624,7 +620,7 @@ function collectSlackDirectShareChannelIds(file: SlackFileInfoSummary): Set { const ids = new Set(); for (const shareMap of collectSlackShareMaps(file)) { for (const channelId of Object.keys(shareMap)) { - const normalized = normalizeSlackScopeValue(channelId); + const normalized = normalizeOptionalString(channelId); if (normalized) { ids.add(normalized); } @@ -672,9 +668,9 @@ function collectSlackThreadShares( continue; } const entry = rawEntry as Record; - const ts = typeof entry.ts === "string" ? normalizeSlackScopeValue(entry.ts) : undefined; + const ts = typeof entry.ts === "string" ? normalizeOptionalString(entry.ts) : undefined; const threadTs = - typeof entry.thread_ts === "string" ? normalizeSlackScopeValue(entry.thread_ts) : undefined; + typeof entry.thread_ts === "string" ? normalizeOptionalString(entry.thread_ts) : undefined; matches.push({ channelId, ts, threadTs }); } } @@ -686,11 +682,11 @@ function hasSlackScopeMismatch(params: { channelId?: string; threadId?: string; }): boolean { - const channelId = normalizeSlackScopeValue(params.channelId); + const channelId = normalizeOptionalString(params.channelId); if (!channelId) { return false; } - const threadId = normalizeSlackScopeValue(params.threadId); + const threadId = normalizeOptionalString(params.threadId); const directIds = collectSlackDirectShareChannelIds(params.file); const sharedIds = collectSlackSharedChannelIds(params.file); diff --git a/extensions/telegram/src/account-selection.ts b/extensions/telegram/src/account-selection.ts index 172edd8977be..a13c71f9f51b 100644 --- a/extensions/telegram/src/account-selection.ts +++ b/extensions/telegram/src/account-selection.ts @@ -10,22 +10,8 @@ import { normalizeOptionalAccountId, } from "openclaw/plugin-sdk/account-id"; import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts"; - -const DEFAULT_AGENT_ID = "main"; - -function normalizeAgentId(value: string | undefined | null): string { - const normalized = (value ?? "") - .trim() - .toLowerCase() - .replace(/[^a-z0-9_-]+/g, "-") - .replace(/^-+/g, "") - .replace(/-+$/g, ""); - return normalized || DEFAULT_AGENT_ID; -} - -function normalizeChannelId(value: unknown): string { - return typeof value === "string" ? value.trim().toLowerCase() : ""; -} +import { normalizeAgentId } from "openclaw/plugin-sdk/routing"; +import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/string-coerce-runtime"; function resolveDefaultAgentId(cfg: OpenClawConfig): string { const agents = Array.isArray(cfg.agents?.list) ? cfg.agents.list : []; @@ -44,7 +30,7 @@ function resolveBindingAccount(params: { agentId?: unknown; match?: { channel?: unknown; accountId?: unknown }; }; - if (normalizeChannelId(binding.match?.channel) !== params.channelId) { + if (normalizeLowercaseStringOrEmpty(binding.match?.channel) !== params.channelId) { return null; } const accountId = typeof binding.match?.accountId === "string" ? binding.match.accountId : ""; diff --git a/extensions/telegram/src/agent-config.ts b/extensions/telegram/src/agent-config.ts index af08fe03bee9..94597d18e8c8 100644 --- a/extensions/telegram/src/agent-config.ts +++ b/extensions/telegram/src/agent-config.ts @@ -1,15 +1,9 @@ // Telegram helper module supports agent config behavior. import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts"; +import { normalizeAgentId } from "openclaw/plugin-sdk/routing"; type ReasoningDefault = "on" | "stream" | "off"; -const DEFAULT_AGENT_ID = "main"; - -function normalizeAgentId(value: string | undefined | null): string { - const normalized = (value ?? "").trim().toLowerCase(); - return normalized || DEFAULT_AGENT_ID; -} - export function resolveTelegramConfigReasoningDefault( cfg: OpenClawConfig, agentId: string, diff --git a/extensions/telegram/src/sticker-cache-store.ts b/extensions/telegram/src/sticker-cache-store.ts index 449687555898..ca0286d142be 100644 --- a/extensions/telegram/src/sticker-cache-store.ts +++ b/extensions/telegram/src/sticker-cache-store.ts @@ -1,6 +1,7 @@ // Telegram plugin module implements sticker cache store behavior. import type { PluginStateSyncKeyedStore } from "openclaw/plugin-sdk/plugin-state-runtime"; import { logVerbose } from "openclaw/plugin-sdk/runtime-env"; +import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/string-coerce-runtime"; import { getTelegramRuntime } from "./runtime.js"; import { normalizeCachedStickerForStore, @@ -20,10 +21,6 @@ function openStickerCacheStore(): TelegramStickerCacheStore { }); } -function normalizeStickerSearchText(value: unknown): string { - return typeof value === "string" ? value.trim().toLowerCase() : ""; -} - function readStickerCacheStore( operation: string, read: (store: TelegramStickerCacheStore) => T, @@ -61,7 +58,7 @@ export function cacheSticker(sticker: CachedSticker): void { * Search cached stickers by text query (fuzzy match on description + emoji + setName). */ export function searchStickers(query: string, limit = 10): CachedSticker[] { - const queryLower = normalizeStickerSearchText(query); + const queryLower = normalizeLowercaseStringOrEmpty(query); const results: Array<{ sticker: CachedSticker; score: number }> = []; for (const { value: sticker } of readStickerCacheStore( @@ -70,7 +67,7 @@ export function searchStickers(query: string, limit = 10): CachedSticker[] { [], )) { let score = 0; - const descLower = normalizeStickerSearchText(sticker.description); + const descLower = normalizeLowercaseStringOrEmpty(sticker.description); // Exact substring match in description if (descLower.includes(queryLower)) { @@ -92,7 +89,7 @@ export function searchStickers(query: string, limit = 10): CachedSticker[] { } // Set name match - if (normalizeStickerSearchText(sticker.setName).includes(queryLower)) { + if (normalizeLowercaseStringOrEmpty(sticker.setName).includes(queryLower)) { score += 3; } diff --git a/extensions/whatsapp/src/auto-reply/monitor/message-line.runtime.ts b/extensions/whatsapp/src/auto-reply/monitor/message-line.runtime.ts index 58534465b9c9..3ce2d7cbc580 100644 --- a/extensions/whatsapp/src/auto-reply/monitor/message-line.runtime.ts +++ b/extensions/whatsapp/src/auto-reply/monitor/message-line.runtime.ts @@ -1,5 +1,6 @@ // Whatsapp plugin module implements message line behavior. import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts"; +import { normalizeAgentId } from "openclaw/plugin-sdk/routing"; export { formatInboundEnvelope, @@ -8,10 +9,6 @@ export { type WhatsAppMessagePrefixConfig = OpenClawConfig; -function normalizeAgentId(agentId: string): string { - return agentId.trim().toLowerCase() || "main"; -} - function resolveIdentityNamePrefix( cfg: WhatsAppMessagePrefixConfig, agentId: string, diff --git a/extensions/xai/provider-catalog.ts b/extensions/xai/provider-catalog.ts index d69ac4b92871..1512bb473eeb 100644 --- a/extensions/xai/provider-catalog.ts +++ b/extensions/xai/provider-catalog.ts @@ -2,6 +2,9 @@ import { buildLiveModelProviderConfig, getCachedLiveProviderModelRows, + readLiveModelCatalogBooleanField, + readLiveModelCatalogPositiveSafeIntegerField, + readLiveModelCatalogStringField, type LiveModelCatalogFetchGuard, } from "openclaw/plugin-sdk/provider-catalog-live-runtime"; import type { @@ -106,10 +109,7 @@ function withXaiOAuthAutoModel( } function readXaiOAuthDefaultModelId(value: unknown): string | undefined { - if (!value || typeof value !== "object" || Array.isArray(value)) { - return undefined; - } - return readLiveModelString(value, "default_model"); + return readLiveModelCatalogStringField(value, "default_model"); } async function fetchXaiOAuthDefaultModelId(params: { @@ -171,36 +171,6 @@ export async function buildLiveXaiProvider(params: { }); } -function readLiveModelString(row: unknown, key: string): string | undefined { - if (!row || typeof row !== "object" || Array.isArray(row)) { - return undefined; - } - const value = (row as Record)[key]; - return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined; -} - -function readLiveModelPositiveInteger(row: unknown, keys: readonly string[]): number | undefined { - if (!row || typeof row !== "object" || Array.isArray(row)) { - return undefined; - } - const record = row as Record; - for (const key of keys) { - const value = record[key]; - if (typeof value === "number" && Number.isSafeInteger(value) && value > 0) { - return value; - } - } - return undefined; -} - -function readLiveModelBoolean(row: unknown, key: string): boolean | undefined { - if (!row || typeof row !== "object" || Array.isArray(row)) { - return undefined; - } - const value = (row as Record)[key]; - return typeof value === "boolean" ? value : undefined; -} - function resolveXaiOauthMetadataFallback(modelId: string) { if (modelId === "grok-build") { return resolveXaiCatalogEntry("grok-build-0.1"); @@ -209,14 +179,15 @@ function resolveXaiOauthMetadataFallback(modelId: string) { } function isXaiOAuthResponsesModel(row: unknown, fallback: ModelDefinitionConfig | undefined) { - const modelId = readLiveModelString(row, "id") ?? readLiveModelString(row, "model"); + const modelId = + readLiveModelCatalogStringField(row, "id") ?? readLiveModelCatalogStringField(row, "model"); if (modelId && (XAI_IMAGE_MODELS as readonly string[]).includes(modelId)) { return false; } const backend = - readLiveModelString(row, "api_backend") ?? - readLiveModelString(row, "apiBackend") ?? - readLiveModelString(row, "backend"); + readLiveModelCatalogStringField(row, "api_backend") ?? + readLiveModelCatalogStringField(row, "apiBackend") ?? + readLiveModelCatalogStringField(row, "backend"); if (backend) { const normalizedBackend = backend.toLowerCase(); return ( @@ -229,7 +200,8 @@ function isXaiOAuthResponsesModel(row: unknown, fallback: ModelDefinitionConfig } function buildXaiOauthModelFromLiveRow(row: unknown): ModelDefinitionConfig | undefined { - const modelId = readLiveModelString(row, "id") ?? readLiveModelString(row, "model"); + const modelId = + readLiveModelCatalogStringField(row, "id") ?? readLiveModelCatalogStringField(row, "model"); if (!modelId) { return undefined; } @@ -238,16 +210,19 @@ function buildXaiOauthModelFromLiveRow(row: unknown): ModelDefinitionConfig | un return undefined; } const contextWindow = - readLiveModelPositiveInteger(row, ["context_window", "contextWindow"]) ?? + readLiveModelCatalogPositiveSafeIntegerField(row, ["context_window", "contextWindow"]) ?? fallback?.contextWindow ?? XAI_DEFAULT_CONTEXT_WINDOW; const maxTokens = - readLiveModelPositiveInteger(row, ["max_completion_tokens", "maxCompletionTokens"]) ?? + readLiveModelCatalogPositiveSafeIntegerField(row, [ + "max_completion_tokens", + "maxCompletionTokens", + ]) ?? fallback?.maxTokens ?? XAI_DEFAULT_MAX_TOKENS; const supportsReasoningEffort = - readLiveModelBoolean(row, "supports_reasoning_effort") ?? - readLiveModelBoolean(row, "supportsReasoningEffort"); + readLiveModelCatalogBooleanField(row, "supports_reasoning_effort") ?? + readLiveModelCatalogBooleanField(row, "supportsReasoningEffort"); const reasoning = supportsReasoningEffort === true || fallback?.reasoning === true || @@ -255,7 +230,7 @@ function buildXaiOauthModelFromLiveRow(row: unknown): ModelDefinitionConfig | un return { id: modelId, - name: readLiveModelString(row, "name") ?? fallback?.name ?? modelId, + name: readLiveModelCatalogStringField(row, "name") ?? fallback?.name ?? modelId, api: "openai-responses", baseUrl: XAI_GROK_OAUTH_BASE_URL, reasoning, diff --git a/package.json b/package.json index c2f5a67b3ec7..2c1a4c72f35f 100644 --- a/package.json +++ b/package.json @@ -1666,7 +1666,6 @@ "lint:extensions": "node scripts/run-oxlint.mjs --tsconfig config/tsconfig/oxlint.extensions.json extensions", "lint:extensions:no-guarded-wildcard-reexports": "node --import tsx scripts/check-extension-wildcard-reexports.mts", "lint:extensions:no-normalization-core-bypass": "node --import tsx scripts/check-extension-plugin-sdk-boundary.mts --mode=normalization-core-bypass", - "lint:extensions:no-plugin-sdk-internal": "node --import tsx scripts/check-extension-plugin-sdk-boundary.mts --mode=plugin-sdk-internal", "lint:extensions:no-plugin-sdk-wildcard-reexports": "node --import tsx scripts/check-plugin-sdk-wildcard-reexports.mts", "lint:extensions:no-relative-outside-package": "node --import tsx scripts/check-extension-plugin-sdk-boundary.mts --mode=relative-outside-package", "lint:extensions:no-src-outside-plugin-sdk": "node --import tsx scripts/check-extension-plugin-sdk-boundary.mts --mode=src-outside-plugin-sdk", @@ -1718,7 +1717,7 @@ "plugin-sdk:usage": "node --max-old-space-size=8192 --import tsx scripts/analyze-plugin-sdk-usage.ts", "policy:config-coverage": "node --import tsx scripts/check-policy-config-coverage.ts", "plugins:boundary-report": "node --import tsx scripts/plugin-boundary-report.ts", - "plugins:boundary-report:ci": "node --import tsx scripts/plugin-boundary-report.ts --summary --fail-on-cross-owner --fail-on-unclassified-unused-reserved --fail-on-eligible-compat", + "plugins:boundary-report:ci": "node --import tsx scripts/plugin-boundary-report.ts --summary --fail-on-eligible-compat", "plugins:boundary-report:json": "node --import tsx scripts/plugin-boundary-report.ts --json", "plugins:boundary-report:summary": "node --import tsx scripts/plugin-boundary-report.ts --summary", "plugins:assets:build": "node --import tsx scripts/bundled-plugin-assets.mts --phase build", diff --git a/packages/memory-host-sdk/src/host/openclaw-runtime.ts b/packages/memory-host-sdk/src/host/openclaw-runtime.ts index 74964ef58460..50ab766d2f1d 100644 --- a/packages/memory-host-sdk/src/host/openclaw-runtime.ts +++ b/packages/memory-host-sdk/src/host/openclaw-runtime.ts @@ -154,7 +154,10 @@ export { parseAgentSessionKey } from "../../../../src/routing/session-key.js"; export { hasInterSessionUserProvenance } from "../../../../src/sessions/input-provenance.js"; export { isCronRunSessionKey } from "../../../../src/sessions/session-key-utils.js"; export { onSessionTranscriptUpdate } from "../../../../src/sessions/transcript-events.js"; -export { CHARS_PER_TOKEN_ESTIMATE, estimateStringChars } from "../../../../src/utils/cjk-chars.js"; +export { + CHARS_PER_TOKEN_ESTIMATE, + estimateStringChars, +} from "@openclaw/normalization-core/cjk-chars"; export { runTasksWithConcurrency } from "../../../../src/utils/run-with-concurrency.js"; export { splitShellArgs } from "../../../../src/utils/shell-argv.js"; export { diff --git a/packages/model-catalog-core/src/model-catalog-refs.ts b/packages/model-catalog-core/src/model-catalog-refs.ts index 4f8b22bb2be2..2e109276d614 100644 --- a/packages/model-catalog-core/src/model-catalog-refs.ts +++ b/packages/model-catalog-core/src/model-catalog-refs.ts @@ -1,5 +1,5 @@ // Model Catalog Core module implements model catalog refs behavior. -import { normalizeLowercaseStringOrEmpty } from "./provider-id.js"; +import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce"; // Stable model catalog ref and merge-key builders. diff --git a/packages/model-catalog-core/src/provider-id.ts b/packages/model-catalog-core/src/provider-id.ts index 52f109f74052..0b970ce4491e 100644 --- a/packages/model-catalog-core/src/provider-id.ts +++ b/packages/model-catalog-core/src/provider-id.ts @@ -1,8 +1,6 @@ // Model Catalog Core module implements provider id behavior. import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce"; -export { normalizeLowercaseStringOrEmpty }; - export function normalizeProviderId(provider: string): string { return normalizeLowercaseStringOrEmpty(provider); } diff --git a/packages/model-catalog-core/src/provider-model-id-normalization.ts b/packages/model-catalog-core/src/provider-model-id-normalization.ts index d11de6a7dd8d..5271e9eaec0c 100644 --- a/packages/model-catalog-core/src/provider-model-id-normalization.ts +++ b/packages/model-catalog-core/src/provider-model-id-normalization.ts @@ -1,6 +1,6 @@ // Model Catalog Core module implements provider model id normalization behavior. +import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce"; import { parseModelCatalogRef } from "./model-catalog-refs.js"; -import { normalizeLowercaseStringOrEmpty } from "./provider-id.js"; import { normalizeGooglePreviewModelId, normalizeTogetherModelId, diff --git a/packages/normalization-core/package.json b/packages/normalization-core/package.json index 1760e0e4a507..afde013e8de7 100644 --- a/packages/normalization-core/package.json +++ b/packages/normalization-core/package.json @@ -39,16 +39,21 @@ "import": "./dist/expect.mjs", "default": "./dist/expect.mjs" }, - "./number-coercion": { - "types": "./dist/number-coercion.d.mts", - "import": "./dist/number-coercion.mjs", - "default": "./dist/number-coercion.mjs" + "./json-coercion": { + "types": "./dist/json-coercion.d.mts", + "import": "./dist/json-coercion.mjs", + "default": "./dist/json-coercion.mjs" }, "./json-schema": { "types": "./dist/json-schema.d.mts", "import": "./dist/json-schema.mjs", "default": "./dist/json-schema.mjs" }, + "./number-coercion": { + "types": "./dist/number-coercion.d.mts", + "import": "./dist/number-coercion.mjs", + "default": "./dist/number-coercion.mjs" + }, "./phone-presentation": { "types": "./dist/phone-presentation.d.mts", "import": "./dist/phone-presentation.mjs", @@ -96,7 +101,7 @@ } }, "scripts": { - "build": "tsdown src/index.ts src/agent-id.ts src/boolean-coercion.ts src/cjk-chars.ts src/error-coercion.ts src/expect.ts src/json-schema.ts src/number-coercion.ts src/phone-presentation.ts src/promise-like.ts src/record-coerce.ts src/result.ts src/stable-node-path.ts src/stable-stringify.ts src/string-coerce.ts src/string-normalization.ts src/utf16-slice.ts --no-config --platform node --format esm --dts --out-dir dist --clean" + "build": "tsdown src/index.ts src/agent-id.ts src/boolean-coercion.ts src/cjk-chars.ts src/error-coercion.ts src/expect.ts src/json-coercion.ts src/json-schema.ts src/number-coercion.ts src/phone-presentation.ts src/promise-like.ts src/record-coerce.ts src/result.ts src/stable-node-path.ts src/stable-stringify.ts src/string-coerce.ts src/string-normalization.ts src/utf16-slice.ts --no-config --platform node --format esm --dts --out-dir dist --clean" }, "dependencies": { "libphonenumber-js": "1.13.9", diff --git a/packages/normalization-core/src/json-coercion.test.ts b/packages/normalization-core/src/json-coercion.test.ts index 9ab3b013eef3..76f875e96a89 100644 --- a/packages/normalization-core/src/json-coercion.test.ts +++ b/packages/normalization-core/src/json-coercion.test.ts @@ -1,7 +1,16 @@ +import { + safeParseJson as safeParseJsonFromRoot, + safeParseJsonRecord as safeParseJsonRecordFromRoot, +} from "@openclaw/normalization-core"; +import { safeParseJson, safeParseJsonRecord } from "@openclaw/normalization-core/json-coercion"; import { describe, expect, it } from "vitest"; -import { safeParseJson, safeParseJsonRecord } from "./json-coercion.js"; describe("json-coercion", () => { + it("preserves the root exports alongside the focused package subpath", () => { + expect(safeParseJsonFromRoot).toBe(safeParseJson); + expect(safeParseJsonRecordFromRoot).toBe(safeParseJsonRecord); + }); + it.each<[string, unknown]>([ ['{"ok":true}', { ok: true }], ["[1]", [1]], diff --git a/packages/normalization-core/src/package-exports.test.ts b/packages/normalization-core/src/package-exports.test.ts new file mode 100644 index 000000000000..02059b71acea --- /dev/null +++ b/packages/normalization-core/src/package-exports.test.ts @@ -0,0 +1,32 @@ +import fs from "node:fs"; +import { fileURLToPath } from "node:url"; +import { describe, expect, it } from "vitest"; + +type PackageManifest = { + exports: Record< + string, + { + default: string; + import: string; + types: string; + } + >; + scripts: { build: string }; +}; + +const packageJsonPath = fileURLToPath(new URL("../package.json", import.meta.url)); +const manifest = JSON.parse(fs.readFileSync(packageJsonPath, "utf8")) as PackageManifest; + +describe("normalization-core package exports", () => { + it("builds every focused export from its matching source entry", () => { + for (const [subpath, target] of Object.entries(manifest.exports)) { + const entryName = subpath === "." ? "index" : subpath.slice(2); + expect(target).toEqual({ + types: `./dist/${entryName}.d.mts`, + import: `./dist/${entryName}.mjs`, + default: `./dist/${entryName}.mjs`, + }); + expect(manifest.scripts.build.split(/\s+/u)).toContain(`src/${entryName}.ts`); + } + }); +}); diff --git a/packages/normalization-core/src/stable-stringify.test.ts b/packages/normalization-core/src/stable-stringify.test.ts index efb9e394e68b..50bdc5452943 100644 --- a/packages/normalization-core/src/stable-stringify.test.ts +++ b/packages/normalization-core/src/stable-stringify.test.ts @@ -9,6 +9,17 @@ const sanitizeSurrogates = (text: string) => text.replace(/[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(? { + it.each([ + ['{"z":1,"a":2}', '{"a":2,"z":1}'], + [ + '{"items":[3,null,{"z":false,"a":1.5}],"enabled":true}', + '{"enabled":true,"items":[3,null,{"a":1.5,"z":false}]}', + ], + ['["text",0,-2.5,null,false]', '["text",0,-2.5,null,false]'], + ])("preserves deterministic bytes for parsed JSON %#", (json, expected) => { + expect(stableStringify(JSON.parse(json))).toBe(expected); + }); + it("sorts object keys recursively", () => { expect(stableStringify({ b: { d: 4, c: 3 }, a: 1 })).toBe('{"a":1,"b":{"c":3,"d":4}}'); }); diff --git a/packages/terminal-core/src/health-style.ts b/packages/terminal-core/src/health-style.ts index ee4b33bb293a..d2b1d7bf2cb1 100644 --- a/packages/terminal-core/src/health-style.ts +++ b/packages/terminal-core/src/health-style.ts @@ -1,5 +1,5 @@ // Terminal Core module implements health style behavior. -import { normalizeLowercaseStringOrEmpty } from "./string.js"; +import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce"; import { theme } from "./theme.js"; // Styles the status word in health output lines. diff --git a/packages/terminal-core/src/note.ts b/packages/terminal-core/src/note.ts index 555796bf780b..3f7778b5a652 100644 --- a/packages/terminal-core/src/note.ts +++ b/packages/terminal-core/src/note.ts @@ -1,9 +1,9 @@ // Terminal Core module implements note behavior. import { AsyncLocalStorage } from "node:async_hooks"; import { note as clackNote } from "@clack/prompts"; +import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce"; import { splitGraphemes, visibleWidth } from "./ansi.js"; import { stylePromptTitle } from "./prompt-style.js"; -import { normalizeLowercaseStringOrEmpty } from "./string.js"; const MIN_NOTE_COLUMNS = 80; const URL_PREFIX_RE = /^(https?:\/\/|file:\/\/)/i; diff --git a/packages/terminal-core/src/string.ts b/packages/terminal-core/src/string.ts deleted file mode 100644 index 524829070a6c..000000000000 --- a/packages/terminal-core/src/string.ts +++ /dev/null @@ -1,3 +0,0 @@ -// Shared terminal string normalization helpers. - -export { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce"; diff --git a/qa/scenarios/plugins/plugin-public-entrypoint-contracts.yaml b/qa/scenarios/plugins/plugin-public-entrypoint-contracts.yaml index cc2435916d26..eb5ac804f618 100644 --- a/qa/scenarios/plugins/plugin-public-entrypoint-contracts.yaml +++ b/qa/scenarios/plugins/plugin-public-entrypoint-contracts.yaml @@ -26,7 +26,7 @@ scenario: - scripts/lib/plugin-sdk-entrypoints.json - scripts/lib/plugin-sdk-private-local-only-subpaths.json - scripts/lib/plugin-sdk-deprecated-public-subpaths.json - - src/plugin-sdk/entrypoints.ts + - scripts/lib/plugin-sdk-entries.mts - src/plugins/contracts/plugin-sdk-subpaths.test.ts execution: kind: vitest diff --git a/scripts/build-all.mts b/scripts/build-all.mts index c3e8ce61bdc5..1bd48689df48 100644 --- a/scripts/build-all.mts +++ b/scripts/build-all.mts @@ -9,6 +9,7 @@ import { performance } from "node:perf_hooks"; import { pathToFileURL } from "node:url"; import { asRecord } from "@openclaw/normalization-core/record-coerce"; import prettyMilliseconds from "pretty-ms"; +import { resolveBuildIdentityEnvironment } from "./lib/build-identity.mts"; import { listPluginSdkDeclarationOutputs, pluginSdkEntrypoints, @@ -59,7 +60,6 @@ type BuildAllStepParams = { comSpec?: string; }; type BuildAllCacheParams = { rootDir?: string; fs?: BuildAllFs; env?: NodeJS.ProcessEnv }; -const FULL_GIT_COMMIT_RE = /^[0-9a-f]{40}$/iu; const BUILD_CACHE_VERSION = 4; const TSDOWN_DECLARATION_EXTENSIONS = [".d.ts", ".d.mts", ".d.cts"]; const TSDOWN_SOURCE_EXTENSIONS = [ @@ -491,19 +491,12 @@ export function resolveBuildAllEnvironment( now: () => Date = () => new Date(), readGitCommit: () => string | null = readCurrentGitCommit, ) { - const explicitTimestamp = env.OPENCLAW_BUILD_TIMESTAMP?.trim(); - const explicitCommit = env.GIT_COMMIT?.trim() || env.GIT_SHA?.trim(); - const checkedOutCommit = explicitCommit ? null : readGitCommit()?.trim(); - // GITHUB_SHA names the workflow invocation and can differ from a checked-out tag. - const commit = explicitCommit || checkedOutCommit || env.GITHUB_SHA?.trim(); - if (commit && !FULL_GIT_COMMIT_RE.test(commit)) { - throw new Error("build commit must be a full 40-character hexadecimal SHA"); - } - return { - ...env, - OPENCLAW_BUILD_TIMESTAMP: explicitTimestamp || now().toISOString(), - ...(commit ? { GIT_COMMIT: commit.toLowerCase() } : {}), - }; + return resolveBuildIdentityEnvironment({ + commitLabel: "build commit", + env, + now, + readGitCommit, + }); } function resolveStepEnv(step: BuildAllStep, env: NodeJS.ProcessEnv, platform: NodeJS.Platform) { diff --git a/scripts/changed-lanes.mts b/scripts/changed-lanes.mts index 0c67762ba70a..f1d3a026a41b 100644 --- a/scripts/changed-lanes.mts +++ b/scripts/changed-lanes.mts @@ -1,5 +1,6 @@ import { execFileSync } from "node:child_process"; import { appendFileSync, existsSync, readFileSync } from "node:fs"; +import { stableStringify } from "../packages/normalization-core/src/stable-stringify.ts"; import { booleanFlag, parseFlagArgs, stringFlag } from "./lib/arg-utils.mts"; import { getChangedPathFacts, normalizeChangedPath } from "./lib/changed-path-facts.mjs"; import { isDirectRunUrl } from "./lib/direct-run.mjs"; @@ -448,8 +449,8 @@ export function isLiveDockerPackageScriptOnlyChange(before: string, after: strin const afterStripped = stripLiveDockerPackageScripts(afterPackage); return ( - stableJson(beforeStripped) === stableJson(afterStripped) && - stableJson(beforeAllowed) !== stableJson(afterAllowed) + stableStringify(beforeStripped) === stableStringify(afterStripped) && + stableStringify(beforeAllowed) !== stableStringify(afterAllowed) ); } @@ -469,8 +470,8 @@ export function isPackageScriptOnlyChange(before: string, after: string): boolea const afterStripped = stripPackageScripts(afterPackage); return ( - stableJson(beforeStripped) === stableJson(afterStripped) && - stableJson(beforeScripts) !== stableJson(afterScripts) + stableStringify(beforeStripped) === stableStringify(afterStripped) && + stableStringify(beforeScripts) !== stableStringify(afterScripts) ); } @@ -542,19 +543,6 @@ function stripPackageScripts(packageJson: Record) { return clone; } -function stableJson(value: unknown): string { - if (Array.isArray(value)) { - return `[${value.map(stableJson).join(",")}]`; - } - if (isRecord(value)) { - return `{${Object.keys(value) - .toSorted((left, right) => left.localeCompare(right)) - .map((key) => `${JSON.stringify(key)}:${stableJson(value[key])}`) - .join(",")}}`; - } - return JSON.stringify(value) ?? "undefined"; -} - /** * Writes changed-lane booleans to the GitHub Actions output file. */ diff --git a/scripts/check-changed.mts b/scripts/check-changed.mts index 5af36fb34ef0..a6891f585726 100644 --- a/scripts/check-changed.mts +++ b/scripts/check-changed.mts @@ -20,7 +20,12 @@ import { listStagedChangedPaths, } from "./changed-lanes.mts"; import type { ChangedLaneResult } from "./changed-lanes.mts"; -import { booleanFlag, parseFlagArgs, stringFlag } from "./lib/arg-utils.mts"; +import { + booleanFlag, + isOpenEndedTruthyValue, + parseFlagArgs, + stringFlag, +} from "./lib/arg-utils.mts"; import { getChangedPathFacts, normalizeChangedPath } from "./lib/changed-path-facts.mjs"; import { printTimingSummary } from "./lib/check-timing-summary.mts"; import { isDirectRunUrl } from "./lib/direct-run.mjs"; @@ -157,11 +162,6 @@ export function createChangedCheckChildEnv(baseEnv: NodeJS.ProcessEnv = process. }; } -function isTruthyEnvFlag(value: string | undefined) { - const normalized = (value ?? "").trim().toLowerCase(); - return normalized !== "" && normalized !== "0" && normalized !== "false" && normalized !== "no"; -} - function hasAndroidVersionSyncPath(paths: string[]) { return paths.some((changedPath) => ANDROID_VERSION_SYNC_PATHS.has(normalizeChangedPath(changedPath)), @@ -231,10 +231,10 @@ export function shouldDelegateChangedCheckToCrabbox( env: NodeJS.ProcessEnv = process.env, options: ChangedCheckDelegateOptions = {}, ) { - if (isTruthyEnvFlag(env.OPENCLAW_CHECK_CHANGED_REMOTE_CHILD)) { + if (isOpenEndedTruthyValue(env.OPENCLAW_CHECK_CHANGED_REMOTE_CHILD)) { return false; } - if (isTruthyEnvFlag(env.CI) || isTruthyEnvFlag(env.GITHUB_ACTIONS)) { + if (isOpenEndedTruthyValue(env.CI) || isOpenEndedTruthyValue(env.GITHUB_ACTIONS)) { return false; } if (argv.includes("--dry-run")) { @@ -247,7 +247,7 @@ export function shouldDelegateChangedCheckToCrabbox( if (result.paths.length === 0) { return false; } - if (isTruthyEnvFlag(env.OPENCLAW_TESTBOX)) { + if (isOpenEndedTruthyValue(env.OPENCLAW_TESTBOX)) { return true; } // Release metadata plans diff the supplied commits after classification. A missing @@ -708,7 +708,7 @@ export function createChangedCheckPlan( add("package patch guard", ["deps:patches:check"]); if ( hasDeadcodeScannedSource(result.paths) && - !isTruthyEnvFlag(baseEnv.OPENCLAW_CHECK_CHANGED_SKIP_DEADCODE) + !isOpenEndedTruthyValue(baseEnv.OPENCLAW_CHECK_CHANGED_SKIP_DEADCODE) ) { addCommand( "dead export scan (skip with OPENCLAW_CHECK_CHANGED_SKIP_DEADCODE=1)", @@ -1066,7 +1066,7 @@ export function createPnpmManagedCommand( env: NodeJS.ProcessEnv = process.env, ) { const commandEnv = command.env ?? resolveLocalHeavyCheckEnv(env); - if (isTruthyEnvFlag(commandEnv.CI) || isTruthyEnvFlag(commandEnv.GITHUB_ACTIONS)) { + if (isOpenEndedTruthyValue(commandEnv.CI) || isOpenEndedTruthyValue(commandEnv.GITHUB_ACTIONS)) { const shimmedEnv = prependCorepackPnpmShim(commandEnv); return { ...command, diff --git a/scripts/check-coercion-helper-declarations.mts b/scripts/check-coercion-helper-declarations.mts index af1c519befdd..76c7dfdee3ab 100644 --- a/scripts/check-coercion-helper-declarations.mts +++ b/scripts/check-coercion-helper-declarations.mts @@ -8,7 +8,7 @@ import { isCodeFile, listRepoFilesSync } from "./check-file-utils.js"; import { isDirectRunUrl } from "./lib/direct-run.mjs"; import { runWithFailedTrailer } from "./lib/failed-trailer.mts"; import { resolveRepoRoot } from "./lib/repo-root.mjs"; -import { toLine, unwrapExpression } from "./lib/ts-guard-utils.mts"; +import { getPropertyNameText, toLine, unwrapExpression } from "./lib/ts-guard-utils.mts"; const ABSOLUTE_LEGACY_COERCION_HELPER_NAMES = [ "asObject", @@ -30,6 +30,11 @@ export type CoercionHelperDeclarationKind = | "variable"; export const CANONICAL_COERCION_HELPER_OWNERS = [ + { + file: "packages/normalization-core/src/agent-id.ts", + kind: "function", + names: ["isValidAgentId", "normalizeAgentId"], + }, { file: "packages/normalization-core/src/string-coerce.ts", kind: "function", @@ -181,6 +186,7 @@ export const CANONICAL_COERCION_HELPER_OWNERS = [ }[]; export const CANONICAL_COERCION_MODULES = [ + "packages/normalization-core/src/agent-id.ts", "packages/normalization-core/src/string-coerce.ts", "packages/normalization-core/src/string-normalization.ts", "packages/normalization-core/src/number-coercion.ts", @@ -192,6 +198,8 @@ export const CANONICAL_COERCION_MODULES = [ "src/utils/boolean.ts", ] as const; +const MIXED_CANONICAL_COERCION_MODULES = ["scripts/lib/arg-utils.runtime.mjs"] as const; + export const DEFERRED_CANONICAL_COERCION_EXPORTS = [ { file: "packages/normalization-core/src/error-coercion.ts", @@ -210,28 +218,24 @@ const EXCEPTIONAL_COERCION_HELPER_CARVE_OUTS = [ file: "ui/src/test-helpers/control-ui-e2e.ts", name: "isRecord", kind: "function", - count: 1, reason: "Serialized mock Gateway closure cannot capture module imports.", }, { file: "scripts/lib/kova-report-gate.mts", name: "isRecord", kind: "function", - count: 1, reason: "Copied standalone report gate cannot rely on workspace package resolution.", }, { file: "scripts/lib/record-shared.mjs", name: "isRecord", kind: "function", - count: 1, reason: "Plain-Node shared helper serves MJS and E2E callers without package resolution.", }, { file: "scripts/pr-lib/process-group-runner.mjs", name: "toError", kind: "function", - count: 1, reason: "Bootstrap process supervisor preserves fallback errors without workspace dependencies.", }, @@ -239,11 +243,9 @@ const EXCEPTIONAL_COERCION_HELPER_CARVE_OUTS = [ file: "scripts/lib/bounded-response.mjs", name: "toLintErrorObject", kind: "function", - count: 1, reason: "Standalone copied response reader cannot resolve workspace packages.", }, ] as const satisfies readonly { - count: number; file: string; kind: CoercionHelperDeclarationKind; name: string; @@ -294,7 +296,6 @@ export type CanonicalCoercionExportAudit = { }; export type CoercionHelperCarveOut = { - count: number; file: string; kind: CoercionHelperDeclarationKind; name: BannedCoercionHelperName; @@ -308,7 +309,6 @@ function canonicalOwnerCarveOuts( file: owner.file, kind: owner.kind, name, - count: 1, reason: "Canonical coercion helper owned by this module.", })); } @@ -318,14 +318,10 @@ export const COERCION_HELPER_CARVE_OUTS: readonly CoercionHelperCarveOut[] = [ ...EXCEPTIONAL_COERCION_HELPER_CARVE_OUTS, ]; -type CarveOutMismatch = CoercionHelperCarveOut & { - actualCount: number; -}; - type CoercionHelperAudit = { excessDeclarations: CoercionHelperDeclaration[]; invalidCarveOuts: string[]; - staleCarveOuts: CarveOutMismatch[]; + staleCarveOuts: CoercionHelperCarveOut[]; }; type ScriptIo = { @@ -362,16 +358,6 @@ export function isGovernedCoercionHelperPath(filePath: string) { ); } -function propertyNameText(name: ts.PropertyName | undefined): string | undefined { - if (!name) { - return undefined; - } - if (ts.isIdentifier(name) || ts.isStringLiteral(name) || ts.isNumericLiteral(name)) { - return name.text; - } - return undefined; -} - function isCallableInitializer(expression: ts.Expression): boolean { const initializer = unwrapCallableInitializer(expression); return ts.isArrowFunction(initializer) || ts.isFunctionExpression(initializer); @@ -434,7 +420,7 @@ export function findBannedCoercionHelperDeclarations( }); } } else if (ts.isMethodDeclaration(node)) { - const name = propertyNameText(node.name); + const name = getPropertyNameText(node.name); if (name && BANNED_HELPER_NAMES.has(name)) { declarations.push({ file, @@ -444,7 +430,7 @@ export function findBannedCoercionHelperDeclarations( }); } } else if (ts.isPropertyDeclaration(node) && node.initializer) { - const name = propertyNameText(node.name); + const name = getPropertyNameText(node.name); if (name && BANNED_HELPER_NAMES.has(name) && isCallableInitializer(node.initializer)) { declarations.push({ file, @@ -454,7 +440,7 @@ export function findBannedCoercionHelperDeclarations( }); } } else if (ts.isPropertyAssignment(node)) { - const name = propertyNameText(node.name); + const name = getPropertyNameText(node.name); if (name && BANNED_HELPER_NAMES.has(name) && isCallableInitializer(node.initializer)) { declarations.push({ file, @@ -562,7 +548,7 @@ export function auditCanonicalCoercionExports( return { invalidClassifications, staleClassifications, unclassifiedExports }; } -/** Checks exact file/name/count carve-outs and rejects stale or excess entries. */ +/** Checks exact file/name/kind carve-outs and rejects stale or excess entries. */ export function auditCoercionHelperDeclarations( declarations: readonly CoercionHelperDeclaration[], carveOuts: readonly CoercionHelperCarveOut[], @@ -583,9 +569,6 @@ export function auditCoercionHelperDeclarations( `${carveOut.file} [${carveOut.name}] has invalid kind ${carveOut.kind}`, ); } - if (!Number.isInteger(carveOut.count) || carveOut.count < 1) { - invalidCarveOuts.push(`${carveOut.file} [${carveOut.name}] must have a positive count`); - } if (!carveOut.reason.trim()) { invalidCarveOuts.push(`${carveOut.file} [${carveOut.name}] needs a non-empty reason`); } @@ -602,22 +585,15 @@ export function auditCoercionHelperDeclarations( const excessDeclarations: CoercionHelperDeclaration[] = []; for (const [key, actual] of declarationsByKey) { - const allowedCount = carveOutByKey.get(key)?.count ?? 0; - if (actual.length > allowedCount) { - excessDeclarations.push(...actual.slice(allowedCount)); + if (carveOutByKey.has(key)) { + excessDeclarations.push(...actual.slice(1)); + } else { + excessDeclarations.push(...actual); } } - const staleCarveOuts = carveOuts - .map((carveOut): CarveOutMismatch | null => { - const actual = declarationsByKey.get(carveOutKey(carveOut)) ?? []; - return actual.length < carveOut.count - ? { - ...carveOut, - actualCount: actual.length, - } - : null; - }) - .filter((entry): entry is CarveOutMismatch => entry !== null); + const staleCarveOuts = carveOuts.filter( + (carveOut) => !declarationsByKey.has(carveOutKey(carveOut)), + ); return { excessDeclarations: excessDeclarations.toSorted( @@ -637,15 +613,28 @@ function writeLine(stream: ScriptIo["stdout"] | ScriptIo["stderr"], value: strin function auditDefaultCanonicalExports(repoRoot: string): CanonicalCoercionExportAudit { const canonicalModules = new Set(CANONICAL_COERCION_MODULES); + const mixedModules = new Set(MIXED_CANONICAL_COERCION_MODULES); + const auditedModules = [...CANONICAL_COERCION_MODULES, ...MIXED_CANONICAL_COERCION_MODULES]; const exportsByFile = new Map( - CANONICAL_COERCION_MODULES.map((file) => { + auditedModules.map((file) => { const source = fs.readFileSync(path.join(repoRoot, file), "utf8"); - return [file, findExportedCallableNames(source, file)] as const; + const exportedNames = findExportedCallableNames(source, file); + if (!mixedModules.has(file)) { + return [file, exportedNames] as const; + } + const registeredNames = new Set( + CANONICAL_COERCION_HELPER_OWNERS.filter((owner) => owner.file === file).flatMap( + (owner) => owner.names, + ), + ); + return [file, exportedNames.filter((name) => registeredNames.has(name))] as const; }), ); const classifications: CanonicalCoercionExportClassification[] = [ - ...CANONICAL_COERCION_HELPER_OWNERS.filter(({ file }) => canonicalModules.has(file)).flatMap( - ({ file, names }) => names.map((name) => ({ file, name, status: "enforced" as const })), + ...CANONICAL_COERCION_HELPER_OWNERS.filter( + ({ file }) => canonicalModules.has(file) || mixedModules.has(file), + ).flatMap(({ file, names }) => + names.map((name) => ({ file, name, status: "enforced" as const })), ), ...DEFERRED_CANONICAL_COERCION_EXPORTS.map(({ file, name, reason }) => ({ file, @@ -719,7 +708,7 @@ export function runCoercionHelperDeclarationGuard( for (const carveOut of audit.staleCarveOuts) { writeLine( io.stderr, - `- ${carveOut.file} [${carveOut.name}] expected ${carveOut.count} ${carveOut.kind} declaration(s), found ${carveOut.actualCount}; remove or reduce the carve-out`, + `- ${carveOut.file} [${carveOut.name}] has no ${carveOut.kind} declaration; remove the carve-out`, ); } } @@ -743,11 +732,11 @@ export function runCoercionHelperDeclarationGuard( } writeLine( io.stderr, - "Core/package/UI/workspace-script code: use the matching @openclaw/normalization-core coercion subpath.", + "Core/package/UI/workspace-script code: use the matching @openclaw/normalization-core export or module.", ); writeLine( io.stderr, - "Plugin production code: use openclaw/plugin-sdk/string-coerce-runtime, number-runtime, or error-runtime.", + "Bundled plugin production code: use the matching openclaw/plugin-sdk runtime; number-runtime is bundled/private-local, not a third-party typed contract.", ); writeLine( io.stderr, diff --git a/scripts/check-extension-plugin-sdk-boundary.mts b/scripts/check-extension-plugin-sdk-boundary.mts index 4c5450b9c4c4..5e4662af4d2d 100644 --- a/scripts/check-extension-plugin-sdk-boundary.mts +++ b/scripts/check-extension-plugin-sdk-boundary.mts @@ -1,9 +1,7 @@ #!/usr/bin/env node -import { promises as fs } from "node:fs"; import path from "node:path"; // Inventories extension imports to enforce plugin SDK boundary rules. -import { isRecord } from "@openclaw/normalization-core/record-coerce"; import { BUNDLED_PLUGIN_PATH_PREFIX, BUNDLED_PLUGIN_ROOT_DIR, @@ -22,7 +20,6 @@ import { runAsScript } from "./lib/ts-guard-utils.mts"; const DEFAULT_REPO_ROOT = resolveRepoRoot(import.meta.url); type BoundaryMode = | "src-outside-plugin-sdk" - | "plugin-sdk-internal" | "relative-outside-package" | "normalization-core-bypass"; type ModuleReference = { kind: string; line: number; specifier: string }; @@ -36,7 +33,6 @@ type BoundaryCheckIo = { const MODES = new Set([ "src-outside-plugin-sdk", - "plugin-sdk-internal", "relative-outside-package", "normalization-core-bypass", ]); @@ -44,27 +40,16 @@ const MODES = new Set([ const ruleTextByMode: Record = { "src-outside-plugin-sdk": "Rule: production bundled plugins must not import src/** outside src/plugin-sdk/**", - "plugin-sdk-internal": - "Rule: production bundled plugins must not import src/plugin-sdk-internal/**", "relative-outside-package": "Rule: production bundled plugins must not use relative imports that escape their own package root", "normalization-core-bypass": "Rule: production bundled plugins must not import normalization-core directly; use the matching openclaw/plugin-sdk coercion runtime", }; -type BaselineBoundaryMode = "plugin-sdk-internal" | "src-outside-plugin-sdk"; const NORMALIZATION_CORE_PACKAGE = "@openclaw/normalization-core"; const NORMALIZATION_CORE_ROOT = "packages/normalization-core"; const DIRECT_COERCION_OWNER_PATHS = new Set(["src/infra/errors", "src/utils/boolean"]); -function baselinePathForMode(repoRoot: string, mode: BaselineBoundaryMode): string { - const fileName = - mode === "src-outside-plugin-sdk" - ? "extension-src-outside-plugin-sdk-inventory.json" - : "extension-plugin-sdk-internal-inventory.json"; - return path.join(repoRoot, "test", "fixtures", fileName); -} - function stripModuleExtension(filePath: string): string { return filePath.replace(/\.(?:[cm]?[jt]s|tsx|jsx)$/u, ""); } @@ -127,9 +112,12 @@ function classifyReason(mode: BoundaryMode, kind: string, resolved: string, spec : "imports"; if (mode === "normalization-core-bypass") { const facade = recommendedCoercionFacade(resolved); + if (facade === "openclaw/plugin-sdk/number-runtime") { + return `${verb} ${specifier} directly; bundled plugin production code must use bundled/private-local ${facade}`; + } return facade - ? `${verb} ${specifier} directly; plugin production code must use ${facade}` - : `${verb} ${specifier} directly; plugin production code must use the matching public openclaw/plugin-sdk facade, adding a narrow public SDK seam if needed`; + ? `${verb} ${specifier} directly; bundled plugin production code must use ${facade}` + : `${verb} ${specifier} directly; bundled plugin production code must use the matching openclaw/plugin-sdk facade, adding a narrow public SDK seam if needed`; } if (mode === "relative-outside-package") { if (resolved.startsWith("src/plugin-sdk/")) { @@ -143,9 +131,6 @@ function classifyReason(mode: BoundaryMode, kind: string, resolved: string, spec } return `${verb} relative path ${specifier} outside the extension package`; } - if (mode === "plugin-sdk-internal") { - return `${verb} src/plugin-sdk-internal from an extension`; - } if (resolved.startsWith("src/plugin-sdk/")) { return `${verb} allowed plugin-sdk path`; } @@ -163,57 +148,6 @@ function compareEntries(left: BoundaryEntry, right: BoundaryEntry): number { ); } -function isBoundaryEntry(value: unknown): value is BoundaryEntry { - return ( - isRecord(value) && - typeof value.file === "string" && - typeof value.line === "number" && - typeof value.kind === "string" && - typeof value.specifier === "string" && - typeof value.resolvedPath === "string" && - typeof value.reason === "string" - ); -} - -function isBoundaryEntryArray(value: unknown): value is BoundaryEntry[] { - return Array.isArray(value) && value.every(isBoundaryEntry); -} - -async function readExpectedInventoryAtRoot( - repoRoot: string, - mode: BaselineBoundaryMode, -): Promise { - const baselinePath = baselinePathForMode(repoRoot, mode); - try { - const inventory: unknown = JSON.parse(await fs.readFile(baselinePath, "utf8")); - if (!isBoundaryEntryArray(inventory)) { - throw new Error(`Invalid boundary inventory: ${baselinePath}`); - } - return inventory; - } catch (error) { - if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") { - return []; - } - throw error; - } -} - -/** - * Diffs expected and actual boundary inventory entries. - */ -export function diffInventory(expected: BoundaryEntry[], actual: BoundaryEntry[]) { - const expectedKeys = new Set(expected.map((entry) => JSON.stringify(entry))); - const actualKeys = new Set(actual.map((entry) => JSON.stringify(entry))); - return { - missing: expected - .filter((entry) => !actualKeys.has(JSON.stringify(entry))) - .toSorted(compareEntries), - unexpected: actual - .filter((entry) => !expectedKeys.has(JSON.stringify(entry))) - .toSorted(compareEntries), - }; -} - const formatInventoryHuman = (mode: BoundaryMode, inventory: BoundaryEntry[]): string => formatGroupedInventoryHuman( { @@ -254,9 +188,6 @@ export function createExtensionPluginSdkBoundaryChecker(options: { repoRoot?: st if (resolvedPath.startsWith("src/") && !resolvedPath.startsWith("src/plugin-sdk/")) { modes.push("src-outside-plugin-sdk"); } - if (resolvedPath.startsWith("src/plugin-sdk-internal/")) { - modes.push("plugin-sdk-internal"); - } if (isNormalizationCoreBypass(specifier, resolvedPath)) { modes.push("normalization-core-bypass"); } @@ -347,42 +278,16 @@ export function createExtensionPluginSdkBoundaryChecker(options: { repoRoot?: st } const actual = await collectInventory(mode); - const strictMode = mode === "normalization-core-bypass" || mode === "relative-outside-package"; if (json) { writeLine(streams.stdout, JSON.stringify(actual, null, 2)); - return strictMode && actual.length > 0 ? 1 : 0; + return actual.length > 0 ? 1 : 0; } writeLine(streams.stdout, formatInventoryHuman(mode, actual)); - if (strictMode) { - if (actual.length === 0) { - return 0; - } - writeLine( - streams.stderr, - `${ruleTextByMode[mode]} violations found (${actual.length}); this strict mode has no baseline.`, - ); - return 1; - } - - const expected = await readExpectedInventoryAtRoot(repoRoot, mode); - const diff = diffInventory(expected, actual); - if (diff.missing.length === 0 && diff.unexpected.length === 0) { - writeLine(streams.stdout, `Baseline matches (${actual.length} entries).`); + if (actual.length === 0) { return 0; } - if (diff.missing.length > 0) { - writeLine(streams.stderr, `Missing baseline entries (${diff.missing.length}):`); - for (const entry of diff.missing) { - writeLine(streams.stderr, ` - ${entry.file}:${entry.line} ${entry.reason}`); - } - } - if (diff.unexpected.length > 0) { - writeLine(streams.stderr, `Unexpected inventory entries (${diff.unexpected.length}):`); - for (const entry of diff.unexpected) { - writeLine(streams.stderr, ` - ${entry.file}:${entry.line} ${entry.reason}`); - } - } + writeLine(streams.stderr, `${ruleTextByMode[mode]} violations found (${actual.length}).`); return 1; } @@ -391,11 +296,6 @@ export function createExtensionPluginSdkBoundaryChecker(options: { repoRoot?: st const defaultBoundaryChecker = createExtensionPluginSdkBoundaryChecker(); -/** Reads the checked-in expected boundary inventory from the real repository. */ -export async function readExpectedInventory(mode: BaselineBoundaryMode): Promise { - return await readExpectedInventoryAtRoot(DEFAULT_REPO_ROOT, mode); -} - /** * Entrypoint wrapper for the extension plugin SDK boundary check. */ diff --git a/scripts/check-gateway-watch-regression.mts b/scripts/check-gateway-watch-regression.mts index e33905de71cf..7a7bd9f0a575 100644 --- a/scripts/check-gateway-watch-regression.mts +++ b/scripts/check-gateway-watch-regression.mts @@ -14,9 +14,12 @@ import { writeBuildStamp, writeRuntimePostBuildStamp, } from "./lib/local-build-metadata.mts"; +import { parseStrictNonNegativeDecimal as readNonNegativeInteger } from "./lib/numeric-options.mjs"; import { sleep } from "./lib/sleep.mjs"; import { resolveBuildRequirement } from "./run-node.mts"; +export { readNonNegativeInteger }; + const DEFAULTS = { outputDir: path.join(process.cwd(), ".local", "gateway-watch-regression"), windowMs: 10_000, @@ -52,7 +55,6 @@ const WATCH_GATEWAY_SKIP_ENV = { export const WATCH_LOG_CAPTURE_MAX_CHARS = 2 * 1024 * 1024; export const WATCH_LOG_FAILURE_TAIL_CHARS = 12_000; const WATCH_BUILD_DETECTION_MAX_CHARS = 4096; -const NON_NEGATIVE_INTEGER_PATTERN = /^(0|[1-9]\d*)$/u; const ANSI_ESCAPE_PATTERN = new RegExp(`${String.fromCharCode(27)}\\[[0-?]*[ -/]*[@-~]`, "g"); type WatchOptions = typeof DEFAULTS; @@ -204,21 +206,6 @@ export function updateWatchBuildDetection( }; } -/** - * Parses a safe non-negative integer CLI value. - */ -export function readNonNegativeInteger(value: unknown, label: string): number { - const raw = String(value).trim(); - if (!NON_NEGATIVE_INTEGER_PATTERN.test(raw)) { - throw new Error(`${label} must be a non-negative integer`); - } - const parsed = Number(raw); - if (!Number.isSafeInteger(parsed)) { - throw new Error(`${label} must be a safe integer`); - } - return parsed; -} - /** * Parses gateway watch regression CLI arguments. */ diff --git a/scripts/check-memory-fd-repro.mts b/scripts/check-memory-fd-repro.mts index 047b647d8da1..0ceb9d77586d 100644 --- a/scripts/check-memory-fd-repro.mts +++ b/scripts/check-memory-fd-repro.mts @@ -8,10 +8,15 @@ import os from "node:os"; import path from "node:path"; import process from "node:process"; import { pathToFileURL } from "node:url"; +import { safeParseJson } from "../packages/normalization-core/src/json-coercion.ts"; import { resolveTimerTimeoutMs } from "../packages/normalization-core/src/number-coercion.ts"; import { asNullableRecord as asRecord } from "../packages/normalization-core/src/record-coerce.ts"; +import { readNonBlankString } from "../packages/normalization-core/src/string-coerce.ts"; import { stripLeadingPackageManagerSeparator } from "./lib/arg-utils.mts"; import { readBoundedResponseText } from "./lib/bounded-response.mjs"; +import { parseStrictNonNegativeDecimal as parseNonNegativeInteger } from "./lib/numeric-options.mjs"; + +export { parseNonNegativeInteger }; const ISSUE_FILE_COUNTS = [ ["memory/transcripts", 9394], @@ -99,7 +104,6 @@ Options: `.trim(); } -const NON_NEGATIVE_INTEGER_PATTERN = /^(0|[1-9]\d*)$/u; const ARGUMENT_FLAGS = new Set([ "--allow-non-darwin", "--expect-leak", @@ -123,21 +127,6 @@ function stripPackageManagerSeparatorForKnownFlags(argv: string[]) { : argv; } -/** - * Parses a safe non-negative integer option. - */ -export function parseNonNegativeInteger(value: unknown, label: string) { - const raw = String(value).trim(); - if (!NON_NEGATIVE_INTEGER_PATTERN.test(raw)) { - throw new Error(`${label} must be a non-negative integer`); - } - const parsed = Number(raw); - if (!Number.isSafeInteger(parsed)) { - throw new Error(`${label} must be a safe integer`); - } - return parsed; -} - /** * Parses a safe positive integer option. */ @@ -615,19 +604,6 @@ async function waitForChildExit( return hasChildExited(child); } -function parseJsonValue(text: string): unknown { - try { - return JSON.parse(text); - } catch { - return null; - } -} - -function readStringProperty(record: Record | null, key: string) { - const value = record?.[key]; - return typeof value === "string" && value.trim() ? value : undefined; -} - function parseToolTextContent(result: Record | null) { const content = Array.isArray(result?.content) ? result.content : []; for (const entry of content) { @@ -636,7 +612,7 @@ function parseToolTextContent(result: Record | null) { if (!text) { continue; } - const parsed = asRecord(parseJsonValue(text)); + const parsed = asRecord(safeParseJson(text)); if (parsed) { return parsed; } @@ -652,7 +628,7 @@ export function classifyMemorySearchInvokeResponse({ status, bodyText, }: InvokeResponseOptions) { - const parsedBody = parseJsonValue(bodyText); + const parsedBody = safeParseJson(bodyText); const body = asRecord(parsedBody); if (!httpOk) { const errorRecord = asRecord(body?.error); @@ -662,8 +638,8 @@ export function classifyMemorySearchInvokeResponse({ status, gatewayOk: body?.ok === true ? true : body?.ok === false ? false : undefined, error: - readStringProperty(errorRecord, "message") ?? - readStringProperty(body, "error") ?? + readNonBlankString(errorRecord?.message) ?? + readNonBlankString(body?.error) ?? `memory_search HTTP request failed with status ${status}`, }; } @@ -685,8 +661,8 @@ export function classifyMemorySearchInvokeResponse({ status, gatewayOk, error: - readStringProperty(errorRecord, "message") ?? - readStringProperty(body, "error") ?? + readNonBlankString(errorRecord?.message) ?? + readNonBlankString(body.error) ?? "memory_search gateway invocation failed", }; } @@ -711,7 +687,7 @@ export function classifyMemorySearchInvokeResponse({ const resultCount = Array.isArray(payload.results) ? payload.results.length : undefined; const toolDisabled = payload.disabled === true; const toolUnavailable = payload.unavailable === true; - const toolError = readStringProperty(payload, "error"); + const toolError = readNonBlankString(payload.error); const ok = gatewayOk === true && !toolDisabled && !toolUnavailable && !toolError; return { diff --git a/scripts/check-plugin-gateway-gauntlet.mts b/scripts/check-plugin-gateway-gauntlet.mts index df3097470fad..fb4b3cb8db49 100644 --- a/scripts/check-plugin-gateway-gauntlet.mts +++ b/scripts/check-plugin-gateway-gauntlet.mts @@ -11,6 +11,7 @@ import { MAX_TIMER_TIMEOUT_MS, resolveTimerTimeoutMs, } from "../packages/normalization-core/src/number-coercion.ts"; +import { normalizeCsvOrLooseStringList } from "../packages/normalization-core/src/string-normalization.ts"; import { stripLeadingPackageManagerSeparator } from "./lib/arg-utils.mts"; import { parseNonNegativeInt, @@ -161,7 +162,7 @@ export function parseArgs(argv: string[]) { failOnObservation: process.env.OPENCLAW_PLUGIN_GATEWAY_GAUNTLET_FAIL_ON_OBSERVATION === "1", keepRunRoot: process.env.OPENCLAW_PLUGIN_GATEWAY_GAUNTLET_KEEP_RUN_ROOT === "1", }; - const envIds = normalizeCsv(process.env.OPENCLAW_PLUGIN_GATEWAY_GAUNTLET_IDS); + const envIds = normalizeCsvOrLooseStringList(process.env.OPENCLAW_PLUGIN_GATEWAY_GAUNTLET_IDS); options.pluginIds.push(...envIds); const seenSingleValueFlags = new Set(); parseArgv: for (let index = 0; index < args.length; index += 1) { @@ -331,15 +332,6 @@ Environment: `); } -function normalizeCsv(raw: string | undefined) { - return raw - ? raw - .split(",") - .map((entry) => entry.trim()) - .filter((entry) => entry.length > 0) - : []; -} - function assertNoDuplicateValues(values: string[], label: string) { const seen = new Set(); for (const value of values) { diff --git a/scripts/check-release-metadata-only.mts b/scripts/check-release-metadata-only.mts index 14851645b006..041f4532f337 100644 --- a/scripts/check-release-metadata-only.mts +++ b/scripts/check-release-metadata-only.mts @@ -4,6 +4,7 @@ import { execFileSync } from "node:child_process"; import { existsSync, readFileSync } from "node:fs"; import path from "node:path"; import { isRecord } from "@openclaw/normalization-core/record-coerce"; +import { stableStringify } from "../packages/normalization-core/src/stable-stringify.ts"; import { RELEASE_METADATA_PATHS } from "./changed-lanes.mts"; const DEFAULT_GIT_TIMEOUT_MS = 60_000; @@ -150,20 +151,7 @@ function stripPackageVersion(raw: string) { throw new Error("package.json must contain an object"); } delete parsed.version; - return stableJson(parsed); -} - -function stableJson(value: unknown): string | undefined { - if (Array.isArray(value)) { - return `[${value.map(stableJson).join(",")}]`; - } - if (isRecord(value)) { - return `{${Object.keys(value) - .toSorted((left, right) => left.localeCompare(right)) - .map((key) => `${JSON.stringify(key)}:${stableJson(value[key])}`) - .join(",")}}`; - } - return JSON.stringify(value); + return stableStringify(parsed); } function normalizeVersionText(raw: string) { diff --git a/scripts/control-ui-i18n.ts b/scripts/control-ui-i18n.ts index 734d30537590..73f78f5f2604 100644 --- a/scripts/control-ui-i18n.ts +++ b/scripts/control-ui-i18n.ts @@ -14,6 +14,7 @@ import { verifyControlUiGeneratedCatalogs, verifyRuntimeLocaleConfig, } from "./control-ui-i18n-verify.ts"; +import { isStrictAffirmativeValue } from "./lib/arg-utils.mts"; import { hashControlUiTranslationText, loadControlUiTranslationMemory, @@ -487,8 +488,7 @@ export function isProviderAuthError(error: Error): boolean { } function isProviderAuthOptional(): boolean { - const raw = process.env[ENV_AUTH_OPTIONAL]?.trim().toLowerCase(); - return raw === "1" || raw === "true" || raw === "yes"; + return isStrictAffirmativeValue(process.env[ENV_AUTH_OPTIONAL]); } function resolvePromptTimeoutMs(): number { diff --git a/scripts/e2e/kitchen-sink-rpc-walk.mts b/scripts/e2e/kitchen-sink-rpc-walk.mts index 18e1362a9ad6..abfa7dd32c2f 100644 --- a/scripts/e2e/kitchen-sink-rpc-walk.mts +++ b/scripts/e2e/kitchen-sink-rpc-walk.mts @@ -13,6 +13,7 @@ import process from "node:process"; import { setTimeout as delay } from "node:timers/promises"; import { fileURLToPath, pathToFileURL } from "node:url"; import { asRecord, isRecord } from "@openclaw/normalization-core/record-coerce"; +import { hasNonEmptyString } from "@openclaw/normalization-core/string-coerce"; import { createBoundedResponseTooLargeError, readBoundedResponseText, @@ -885,8 +886,8 @@ function createGatewayClientRequestError(requestError: unknown): GatewayRequestE const candidate = asRecord(requestError); if ( candidate.type !== "gateway_request_error" || - !isNonEmptyString(candidate.code) || - !isNonEmptyString(candidate.message) || + !hasNonEmptyString(candidate.code) || + !hasNonEmptyString(candidate.message) || typeof candidate.retryable !== "boolean" || (candidate.retryAfterMs !== undefined && (typeof candidate.retryAfterMs !== "number" || @@ -1710,7 +1711,7 @@ export function extractPluginCommandNames(payload: unknown) { } } return names - .filter(isNonEmptyString) + .filter(hasNonEmptyString) .map((name) => name.replace(/^\//u, "")) .filter((name, index, all) => all.indexOf(name) === index) .toSorted((left, right) => left.localeCompare(right)); @@ -1744,7 +1745,7 @@ export function assertExpectedKitchenSinkToolEntries( options: { requirePluginProvenance?: boolean } = {}, ) { const { requirePluginProvenance = false } = options; - const ids = entries.map((entry) => asRecord(entry).id).filter(isNonEmptyString); + const ids = entries.map((entry) => asRecord(entry).id).filter(hasNonEmptyString); assertIncludesAll(ids, EXPECTED_TOOLS, label); if (requirePluginProvenance) { const wrongProvenance = entries @@ -1772,7 +1773,7 @@ export function assertChannelAccountRunning(payload: unknown) { const accounts = Array.isArray(channelAccounts[CHANNEL_ID]) ? channelAccounts[CHANNEL_ID] : []; const account = accounts.find((entry) => asRecord(entry).accountId === CHANNEL_ACCOUNT_ID); if (!account) { - const accountIds = accounts.map((entry) => asRecord(entry).accountId).filter(isNonEmptyString); + const accountIds = accounts.map((entry) => asRecord(entry).accountId).filter(hasNonEmptyString); throw new Error( `Kitchen Sink channel account ${CHANNEL_ACCOUNT_ID} was not reported. Available account ids: ${boundedJsonPreview( accountIds, @@ -1801,12 +1802,12 @@ export function assertTtsProviderCoverage(payload: unknown, surface: "providers" `tts.${surface} returned invalid provider list: ${boundedJsonPreview(payload)}`, ); } - const ids = entries.map((entry) => asRecord(entry).id).filter(isNonEmptyString); + const ids = entries.map((entry) => asRecord(entry).id).filter(hasNonEmptyString); assertIncludesAny(ids, EXPECTED_SPEECH_PROVIDERS, `tts.${surface}`); const configuredEntry = entries.find((entry) => { const provider = asRecord(entry); return ( - isNonEmptyString(provider.id) && + hasNonEmptyString(provider.id) && EXPECTED_SPEECH_PROVIDERS.includes(provider.id) && provider.configured === true ); @@ -1990,7 +1991,7 @@ export async function assertOperatorRpcDenied( export function assertCreatedKitchenSinkSession(payload: unknown, expectedKey = SESSION_KEY) { const created = assertObjectPayload(payload, "sessions.create"); - if (created.ok !== true || created.key !== expectedKey || !isNonEmptyString(created.sessionId)) { + if (created.ok !== true || created.key !== expectedKey || !hasNonEmptyString(created.sessionId)) { throw new Error( `sessions.create did not return the requested Kitchen Sink session: ${boundedJsonPreview( payload, @@ -2076,11 +2077,11 @@ export function assertGatewayHealthPayload(payload: unknown) { [Number.isFinite(health.durationMs), "numeric durationMs"], [isRecord(health.channels), "channels object"], [Array.isArray(health.channelOrder), "channelOrder array"], - [isNonEmptyString(health.defaultAgentId), "defaultAgentId"], + [hasNonEmptyString(health.defaultAgentId), "defaultAgentId"], [Array.isArray(health.agents), "agents array"], [ isRecord(sessions) && - isNonEmptyString(sessions.path) && + hasNonEmptyString(sessions.path) && Number.isFinite(sessions.count) && Array.isArray(sessions.recent), "sessions summary", @@ -2099,7 +2100,7 @@ export function assertGatewayStatusPayload(payload: unknown) { const problems = failedPayloadChecks([ [ isRecord(heartbeat) && - isNonEmptyString(heartbeat.defaultAgentId) && + hasNonEmptyString(heartbeat.defaultAgentId) && Array.isArray(heartbeat.agents), "heartbeat summary", ], @@ -2730,10 +2731,6 @@ function tailText(text: string) { return text.split(/\r?\n/u).slice(-120).join("\n"); } -function isNonEmptyString(value: unknown): value is string { - return typeof value === "string" && value.trim().length > 0; -} - async function main() { const config = resolveKitchenSinkRpcConfig(); let runner = resolveOpenClawRunner(); diff --git a/scripts/e2e/lib/codex-media-path/limits.mjs b/scripts/e2e/lib/codex-media-path/limits.mjs index a163533302bf..1d480c59ca0a 100644 --- a/scripts/e2e/lib/codex-media-path/limits.mjs +++ b/scripts/e2e/lib/codex-media-path/limits.mjs @@ -1,21 +1,2 @@ -// Limits shared by Codex media-path E2E fixtures. -export function readPositiveIntEnv(name, fallback, env = process.env) { - const text = String(env[name] ?? fallback).trim(); - if (!/^\d+$/u.test(text)) { - throw new Error(`invalid ${name}: ${text}`); - } - const value = Number(text); - if (!Number.isSafeInteger(value) || value <= 0) { - throw new Error(`invalid ${name}: ${text}`); - } - return value; -} - -export function readTcpPortEnv(name, fallback, env = process.env) { - const value = readPositiveIntEnv(name, fallback, env); - if (value > 65_535) { - const text = String(env[name] ?? fallback).trim(); - throw new Error(`invalid ${name}: ${text}`); - } - return value; -} +// Compatibility path for Codex media-path E2E fixtures. +export { readPositiveIntEnv, readTcpPortEnv } from "../env-limits.mjs"; diff --git a/scripts/e2e/lib/fixtures/workspace.mjs b/scripts/e2e/lib/fixtures/workspace.mjs index 37272abb19a2..7636921aaddf 100644 --- a/scripts/e2e/lib/fixtures/workspace.mjs +++ b/scripts/e2e/lib/fixtures/workspace.mjs @@ -1,6 +1,7 @@ // Workspace fixture writer commands for E2E scenarios. import fs from "node:fs"; import path from "node:path"; +import { readPositiveIntEnv } from "../env-limits.mjs"; import { readTextFileTail } from "../text-file-utils.mjs"; import { assert, readJson, requireArg, write, writeJson } from "./common.mjs"; @@ -10,18 +11,6 @@ const AGENTS_DELETE_OUTPUT_MAX_BYTES = readPositiveIntEnv( ); const ERROR_DETAIL_TAIL_BYTES = 16 * 1024; -function readPositiveIntEnv(name, fallback) { - const text = String(process.env[name] ?? fallback).trim(); - if (!/^\d+$/u.test(text)) { - throw new Error(`invalid ${name}: ${text}`); - } - const value = Number(text); - if (!Number.isSafeInteger(value) || value <= 0) { - throw new Error(`invalid ${name}: ${text}`); - } - return value; -} - function writeOpenWebUiWorkspace() { const workspace = process.env.OPENCLAW_WORKSPACE_DIR || path.join(process.env.HOME, ".openclaw", "workspace"); diff --git a/scripts/e2e/lib/gateway-network/limits.mts b/scripts/e2e/lib/gateway-network/limits.mts index 4b355a848087..4f22ad323d9e 100644 --- a/scripts/e2e/lib/gateway-network/limits.mts +++ b/scripts/e2e/lib/gateway-network/limits.mts @@ -1,15 +1,5 @@ // Limits shared by gateway network E2E fixtures. -function readPositiveIntEnv(name: string, fallback: number, env: NodeJS.ProcessEnv) { - const text = String(env[name] ?? fallback).trim(); - if (!/^\d+$/u.test(text)) { - throw new Error(`invalid ${name}: ${text}`); - } - const value = Number(text); - if (!Number.isSafeInteger(value) || value <= 0) { - throw new Error(`invalid ${name}: ${text}`); - } - return value; -} +import { readPositiveIntEnv } from "../env-limits.mjs"; export function readGatewayNetworkClientConnectTimeoutMs(env: NodeJS.ProcessEnv = process.env) { if (env.OPENCLAW_GATEWAY_NETWORK_CLIENT_CONNECT_TIMEOUT_MS != null) { diff --git a/scripts/e2e/lib/live-plugin-tool/assertions.mjs b/scripts/e2e/lib/live-plugin-tool/assertions.mjs index c1eb51af26fb..2ec09c49ee42 100644 --- a/scripts/e2e/lib/live-plugin-tool/assertions.mjs +++ b/scripts/e2e/lib/live-plugin-tool/assertions.mjs @@ -4,24 +4,13 @@ import path from "node:path"; import { DatabaseSync } from "node:sqlite"; import { isRecord } from "../../../lib/record-shared.mjs"; import { extractAgentReplyTexts } from "../agent-turn-output.mjs"; +import { readPositiveIntEnv } from "../env-limits.mjs"; import { readPluginInstallRecords } from "../plugin-index-sqlite.mjs"; import { readTextFileTail, tailText } from "../text-file-utils.mjs"; const command = process.argv[2]; const readJson = (file) => JSON.parse(fs.readFileSync(file, "utf8")); -function readPositiveIntEnv(name, fallback) { - const text = String(process.env[name] ?? fallback).trim(); - if (!/^\d+$/u.test(text)) { - throw new Error(`invalid ${name}: ${text}`); - } - const value = Number(text); - if (!Number.isSafeInteger(value) || value <= 0) { - throw new Error(`invalid ${name}: ${text}`); - } - return value; -} - const agentTurnTimeoutSeconds = readPositiveIntEnv( "OPENCLAW_LIVE_PLUGIN_TOOL_TIMEOUT_SECONDS", 300, diff --git a/scripts/e2e/npm-telegram-live-runner.ts b/scripts/e2e/npm-telegram-live-runner.ts index eb88f0b93c93..76b008243702 100644 --- a/scripts/e2e/npm-telegram-live-runner.ts +++ b/scripts/e2e/npm-telegram-live-runner.ts @@ -8,18 +8,8 @@ import { pathToFileURL } from "node:url"; import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts"; import type { QaProviderMode } from "../../extensions/qa-lab/src/run-config.ts"; import type { QaSuiteRoundTripProbe } from "../../extensions/qa-lab/src/suite-round-trip.ts"; - -function isTruthyNpmTelegramEnvValue(value: string | undefined) { - const normalized = value?.trim().toLowerCase(); - return normalized === "1" || normalized === "true" || normalized === "yes"; -} - -function splitCsv(value: string | undefined) { - return (value ?? "") - .split(",") - .map((entry) => entry.trim()) - .filter((entry) => entry.length > 0); -} +import { normalizeCsvOrLooseStringList } from "../../packages/normalization-core/src/string-normalization.ts"; +import { isStrictAffirmativeValue } from "../lib/arg-utils.mts"; function parsePositiveIntegerEnv(env: NodeJS.ProcessEnv, name: string) { const raw = env[name]?.trim(); @@ -89,7 +79,7 @@ function resolvePackageConfigMutation(env: NodeJS.ProcessEnv = process.env) { } function resolveRttOptions(env: NodeJS.ProcessEnv, selectedScenarioIds: readonly string[] = []) { - const explicitCheckIds = splitCsv(env.OPENCLAW_NPM_TELEGRAM_RTT_CHECKS); + const explicitCheckIds = normalizeCsvOrLooseStringList(env.OPENCLAW_NPM_TELEGRAM_RTT_CHECKS); const checkIds = explicitCheckIds.length > 0 ? explicitCheckIds : [DEFAULT_RTT_CHECK_ID]; const unknownCheckIds = checkIds.filter((checkId) => checkId !== DEFAULT_RTT_CHECK_ID); if (unknownCheckIds.length > 0) { @@ -147,7 +137,7 @@ async function shouldFailPackageTelegramRun( result: { summaryPath: string }, env: NodeJS.ProcessEnv = process.env, ) { - if (isTruthyNpmTelegramEnvValue(env.OPENCLAW_NPM_TELEGRAM_ALLOW_FAILURES)) { + if (isStrictAffirmativeValue(env.OPENCLAW_NPM_TELEGRAM_ALLOW_FAILURES)) { return false; } const { readQaSuiteFailedOrSkippedScenarioCountFromFile } = @@ -204,7 +194,7 @@ async function main() { const repoRoot = path.resolve(process.env.OPENCLAW_NPM_TELEGRAM_REPO_ROOT ?? process.cwd()); const outputDir = resolvePackageTelegramOutputDir(process.env, repoRoot); - const scenarioIds = splitCsv(process.env.OPENCLAW_NPM_TELEGRAM_SCENARIOS); + const scenarioIds = normalizeCsvOrLooseStringList(process.env.OPENCLAW_NPM_TELEGRAM_SCENARIOS); const providerMode = (process.env.OPENCLAW_NPM_TELEGRAM_PROVIDER_MODE as QaProviderMode | undefined) ?? DEFAULT_QA_LIVE_PROVIDER_MODE; @@ -224,7 +214,7 @@ async function main() { providerMode, primaryModel, alternateModel: process.env.OPENCLAW_NPM_TELEGRAM_ALT_MODEL, - fastMode: isTruthyNpmTelegramEnvValue(process.env.OPENCLAW_NPM_TELEGRAM_FAST), + fastMode: isStrictAffirmativeValue(process.env.OPENCLAW_NPM_TELEGRAM_FAST), scenarioIds, resolvedScenarioIds: prioritizeRoundTripProbeScenario(resolvedScenarioIds, rttOptions), roundTripProbe: createRoundTripProbe(rttOptions), diff --git a/scripts/e2e/parallels/linux-smoke.ts b/scripts/e2e/parallels/linux-smoke.ts index 1c34c9f2ff14..eb2bf0cdccdd 100755 --- a/scripts/e2e/parallels/linux-smoke.ts +++ b/scripts/e2e/parallels/linux-smoke.ts @@ -3,6 +3,7 @@ import { mkdir, readFile } from "node:fs/promises"; import path from "node:path"; import { pathToFileURL } from "node:url"; +import { stripLeadingPackageManagerSeparator } from "../../lib/arg-utils.mts"; import { posixAgentWorkspaceScript } from "./agent-workspace.ts"; import { die, @@ -248,10 +249,6 @@ export function parseArgs(argv: string[]): LinuxOptions { return options; } -function stripLeadingPackageManagerSeparator(argv: string[]): string[] { - return argv[0] === "--" ? argv.slice(1) : argv; -} - class LinuxSmoke extends SmokeRunController { private auth: ProviderAuth; private disableBonjour = parseBoolEnv(process.env.OPENCLAW_PARALLELS_LINUX_DISABLE_BONJOUR); diff --git a/scripts/e2e/parallels/macos-smoke.ts b/scripts/e2e/parallels/macos-smoke.ts index 1cf2822bbc1c..075e2d2b35a5 100755 --- a/scripts/e2e/parallels/macos-smoke.ts +++ b/scripts/e2e/parallels/macos-smoke.ts @@ -3,6 +3,7 @@ import { readFile, rm } from "node:fs/promises"; import path from "node:path"; import { pathToFileURL } from "node:url"; +import { stripLeadingPackageManagerSeparator } from "../../lib/arg-utils.mts"; import { posixAgentWorkspaceScript } from "./agent-workspace.ts"; import { die, @@ -268,10 +269,6 @@ export function parseArgs(argv: string[]): MacosOptions { return options; } -function stripLeadingPackageManagerSeparator(argv: string[]): string[] { - return argv[0] === "--" ? argv.slice(1) : argv; -} - class MacosSmoke { private agentTimeoutSeconds: number; private auth: ProviderAuth; diff --git a/scripts/e2e/parallels/npm-update-smoke.ts b/scripts/e2e/parallels/npm-update-smoke.ts index b8fd35c8d362..6c6af87b6e6b 100755 --- a/scripts/e2e/parallels/npm-update-smoke.ts +++ b/scripts/e2e/parallels/npm-update-smoke.ts @@ -14,6 +14,7 @@ import { import { isRecord } from "@openclaw/normalization-core/record-coerce"; import { readStringValue } from "@openclaw/normalization-core/string-coerce"; import prettyMilliseconds from "pretty-ms"; +import { stripLeadingPackageManagerSeparator } from "../../lib/arg-utils.mts"; import { die, ensureValue, @@ -525,10 +526,6 @@ export function parseArgs(argv: string[]): NpmUpdateOptions { return options; } -function stripLeadingPackageManagerSeparator(argv: string[]): string[] { - return argv[0] === "--" ? argv.slice(1) : argv; -} - function platformRecord(value: T): Record { return { linux: value, macos: value, windows: value }; } diff --git a/scripts/e2e/parallels/windows-smoke.ts b/scripts/e2e/parallels/windows-smoke.ts index 7d6936ba6648..8340d3bddad3 100755 --- a/scripts/e2e/parallels/windows-smoke.ts +++ b/scripts/e2e/parallels/windows-smoke.ts @@ -2,6 +2,7 @@ // Windows Smoke script supports OpenClaw repository automation. import path from "node:path"; import { pathToFileURL } from "node:url"; +import { stripLeadingPackageManagerSeparator } from "../../lib/arg-utils.mts"; import { windowsAgentWorkspaceScript } from "./agent-workspace.ts"; import { die, @@ -246,10 +247,6 @@ export function parseArgs(argv: string[]): WindowsOptions { return options; } -function stripLeadingPackageManagerSeparator(argv: string[]): string[] { - return argv[0] === "--" ? argv.slice(1) : argv; -} - class WindowsSmoke extends SmokeRunController { private auth: ProviderAuth; private agentTimeoutSeconds = readPositiveIntEnv( diff --git a/scripts/e2e/telegram-user-crabbox-proof.ts b/scripts/e2e/telegram-user-crabbox-proof.ts index a180d80d8de2..4d2bde682917 100644 --- a/scripts/e2e/telegram-user-crabbox-proof.ts +++ b/scripts/e2e/telegram-user-crabbox-proof.ts @@ -15,7 +15,7 @@ import { fileURLToPath } from "node:url"; import { clampTimerTimeoutMs } from "@openclaw/normalization-core/number-coercion"; import { sliceUtf16Safe } from "@openclaw/normalization-core/utf16-slice"; import { parseStrictBooleanArg } from "../lib/arg-utils.mts"; -import { coerceErrorMessage } from "../lib/error-format.mts"; +import { coerceErrorMessage, toStringifiedError } from "../lib/error-format.mts"; import { sleep } from "../lib/sleep.mjs"; import { resolveWindowsTaskkillPath } from "../lib/windows-taskkill.mjs"; import { createPnpmRunnerSpawnSpec } from "../pnpm-runner.mts"; @@ -965,8 +965,7 @@ export function runCommand(params: { timeoutKillGraceMs, }).then( () => reject(error), - (cleanupError: unknown) => - reject(cleanupError instanceof Error ? cleanupError : new Error(String(cleanupError))), + (cleanupError: unknown) => reject(toStringifiedError(cleanupError)), ); return; } @@ -2070,12 +2069,12 @@ function sshArgs(inspect: CrabboxInspect, sshPort = inspect.sshPort?.trim() || " } function isTransientSshFailure(error: unknown) { - const message = error instanceof Error ? error.message : String(error); + const message = coerceErrorMessage(error); return /Connection (?:closed|reset)|Operation timed out|Connection timed out/u.test(message); } function isSshConnectionFailure(error: unknown) { - const message = error instanceof Error ? error.message : String(error); + const message = coerceErrorMessage(error); const code = error && typeof error === "object" && "code" in error ? error.code : undefined; return ( code === "ETIMEDOUT" || @@ -3177,7 +3176,7 @@ async function finishSession(root: string, opts: Options, outputDir: string) { } desktopSessionTerminationAttempted = true; await terminateRemoteDesktopSession(root, session.crabbox.inspect).catch((error: unknown) => { - summary.desktopSessionTerminateError = error instanceof Error ? error.message : String(error); + summary.desktopSessionTerminateError = coerceErrorMessage(error); }); }; try { @@ -3254,37 +3253,37 @@ async function finishSession(root: string, opts: Options, outputDir: string) { await stopLocalSutDaemon(session.localSut); sutQuiesced = true; } catch (error) { - summary.sutStopError = error instanceof Error ? error.message : String(error); + summary.sutStopError = coerceErrorMessage(error); summary.status = "fail"; } if (sutQuiesced) { try { preserveLocalSutRuntimeArtifacts(session.localSut, session.outputDir); } catch (error) { - summary.runtimeArtifactError = error instanceof Error ? error.message : String(error); + summary.runtimeArtifactError = coerceErrorMessage(error); summary.status = "fail"; } } try { destroyLocalSutRuntime(session.localSut); } catch (error) { - summary.sutDestroyError = error instanceof Error ? error.message : String(error); + summary.sutDestroyError = coerceErrorMessage(error); summary.status = "fail"; } if (session.localSut.funnelBridge) { await stopTailscaleFunnelBridge(root, session.localSut.funnelBridge).catch( (error: unknown) => { - summary.funnelResetError = error instanceof Error ? error.message : String(error); + summary.funnelResetError = coerceErrorMessage(error); }, ); } await terminateDesktopSession(); await releaseCredential(root, opts, session.credential.leaseFile).catch((error: unknown) => { - summary.credentialReleaseError = error instanceof Error ? error.message : String(error); + summary.credentialReleaseError = coerceErrorMessage(error); }); if (session.crabbox.createdLease && !opts.keepBox) { await stopCrabbox(root, opts, session.crabbox.id).catch((error: unknown) => { - summary.crabboxStopError = error instanceof Error ? error.message : String(error); + summary.crabboxStopError = coerceErrorMessage(error); }); } if (opts.keepBox) { @@ -3613,12 +3612,12 @@ async function main() { killTree(localSut?.mock); if (credential) { await releaseCredential(root, opts, credential.leaseFile).catch((error: unknown) => { - summary.credentialReleaseError = error instanceof Error ? error.message : String(error); + summary.credentialReleaseError = coerceErrorMessage(error); }); } if (leaseId && createdLease && !opts.keepBox) { await stopCrabbox(root, opts, leaseId).catch((error: unknown) => { - summary.crabboxStopError = error instanceof Error ? error.message : String(error); + summary.crabboxStopError = coerceErrorMessage(error); }); } if (opts.keepBox && leaseId) { @@ -3685,7 +3684,7 @@ function isMainModule(): boolean { if (isMainModule()) { main().catch((error: unknown) => { - console.error(error instanceof Error ? error.message : String(error)); + console.error(coerceErrorMessage(error)); process.exit(1); }); } diff --git a/scripts/github/dependency-guard.mjs b/scripts/github/dependency-guard.mjs index 07874c381138..18fcfa6c88c2 100644 --- a/scripts/github/dependency-guard.mjs +++ b/scripts/github/dependency-guard.mjs @@ -12,6 +12,7 @@ import { createIssueMutationHelpers, guardTrustedActorCandidates, isCommentNewerThan, + normalizeGuardLoginSet, readBoundedGitHubErrorText, readBoundedGitHubJson, } from "./guard-shared.mjs"; @@ -264,21 +265,11 @@ export function isDependencyGuardTrustedForHead(comment, currentHeadSha) { } export function securityApproverSet(value) { - return new Set( - String(value ?? "") - .split(/[\s,]+/u) - .map((login) => login.trim().toLowerCase()) - .filter(Boolean), - ); + return normalizeGuardLoginSet(value); } export function dependencyGuardCommentAuthors(value) { - return new Set( - String(value ?? "github-actions[bot]") - .split(/[\s,]+/u) - .map((login) => login.trim().toLowerCase()) - .filter(Boolean), - ); + return normalizeGuardLoginSet(value, "github-actions[bot]"); } export function isDependencyGuardMarkerComment(comment, marker, trustedAuthors) { diff --git a/scripts/github/guard-shared.mjs b/scripts/github/guard-shared.mjs index 1a21c85c9c90..d52961ebc46e 100644 --- a/scripts/github/guard-shared.mjs +++ b/scripts/github/guard-shared.mjs @@ -8,6 +8,19 @@ export const GITHUB_API_REQUEST_TIMEOUT_MS = 30_000; const githubApiRetryStatuses = new Set([502, 503, 504]); const githubApiRetryDelaysMs = [1_000, 2_000, 4_000]; +/** + * @param {string | null | undefined} value + * @param {string} [fallback] + */ +export function normalizeGuardLoginSet(value, fallback = "") { + return new Set( + (value ?? fallback) + .split(/[\s,]+/u) + .map((login) => login.trim().toLowerCase()) + .filter(Boolean), + ); +} + export function guardTrustedActorCandidates({ pullRequest, event, currentHeadSha }) { const eventHeadSha = event?.pull_request?.head?.sha; const eventAfterSha = event?.after; diff --git a/scripts/github/security-sensitive-guard.mjs b/scripts/github/security-sensitive-guard.mjs index eac903458120..6f193d53102a 100644 --- a/scripts/github/security-sensitive-guard.mjs +++ b/scripts/github/security-sensitive-guard.mjs @@ -13,6 +13,7 @@ import { guardCommentHeadSha, guardTrustedActorCandidates, isCommentNewerThan, + normalizeGuardLoginSet, readBoundedGitHubErrorText, readBoundedGitHubJson, } from "./guard-shared.mjs"; @@ -170,21 +171,11 @@ export function isSecuritySensitiveGuardTrustedForHead(comment, currentHeadSha) } export function securityApproverSet(value) { - return new Set( - String(value ?? "") - .split(/[\s,]+/u) - .map((login) => login.trim().toLowerCase()) - .filter(Boolean), - ); + return normalizeGuardLoginSet(value); } export function securitySensitiveGuardCommentAuthors(value) { - return new Set( - String(value ?? "github-actions[bot]") - .split(/[\s,]+/u) - .map((login) => login.trim().toLowerCase()) - .filter(Boolean), - ); + return normalizeGuardLoginSet(value, "github-actions[bot]"); } export function isSecuritySensitiveGuardMarkerComment(comment, trustedAuthors) { diff --git a/scripts/lib/arg-utils.runtime.d.mts b/scripts/lib/arg-utils.runtime.d.mts index 9b2390430fe0..d76846ab3d1c 100644 --- a/scripts/lib/arg-utils.runtime.d.mts +++ b/scripts/lib/arg-utils.runtime.d.mts @@ -8,13 +8,13 @@ type StringOptions = { }; type ConsumedFlag> = { - flag?: string; + flag: string; nextIndex: number; repeatable?: boolean; apply(target: T): void; }; -type FlagSpec> = { +export type FlagSpec> = { consume(argv: readonly string[], index: number, args: T): ConsumedFlag | null; }; @@ -40,6 +40,8 @@ export function classifyBoundedUnsignedDecimal( max: number, ): BoundedUnsignedDecimalResult; export function parsePermissiveBooleanToken(value: unknown): boolean | undefined; +export function isOpenEndedTruthyValue(value: string | undefined): boolean; +export function isStrictAffirmativeValue(value: string | undefined): boolean; export function stringFlag>( flag: string, key: string, diff --git a/scripts/lib/arg-utils.runtime.mjs b/scripts/lib/arg-utils.runtime.mjs index c0c00b55c0d9..2fec176519d4 100644 --- a/scripts/lib/arg-utils.runtime.mjs +++ b/scripts/lib/arg-utils.runtime.mjs @@ -6,7 +6,7 @@ /** * @template {Record} T * @typedef {{ - * flag?: string, + * flag: string, * nextIndex: number, * repeatable?: boolean, * apply: ApplyFlag, @@ -224,6 +224,23 @@ export function parsePermissiveBooleanToken(value) { } return PERMISSIVE_BOOLEAN_FALSE_TOKENS.has(normalized) ? false : undefined; } +const OPEN_ENDED_FALSE_TOKENS = new Set(["", "0", "false", "no"]); +/** + * Treat every non-empty token except the explicit false language as enabled. + * @param {string | undefined} value + */ +export function isOpenEndedTruthyValue(value) { + return !OPEN_ENDED_FALSE_TOKENS.has((value ?? "").trim().toLowerCase()); +} + +const STRICT_AFFIRMATIVE_TOKENS = new Set(["1", "true", "yes"]); +/** + * Accept only the narrow affirmative token language used by script environment flags. + * @param {string | undefined} value + */ +export function isStrictAffirmativeValue(value) { + return STRICT_AFFIRMATIVE_TOKENS.has(value?.trim().toLowerCase() ?? ""); +} /** * @param {string} raw * @param {string} flag @@ -385,9 +402,6 @@ export function parseFlagArgs(argv, args, specs, options = {}) { if (!option) { continue; } - if (typeof option.flag !== "string" || !option.flag) { - failFlagParse("parseFlagArgs specs must declare a flag for consumed options"); - } if (option.repeatable !== true) { if (seenFlags.has(option.flag)) { failFlagParse( diff --git a/scripts/lib/build-identity.mts b/scripts/lib/build-identity.mts new file mode 100644 index 000000000000..133f47cdf438 --- /dev/null +++ b/scripts/lib/build-identity.mts @@ -0,0 +1,30 @@ +const FULL_GIT_COMMIT_RE = /^[0-9a-f]{40}$/iu; + +type BuildIdentityOptions = { + commitLabel: string; + env?: NodeJS.ProcessEnv; + now?: () => Date; + readGitCommit: () => string | null; +}; + +/** Pins one timestamp and source commit across every child in a build lifecycle. */ +export function resolveBuildIdentityEnvironment({ + commitLabel, + env = process.env, + now = () => new Date(), + readGitCommit, +}: BuildIdentityOptions): NodeJS.ProcessEnv { + const explicitTimestamp = env.OPENCLAW_BUILD_TIMESTAMP?.trim(); + const explicitCommit = env.GIT_COMMIT?.trim() || env.GIT_SHA?.trim(); + const checkedOutCommit = explicitCommit ? null : readGitCommit()?.trim(); + // GITHUB_SHA names the workflow invocation and can differ from a checked-out tag. + const commit = explicitCommit || checkedOutCommit || env.GITHUB_SHA?.trim(); + if (commit && !FULL_GIT_COMMIT_RE.test(commit)) { + throw new Error(`${commitLabel} must be a full 40-character hexadecimal SHA`); + } + return { + ...env, + OPENCLAW_BUILD_TIMESTAMP: explicitTimestamp || now().toISOString(), + ...(commit ? { GIT_COMMIT: commit.toLowerCase() } : {}), + }; +} diff --git a/scripts/lib/ci-node-test-plan.mts b/scripts/lib/ci-node-test-plan.mts index 7a1c71f912d8..0bc023da892c 100644 --- a/scripts/lib/ci-node-test-plan.mts +++ b/scripts/lib/ci-node-test-plan.mts @@ -1047,7 +1047,6 @@ function resolveInfraShardName(file: string): string { name.startsWith("fixed-window") || name.startsWith("format-time/") || name.startsWith("http-body") || - name.startsWith("parse-finite-number") || name.startsWith("plain-object") || name.startsWith("prototype-keys") || name.startsWith("retry") || diff --git a/scripts/lib/numeric-options.mjs b/scripts/lib/numeric-options.mjs index 48810b7c6332..265518b47e4b 100644 --- a/scripts/lib/numeric-options.mjs +++ b/scripts/lib/numeric-options.mjs @@ -58,6 +58,24 @@ export function parseNonNegativeInt(raw, label) { return value; } +/** + * Parse a safe non-negative integer written in canonical decimal notation. + * @param {unknown} raw + * @param {string} label + * @returns {number} + */ +export function parseStrictNonNegativeDecimal(raw, label) { + const text = String(raw).trim(); + if (!/^(0|[1-9]\d*)$/u.test(text)) { + throw new Error(`${label} must be a non-negative integer`); + } + const value = Number(text); + if (!Number.isSafeInteger(value)) { + throw new Error(`${label} must be a safe integer`); + } + return value; +} + /** * Parse a finite positive number option. * @param {string | number} raw diff --git a/scripts/lib/plugin-sdk-entries.mts b/scripts/lib/plugin-sdk-entries.mts index 2f02f32a35ce..173f3137c7c8 100644 --- a/scripts/lib/plugin-sdk-entries.mts +++ b/scripts/lib/plugin-sdk-entries.mts @@ -110,6 +110,16 @@ export const deprecatedBarrelPluginSdkEntrypoints = pluginSdkSubpaths.filter((en deprecatedBarrelPluginSdkSubpathList.includes(entry), ); +/** Supported SDK facades backed by bundled plugins until generic contracts replace them. */ +export const supportedBundledFacadeSdkEntrypoints = [ + "discord", + "matrix", + "telegram-account", +] as const; + +/** Plugin-owned surfaces intentionally public and documented for third-party plugins. */ +export const publicPluginOwnedSdkEntrypoints = ["memory-core-host-engine-foundation"] as const; + /** * Build tsdown entry source paths for plugin SDK entrypoints. * @internal Shared repository-script contract. diff --git a/scripts/lib/release-beta-verifier.ts b/scripts/lib/release-beta-verifier.ts index 0114aa7cf2b9..d3ddcc9d270d 100644 --- a/scripts/lib/release-beta-verifier.ts +++ b/scripts/lib/release-beta-verifier.ts @@ -5,6 +5,7 @@ import { mkdirSync, readFileSync, writeFileSync } from "node:fs"; import { dirname, resolve } from "node:path"; import { fileURLToPath } from "node:url"; import { isRecord as isJsonRecord } from "../../packages/normalization-core/src/record-coerce.ts"; +import { normalizeOptionalString } from "../../packages/normalization-core/src/string-coerce.ts"; import { readPublicationArtifactArchive, sha256Digest } from "./actions-artifact-archive.mjs"; import { readBoundedResponseText } from "./bounded-response.mjs"; import { collectClawHubPublishablePluginPackages } from "./plugin-clawhub-release.ts"; @@ -93,16 +94,12 @@ const TRUSTED_TOOLING_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), ". const NPM_VIEW_ATTEMPTS = 30; const NPM_VIEW_RETRY_MAX_DELAY_MS = 10_000; -function normalizeOptionalText(value: unknown): string | undefined { - return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined; -} - function compareCodeUnits(left: string, right: string): number { return left < right ? -1 : left > right ? 1 : 0; } function requireString(value: unknown, label: string): string { - const stringValue = normalizeOptionalText(value); + const stringValue = normalizeOptionalString(value); if (stringValue === undefined) { throw new Error(`${label} is missing.`); } @@ -197,10 +194,10 @@ export function parseNpmViewFields(raw: string, distTag: string): NpmViewFields const parsed = parseJson(raw, "npm view"); if (Array.isArray(parsed)) { return { - version: normalizeOptionalText(parsed[0]), - distTagVersion: normalizeOptionalText(parsed[1]), - integrity: normalizeOptionalText(parsed[2]), - tarball: normalizeOptionalText(parsed[3]), + version: normalizeOptionalString(parsed[0]), + distTagVersion: normalizeOptionalString(parsed[1]), + integrity: normalizeOptionalString(parsed[2]), + tarball: normalizeOptionalString(parsed[3]), }; } if (!isJsonRecord(parsed)) { @@ -209,13 +206,14 @@ export function parseNpmViewFields(raw: string, distTag: string): NpmViewFields const distTags = isJsonRecord(parsed["dist-tags"]) ? parsed["dist-tags"] : undefined; const dist = isJsonRecord(parsed.dist) ? parsed.dist : undefined; return { - version: normalizeOptionalText(parsed.version), + version: normalizeOptionalString(parsed.version), distTagVersion: - normalizeOptionalText(parsed[`dist-tags.${distTag}`]) ?? - normalizeOptionalText(distTags?.[distTag]), + normalizeOptionalString(parsed[`dist-tags.${distTag}`]) ?? + normalizeOptionalString(distTags?.[distTag]), integrity: - normalizeOptionalText(parsed["dist.integrity"]) ?? normalizeOptionalText(dist?.integrity), - tarball: normalizeOptionalText(parsed["dist.tarball"]) ?? normalizeOptionalText(dist?.tarball), + normalizeOptionalString(parsed["dist.integrity"]) ?? normalizeOptionalString(dist?.integrity), + tarball: + normalizeOptionalString(parsed["dist.tarball"]) ?? normalizeOptionalString(dist?.tarball), }; } @@ -612,19 +610,19 @@ function verifyWorkflowRun(params: { if (!isJsonRecord(run)) { throw new Error(`${params.label}: workflow run returned an unsupported JSON shape.`); } - const workflowName = normalizeOptionalText(run.workflowName); + const workflowName = normalizeOptionalString(run.workflowName); if (workflowName !== params.expectedWorkflowName) { throw new Error( `${params.label}: run ${params.id} workflow is ${workflowName ?? ""}, expected ${params.expectedWorkflowName}.`, ); } - const event = normalizeOptionalText(run.event); + const event = normalizeOptionalString(run.event); if (event !== "workflow_dispatch") { throw new Error( `${params.label}: run ${params.id} event is ${event ?? ""}, expected workflow_dispatch.`, ); } - const headBranch = normalizeOptionalText(run.headBranch); + const headBranch = normalizeOptionalString(run.headBranch); const allowedHeadBranches = params.allowedHeadBranches ?? (params.expectedHeadBranch !== undefined ? [params.expectedHeadBranch] : []); @@ -633,11 +631,11 @@ function verifyWorkflowRun(params: { `${params.label}: run ${params.id} branch is ${headBranch ?? ""}, expected ${allowedHeadBranches.join(" or ")}.`, ); } - const status = normalizeOptionalText(run.status); - const conclusion = normalizeOptionalText(run.conclusion); + const status = normalizeOptionalString(run.status); + const conclusion = normalizeOptionalString(run.conclusion); const jobs = Array.isArray(run.jobs) ? run.jobs.filter(isJsonRecord) : []; const failedJobs = jobs.filter((job) => { - const jobConclusion = normalizeOptionalText(job.conclusion); + const jobConclusion = normalizeOptionalString(job.conclusion); return ( jobConclusion !== undefined && jobConclusion !== "success" && jobConclusion !== "skipped" ); @@ -650,14 +648,14 @@ function verifyWorkflowRun(params: { } if (status !== "completed" || conclusion !== "success" || failedJobs.length > 0) { const failedNames = failedJobs - .map((job) => normalizeOptionalText(job.name) ?? "") + .map((job) => normalizeOptionalString(job.name) ?? "") .join(", "); throw new Error( `${params.label}: run ${params.id} is ${status ?? ""}/${conclusion ?? ""}${failedNames ? `; failed jobs: ${failedNames}` : ""}.`, ); } - const createdAt = normalizeOptionalText(run.createdAt); - const updatedAt = normalizeOptionalText(run.updatedAt); + const createdAt = normalizeOptionalString(run.createdAt); + const updatedAt = normalizeOptionalString(run.updatedAt); const createdMs = createdAt === undefined ? Number.NaN : Date.parse(createdAt); const updatedMs = updatedAt === undefined ? Number.NaN : Date.parse(updatedAt); const durationSeconds = @@ -667,7 +665,7 @@ function verifyWorkflowRun(params: { return { id: params.id, label: params.label, - url: normalizeOptionalText(run.url), + url: normalizeOptionalString(run.url), durationSeconds, }; } @@ -676,7 +674,7 @@ function requirePositiveIntegerString(value: unknown, label: string): string { if (typeof value === "number" && Number.isSafeInteger(value) && value > 0) { return String(value); } - const stringValue = normalizeOptionalText(value); + const stringValue = normalizeOptionalString(value); if (stringValue === undefined || !POSITIVE_INTEGER_PATTERN.test(stringValue)) { throw new Error(`${label} must be a positive integer.`); } @@ -1055,14 +1053,14 @@ export function validateClawHubBootstrapEvidence(params: { ); } - const createdAt = normalizeOptionalText(runBinding.run.created_at); - const updatedAt = normalizeOptionalText(runBinding.run.updated_at); + const createdAt = normalizeOptionalString(runBinding.run.created_at); + const updatedAt = normalizeOptionalString(runBinding.run.updated_at); const createdMs = createdAt === undefined ? Number.NaN : Date.parse(createdAt); const updatedMs = updatedAt === undefined ? Number.NaN : Date.parse(updatedAt); return { id: runId, label: "Plugin ClawHub New", - url: normalizeOptionalText(runBinding.run.html_url), + url: normalizeOptionalString(runBinding.run.html_url), durationSeconds: Number.isFinite(createdMs) && Number.isFinite(updatedMs) ? Math.max(0, Math.round((updatedMs - createdMs) / 1000)) diff --git a/scripts/lib/tsgo-sparse-guard.mts b/scripts/lib/tsgo-sparse-guard.mts index 3a0588aa48b6..0edb6e0368e3 100644 --- a/scripts/lib/tsgo-sparse-guard.mts +++ b/scripts/lib/tsgo-sparse-guard.mts @@ -41,7 +41,7 @@ const CORE_PROD_REQUIRED_PATHS = [ }, { path: "scripts/lib/plugin-sdk-entrypoints.json", - whenPresent: "src/plugin-sdk/entrypoints.ts", + whenPresent: "scripts/lib/plugin-sdk-entries.mts", }, ]; diff --git a/scripts/ocm-npm-workspace-deps.mts b/scripts/ocm-npm-workspace-deps.mts index 3261822b121d..57440de332df 100755 --- a/scripts/ocm-npm-workspace-deps.mts +++ b/scripts/ocm-npm-workspace-deps.mts @@ -5,6 +5,7 @@ import { mkdtempSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSyn import { tmpdir } from "node:os"; import { delimiter, join, resolve } from "node:path"; import { fileURLToPath, pathToFileURL } from "node:url"; +import { resolveBuildIdentityEnvironment } from "./lib/build-identity.mts"; const WORKSPACE_DIRS_ENV = "OPENCLAW_OCM_WORKSPACE_DEPENDENCY_DIRS"; const REAL_NPM_ENV = "OPENCLAW_OCM_REAL_NPM_BIN"; @@ -12,7 +13,6 @@ const INTERNAL_NPM_BIN_ENV = "OCM_INTERNAL_NPM_BIN"; const ALLOW_UNRELEASED_CHANGELOG_ENV = "OPENCLAW_PREPACK_ALLOW_UNRELEASED_CHANGELOG"; const RUNTIME_BUILD_PROFILE_ENV = "OPENCLAW_OCM_RUNTIME_BUILD_PROFILE"; const supportedRuntimeBuildProfiles = new Set(["sourcePerformance"]); -const fullGitCommitPattern = /^[0-9a-f]{40}$/iu; type WorkspacePackage = { name: string; version: string; tarball: string }; type WorkspacePackageSource = Omit & { dir: string }; @@ -118,18 +118,12 @@ export function resolveRuntimePackEnvironment( return result.status === 0 ? result.stdout.trim() : null; }, ) { - const explicitTimestamp = env.OPENCLAW_BUILD_TIMESTAMP?.trim(); - const explicitCommit = env.GIT_COMMIT?.trim() || env.GIT_SHA?.trim(); - const checkedOutCommit = explicitCommit ? null : readGitCommit()?.trim(); - const commit = explicitCommit || checkedOutCommit || env.GITHUB_SHA?.trim(); - if (commit && !fullGitCommitPattern.test(commit)) { - throw new Error("runtime pack commit must be a full 40-character hexadecimal SHA"); - } - return { - ...env, - OPENCLAW_BUILD_TIMESTAMP: explicitTimestamp || now().toISOString(), - ...(commit ? { GIT_COMMIT: commit.toLowerCase() } : {}), - }; + return resolveBuildIdentityEnvironment({ + commitLabel: "runtime pack commit", + env, + now, + readGitCommit, + }); } function runTar(args: string[]) { diff --git a/scripts/openclaw-prepack.ts b/scripts/openclaw-prepack.ts index bf9b74dd5c8d..00f86670b2af 100644 --- a/scripts/openclaw-prepack.ts +++ b/scripts/openclaw-prepack.ts @@ -6,6 +6,7 @@ import { existsSync, readFileSync, readdirSync } from "node:fs"; import { basename, delimiter, join } from "node:path"; import { pathToFileURL } from "node:url"; import { formatErrorMessage } from "../src/infra/errors.ts"; +import { resolveBuildIdentityEnvironment } from "./lib/build-identity.mts"; import { readPositiveEnvInt } from "./lib/numeric-options.mjs"; import { writePackageDistInventoryForPublish } from "./lib/package-dist-inventory.ts"; import { restorePrepackArtifacts } from "./openclaw-postpack.mjs"; @@ -13,7 +14,6 @@ import { preparePackageChangelog } from "./package-changelog.mjs"; import { preparePackageDocsMap } from "./package-docs-map.mjs"; import { preparePackageManifest } from "./package-manifest.mjs"; import { createPnpmRunnerSpawnSpec } from "./pnpm-runner.mts"; -const FULL_GIT_COMMIT_RE = /^[0-9a-f]{40}$/iu; const requiredPreparedPathGroups = [ ["dist/index.js", "dist/index.mjs"], ["dist/control-ui/index.html"], @@ -249,22 +249,12 @@ export function resolvePrepackBuildEnvironment( return result.status === 0 ? result.stdout.trim() : null; }, ): NodeJS.ProcessEnv { - const explicitTimestamp = env.OPENCLAW_BUILD_TIMESTAMP?.trim(); - const explicitCommit = env.GIT_COMMIT?.trim() || env.GIT_SHA?.trim(); - const checkedOutCommit = explicitCommit ? null : readGitCommit()?.trim(); - // GITHUB_SHA names the workflow invocation and can differ from a checked-out tag. - const commit = explicitCommit || checkedOutCommit || env.GITHUB_SHA?.trim(); - if (commit && !FULL_GIT_COMMIT_RE.test(commit)) { - throw new Error("build commit must be a full 40-character hexadecimal SHA"); - } - const buildEnv: NodeJS.ProcessEnv = { - ...env, - OPENCLAW_BUILD_TIMESTAMP: explicitTimestamp || now().toISOString(), - }; - if (commit) { - buildEnv.GIT_COMMIT = commit.toLowerCase(); - } - return buildEnv; + return resolveBuildIdentityEnvironment({ + commitLabel: "build commit", + env, + now, + readGitCommit, + }); } function runPnpm(args: string[], env: NodeJS.ProcessEnv): void { diff --git a/scripts/plugin-boundary-report.ts b/scripts/plugin-boundary-report.ts index 55f69d7ed46a..a344df5e419c 100644 --- a/scripts/plugin-boundary-report.ts +++ b/scripts/plugin-boundary-report.ts @@ -3,14 +3,13 @@ import { spawnSync } from "node:child_process"; import { existsSync, lstatSync, readdirSync, readFileSync } from "node:fs"; import { join, relative, resolve } from "node:path"; import { fileURLToPath } from "node:url"; +import { listPluginCompatRecords } from "../src/plugins/compat/registry.ts"; +import type { PluginCompatRecord } from "../src/plugins/compat/types.ts"; import { pluginSdkEntrypoints, publicPluginOwnedSdkEntrypoints, - reservedBundledPluginSdkEntrypoints, supportedBundledFacadeSdkEntrypoints, -} from "../src/plugin-sdk/entrypoints.ts"; -import { listPluginCompatRecords } from "../src/plugins/compat/registry.ts"; -import type { PluginCompatRecord } from "../src/plugins/compat/types.ts"; +} from "./lib/plugin-sdk-entries.mts"; const REPO_ROOT = process.cwd(); const SOURCE_ROOTS = ["src", "extensions", "packages", "scripts", "test", "docs"] as const; @@ -23,16 +22,11 @@ const SKIPPED_DIRS = new Set([ "node_modules", ]); const TEXT_FILE_PATTERN = /\.(?:[cm]?[jt]sx?|json|mdx?|ya?ml)$/u; -const PLUGIN_SDK_SPECIFIER_PATTERN = - /\b(?:from\s*["']|import\s*\(\s*["']|require\s*\(\s*["']|vi\.(?:mock|doMock)\s*\(\s*["'])(openclaw\/plugin-sdk\/([a-z0-9][a-z0-9-]*))["']/g; - type CliOptions = { json: boolean; summary: boolean; owner?: string; - failOnCrossOwner: boolean; failOnEligibleCompat: boolean; - failOnUnclassifiedUnusedReserved: boolean; help: boolean; }; @@ -73,15 +67,6 @@ type WorkspaceTextFile = { source: string; }; -type ReservedSdkImport = { - file: string; - specifier: string; - subpath: string; - owner?: string; - consumerOwner?: string; - relation: "owner" | "cross-owner" | "workspace"; -}; - type BoundaryReport = { generatedAt: string; compat: { @@ -94,12 +79,8 @@ type BoundaryReport = { }; pluginSdk: { entrypointCount: number; - reservedCount: number; supportedBundledFacadeCount: number; publicPluginOwnedCount: number; - reservedImports: ReservedSdkImport[]; - crossOwnerReservedImports: ReservedSdkImport[]; - unusedReservedSubpaths: string[]; }; memoryHostSdk: { privatePackage: boolean; @@ -123,14 +104,8 @@ type BoundaryReportSummary = { }; pluginSdk: { entrypointCount: number; - reservedCount: number; supportedBundledFacadeCount: number; publicPluginOwnedCount: number; - reservedImportCount: number; - crossOwnerReservedImportCount: number; - unusedReservedCount: number; - unusedReservedSubpaths: string[]; - crossOwnerReservedImports: ReservedSdkImport[]; }; memoryHostSdk: { privatePackage: boolean; @@ -276,9 +251,7 @@ function parseArgs(args: readonly string[]): CliOptions { const options: CliOptions = { json: false, summary: false, - failOnCrossOwner: false, failOnEligibleCompat: false, - failOnUnclassifiedUnusedReserved: false, help: false, }; for (let index = 0; index < args.length; index += 1) { @@ -294,12 +267,8 @@ function parseArgs(args: readonly string[]): CliOptions { } options.owner = owner; index += 1; - } else if (arg === "--fail-on-cross-owner") { - options.failOnCrossOwner = true; } else if (arg === "--fail-on-eligible-compat") { options.failOnEligibleCompat = true; - } else if (arg === "--fail-on-unclassified-unused-reserved") { - options.failOnUnclassifiedUnusedReserved = true; } else if (arg === "--help" || arg === "-h") { options.help = true; } else { @@ -316,30 +285,11 @@ function renderHelp(): string { "Options:", " --summary Print compact counts only.", " --json Emit JSON instead of text.", - " --owner Filter compat/imports/reserved shims by owner id.", - " --fail-on-cross-owner Exit non-zero on cross-owner reserved SDK imports.", + " --owner Filter compatibility records by owner id.", " --fail-on-eligible-compat Exit non-zero when deprecated compat is due for removal.", - " --fail-on-unclassified-unused-reserved Exit non-zero on unused reserved SDK shims.", ].join("\n"); } -function collectBundledPluginIds(): string[] { - return readdirSync(resolve(REPO_ROOT, "extensions"), { withFileTypes: true }) - .filter((entry) => entry.isDirectory()) - .map((entry) => entry.name) - .toSorted((left, right) => right.length - left.length || left.localeCompare(right)); -} - -function resolvePluginOwner(entrypoint: string, pluginIds: readonly string[]): string | undefined { - return pluginIds.find( - (pluginId) => entrypoint === pluginId || entrypoint.startsWith(`${pluginId}-`), - ); -} - -function resolveConsumerOwner(file: string): string | undefined { - return /^extensions\/([^/]+)\//u.exec(file)?.[1]; -} - function extractCompatTokensFromValues(values: readonly (string | undefined)[]): string[] { const tokens = new Set(); for (const value of values) { @@ -481,32 +431,6 @@ function collectRemovalPendingDebt( ); } -function collectReservedSdkImports(files: readonly WorkspaceTextFile[]): ReservedSdkImport[] { - const reserved = new Set(reservedBundledPluginSdkEntrypoints); - const pluginIds = collectBundledPluginIds(); - const imports: ReservedSdkImport[] = []; - for (const { relativeFile, source } of files) { - for (const match of source.matchAll(PLUGIN_SDK_SPECIFIER_PATTERN)) { - const specifier = match[1]; - const subpath = match[2]; - if (!specifier || !subpath || !reserved.has(subpath)) { - continue; - } - const owner = resolvePluginOwner(subpath, pluginIds); - const consumerOwner = resolveConsumerOwner(relativeFile); - const relation = - owner && consumerOwner ? (owner === consumerOwner ? "owner" : "cross-owner") : "workspace"; - imports.push({ file: relativeFile, specifier, subpath, owner, consumerOwner, relation }); - } - } - return imports.toSorted( - (left, right) => - left.subpath.localeCompare(right.subpath) || - left.file.localeCompare(right.file) || - left.specifier.localeCompare(right.specifier), - ); -} - function collectMemoryHostBoundary( files: readonly WorkspaceTextFile[], ): BoundaryReport["memoryHostSdk"] { @@ -595,14 +519,8 @@ function buildSummary(report: BoundaryReport, owner?: string): BoundaryReportSum }, pluginSdk: { entrypointCount: report.pluginSdk.entrypointCount, - reservedCount: report.pluginSdk.reservedCount, supportedBundledFacadeCount: report.pluginSdk.supportedBundledFacadeCount, publicPluginOwnedCount: report.pluginSdk.publicPluginOwnedCount, - reservedImportCount: report.pluginSdk.reservedImports.length, - crossOwnerReservedImportCount: report.pluginSdk.crossOwnerReservedImports.length, - unusedReservedCount: report.pluginSdk.unusedReservedSubpaths.length, - unusedReservedSubpaths: report.pluginSdk.unusedReservedSubpaths, - crossOwnerReservedImports: report.pluginSdk.crossOwnerReservedImports, }, memoryHostSdk: { privatePackage: report.memoryHostSdk.privatePackage, @@ -618,25 +536,12 @@ function buildReport(options: Partial> = { const files = options.summary ? collectSummaryWorkspaceTextFileSources() : collectWorkspaceTextFileSources(); - const pluginIds = collectBundledPluginIds(); const compatRecords = collectCompatDebt(files, new Date(), { includeReferenceFiles: !options.summary, }).filter((record) => matchesOwner(options.owner, record.owner)); const removalPending = collectRemovalPendingDebt(files).filter((record) => matchesOwner(options.owner, record.owner), ); - const reservedImports = collectReservedSdkImports(files).filter( - (entry) => - matchesOwner(options.owner, entry.owner) || matchesOwner(options.owner, entry.consumerOwner), - ); - const usedReserved = new Set(reservedImports.map((entry) => entry.subpath)); - const unusedReservedSubpaths = (reservedBundledPluginSdkEntrypoints as readonly string[]) - .filter( - (subpath) => - !usedReserved.has(subpath) && - matchesOwner(options.owner, resolvePluginOwner(subpath, pluginIds)), - ) - .toSorted((a, b) => a.localeCompare(b)); return { generatedAt: new Date().toISOString(), compat: { @@ -649,14 +554,8 @@ function buildReport(options: Partial> = { }, pluginSdk: { entrypointCount: pluginSdkEntrypoints.length, - reservedCount: reservedBundledPluginSdkEntrypoints.length, supportedBundledFacadeCount: supportedBundledFacadeSdkEntrypoints.length, publicPluginOwnedCount: publicPluginOwnedSdkEntrypoints.length, - reservedImports, - crossOwnerReservedImports: reservedImports.filter( - (entry) => entry.relation === "cross-owner", - ), - unusedReservedSubpaths, }, memoryHostSdk: collectMemoryHostBoundary(files), }; @@ -675,17 +574,8 @@ function renderSummaryText(summary: BoundaryReportSummary): string { ); } lines.push( - `plugin-sdk entrypoints=${summary.pluginSdk.entrypointCount} reserved=${summary.pluginSdk.reservedCount}`, + `plugin-sdk entrypoints=${summary.pluginSdk.entrypointCount} supportedBundledFacade=${summary.pluginSdk.supportedBundledFacadeCount} publicPluginOwned=${summary.pluginSdk.publicPluginOwnedCount}`, ); - lines.push( - ` reservedImports=${summary.pluginSdk.reservedImportCount} crossOwnerReservedImports=${summary.pluginSdk.crossOwnerReservedImportCount} unusedReserved=${summary.pluginSdk.unusedReservedCount}`, - ); - for (const subpath of summary.pluginSdk.unusedReservedSubpaths) { - lines.push(` unused-reserved ${subpath}`); - } - for (const entry of summary.pluginSdk.crossOwnerReservedImports) { - lines.push(` cross-owner ${entry.file}: ${entry.specifier} owner=${entry.owner ?? "unknown"}`); - } lines.push( `memory-host-sdk implementation=${summary.memoryHostSdk.implementation} private=${summary.memoryHostSdk.privatePackage} exports=${summary.memoryHostSdk.exportedSubpathCount} sourceBridgeFiles=${summary.memoryHostSdk.sourceBridgeFileCount} coreReferenceFiles=${summary.memoryHostSdk.packageCoreReferenceFileCount}`, ); @@ -714,17 +604,8 @@ function renderText(report: BoundaryReport, owner?: string): string { } lines.push(""); lines.push( - `plugin-sdk entrypoints=${report.pluginSdk.entrypointCount} reserved=${report.pluginSdk.reservedCount} supportedBundledFacade=${report.pluginSdk.supportedBundledFacadeCount} publicPluginOwned=${report.pluginSdk.publicPluginOwnedCount}`, + `plugin-sdk entrypoints=${report.pluginSdk.entrypointCount} supportedBundledFacade=${report.pluginSdk.supportedBundledFacadeCount} publicPluginOwned=${report.pluginSdk.publicPluginOwnedCount}`, ); - lines.push( - ` reservedImports=${report.pluginSdk.reservedImports.length} crossOwnerReservedImports=${report.pluginSdk.crossOwnerReservedImports.length} unusedReserved=${report.pluginSdk.unusedReservedSubpaths.length}`, - ); - for (const subpath of report.pluginSdk.unusedReservedSubpaths) { - lines.push(` unused-reserved ${subpath}`); - } - for (const entry of report.pluginSdk.crossOwnerReservedImports) { - lines.push(` cross-owner ${entry.file}: ${entry.specifier} owner=${entry.owner ?? "unknown"}`); - } lines.push(""); lines.push( `memory-host-sdk implementation=${resolveMemoryHostImplementation(report.memoryHostSdk)} private=${report.memoryHostSdk.privatePackage} exports=${report.memoryHostSdk.exportedSubpaths.length} sourceBridgeFiles=${report.memoryHostSdk.sourceBridgeFiles.length} coreReferenceFiles=${report.memoryHostSdk.packageCoreReferenceFiles.length}`, @@ -734,19 +615,6 @@ function renderText(report: BoundaryReport, owner?: string): string { function collectFailures(report: BoundaryReport, options: CliOptions): string[] { const failures: string[] = []; - if (options.failOnCrossOwner && report.pluginSdk.crossOwnerReservedImports.length > 0) { - failures.push( - `${report.pluginSdk.crossOwnerReservedImports.length} cross-owner reserved SDK import(s) found`, - ); - } - if ( - options.failOnUnclassifiedUnusedReserved && - report.pluginSdk.unusedReservedSubpaths.length > 0 - ) { - failures.push( - `${report.pluginSdk.unusedReservedSubpaths.length} unused reserved SDK subpath(s) found`, - ); - } if (options.failOnEligibleCompat && report.compat.eligibleForRemovalCount > 0) { failures.push( `${report.compat.eligibleForRemovalCount} compatibility record(s) are due for removal`, diff --git a/scripts/plugin-sdk-surface-report.mts b/scripts/plugin-sdk-surface-report.mts index 1936959291f7..d27f7f199981 100644 --- a/scripts/plugin-sdk-surface-report.mts +++ b/scripts/plugin-sdk-surface-report.mts @@ -365,7 +365,8 @@ export function readPluginSdkSurfaceBudgets(env: NodeJS.ProcessEnv = process.env // -1: infra-runtime now names its error exports explicitly. // -1: infra-runtime excludes the internal system-event receipt API. // -2: text-runtime names record and string coercion compatibility exports explicitly. - 76, + // -1: infra-runtime re-exports number coercion directly from its canonical owner. + 75, env, ), }; diff --git a/scripts/qa-lab-up.ts b/scripts/qa-lab-up.ts index a26118baad8b..f4655b60cc1e 100644 --- a/scripts/qa-lab-up.ts +++ b/scripts/qa-lab-up.ts @@ -2,7 +2,7 @@ import { resolve } from "node:path"; import { fileURLToPath } from "node:url"; import { parseArgs } from "node:util"; -import { parseStrictPositiveInteger } from "../src/infra/parse-finite-number.js"; +import { parseStrictPositiveInteger } from "@openclaw/normalization-core/number-coercion"; const options = { help: { type: "boolean", short: "h" }, diff --git a/scripts/report-test-temp-creations.mts b/scripts/report-test-temp-creations.mts index 45c4b02f9d34..f44bdb4f0633 100644 --- a/scripts/report-test-temp-creations.mts +++ b/scripts/report-test-temp-creations.mts @@ -5,7 +5,12 @@ import fs from "node:fs"; import path from "node:path"; import ts from "typescript"; import { isChangedLaneTestPath } from "./changed-lanes.mts"; -import { booleanFlag, parseFlagArgs, stringFlag } from "./lib/arg-utils.mts"; +import { + booleanFlag, + isOpenEndedTruthyValue, + parseFlagArgs, + stringFlag, +} from "./lib/arg-utils.mts"; import { runAsScript } from "./lib/ts-guard-utils.mts"; type AddedLine = { @@ -100,11 +105,6 @@ function shouldInspectManualHelperUsage(filePath: string): boolean { return normalizedPath !== TEMP_DIR_HELPER_TEST_PATH && shouldInspectFile(normalizedPath); } -function isTruthyEnvFlag(value: string | undefined): boolean { - const normalized = value?.trim().toLowerCase() ?? ""; - return normalized !== "" && normalized !== "0" && normalized !== "false" && normalized !== "no"; -} - function escapeGithubCommandValue(value: unknown): string { return String(value).replaceAll("%", "%25").replaceAll("\r", "%0D").replaceAll("\n", "%0A"); } @@ -516,7 +516,7 @@ async function main(argv?: string[], io?: ScriptIo): Promise<0 | 1> { stdout.write(`${JSON.stringify(findings, null, 2)}\n`); } else if (findings.length === 0) { stderr.write("No new test temp-directory migration warnings found.\n"); - } else if (isTruthyEnvFlag(env.GITHUB_ACTIONS)) { + } else if (isOpenEndedTruthyValue(env.GITHUB_ACTIONS)) { for (const finding of findings) { stderr.write(`${formatGithubWarning(finding)}\n`); } diff --git a/scripts/run-additional-boundary-checks.mts b/scripts/run-additional-boundary-checks.mts index 6534534dda0e..21f2a907ef75 100644 --- a/scripts/run-additional-boundary-checks.mts +++ b/scripts/run-additional-boundary-checks.mts @@ -100,11 +100,6 @@ export const BOUNDARY_CHECKS = ( "pnpm", ["run", "lint:extensions:no-normalization-core-bypass"], ], - [ - "extension-plugin-sdk-internal-boundary", - "pnpm", - ["run", "lint:extensions:no-plugin-sdk-internal"], - ], [ "extension-relative-outside-package-boundary", "pnpm", diff --git a/scripts/test-group-report.mts b/scripts/test-group-report.mts index f52bd851b32f..337c93f2bf08 100644 --- a/scripts/test-group-report.mts +++ b/scripts/test-group-report.mts @@ -6,7 +6,7 @@ import path from "node:path"; import { pathToFileURL } from "node:url"; import { isRecord } from "@openclaw/normalization-core/record-coerce"; import pMap from "p-map"; -import { coerceErrorMessage as formatSpawnError } from "./lib/error-format.mts"; +import { coerceErrorMessage } from "./lib/error-format.mts"; import { parsePositiveInt } from "./lib/numeric-options.mjs"; import { buildGroupedTestComparison, @@ -361,7 +361,7 @@ export function signalTestGroupReportChild( } catch (error) { if (error && !hasErrorCode(error, "ESRCH")) { appendDiagnostic( - `[test-group-report] failed to send ${signal} to process group: ${formatSpawnError(error)}\n`, + `[test-group-report] failed to send ${signal} to process group: ${coerceErrorMessage(error)}\n`, ); } } @@ -733,7 +733,7 @@ function readReportInputs(entries: ReportInputEntry[]) { } catch (error) { invalid.push({ entry, - reason: error instanceof Error ? error.message : String(error), + reason: coerceErrorMessage(error), }); } } @@ -1134,7 +1134,7 @@ export async function runReportPlans(params: { `[test-group-report] config failed; keeping partial report from ${run.reportPath}`, ); } catch (error) { - const reason = error instanceof Error ? error.message : String(error); + const reason = coerceErrorMessage(error); console.error( `[test-group-report] config failed; skipping unusable JSON report from ${run.reportPath} (${reason})`, ); @@ -1283,7 +1283,7 @@ const isMain = if (isMain) { main().catch((error: unknown) => { - console.error(error instanceof Error ? error.message : String(error)); + console.error(coerceErrorMessage(error)); process.exit(1); }); } diff --git a/scripts/test-perf-budget.mts b/scripts/test-perf-budget.mts index 33eef7a2cf87..78fe4b5d2c98 100644 --- a/scripts/test-perf-budget.mts +++ b/scripts/test-perf-budget.mts @@ -1,21 +1,22 @@ // Runs a Vitest config and enforces wall-time regression budgets. import { pathToFileURL } from "node:url"; import { isRecord } from "@openclaw/normalization-core/record-coerce"; -import { booleanFlag, parseFlagArgs, stringFlag, type FlagSpec } from "./lib/arg-utils.mts"; +import { + booleanFlag, + isStrictAffirmativeValue, + parseFlagArgs, + stringFlag, + type FlagSpec, +} from "./lib/arg-utils.mts"; import { budgetFloatFlag, parseBudgetNumber, readBudgetEnvNumber, } from "./lib/budget-number-args.mts"; -import { coerceErrorMessage as formatErrorMessage } from "./lib/error-format.mts"; +import { coerceErrorMessage } from "./lib/error-format.mts"; import { formatMs } from "./lib/vitest-report-cli-utils.mts"; import { readJsonFile, runVitestJsonReport } from "./test-report-utils.mts"; -function readBooleanEnv(name: string, env = process.env) { - const normalized = env[name]?.trim().toLowerCase(); - return normalized === "1" || normalized === "true" || normalized === "yes"; -} - type PerfBudgetOptions = { baselineWallMs: number | null; config: string; @@ -61,7 +62,7 @@ function parseArgs(argv: readonly string[], env = process.env) { maxWallMs: readBudgetEnvNumber("OPENCLAW_TEST_PERF_MAX_WALL_MS", env), baselineWallMs: readBudgetEnvNumber("OPENCLAW_TEST_PERF_BASELINE_WALL_MS", env), maxRegressionPct: readBudgetEnvNumber("OPENCLAW_TEST_PERF_MAX_REGRESSION_PCT", env) ?? 10, - reportOnly: readBooleanEnv("OPENCLAW_TEST_PERF_REPORT_ONLY", env), + reportOnly: isStrictAffirmativeValue(env.OPENCLAW_TEST_PERF_REPORT_ONLY), }, [ stringFlag("--config", "config"), @@ -85,7 +86,7 @@ function collectPerfReportStats(reportPath: string) { report = readJsonFile(reportPath); } catch (error) { throw new Error( - `[test-perf-budget] failed to read Vitest JSON report ${reportPath}: ${formatErrorMessage( + `[test-perf-budget] failed to read Vitest JSON report ${reportPath}: ${coerceErrorMessage( error, )}`, { cause: error }, @@ -113,7 +114,7 @@ function main() { try { opts = parseArgs(process.argv.slice(2)); } catch (error) { - console.error(error instanceof Error ? error.message : String(error)); + console.error(coerceErrorMessage(error)); process.exit(1); } @@ -128,7 +129,7 @@ function main() { try { reportStats = collectPerfReportStats(reportPath); } catch (error) { - console.error(formatErrorMessage(error)); + console.error(coerceErrorMessage(error)); process.exit(1); } diff --git a/scripts/verify-plugin-npm-published-runtime.mts b/scripts/verify-plugin-npm-published-runtime.mts index d8d741583715..f12920a13ba5 100644 --- a/scripts/verify-plugin-npm-published-runtime.mts +++ b/scripts/verify-plugin-npm-published-runtime.mts @@ -9,8 +9,11 @@ import path from "node:path"; import { pathToFileURL } from "node:url"; import { isRecord } from "@openclaw/normalization-core/record-coerce"; import * as tar from "tar"; +import { readPositiveIntEnv } from "./e2e/lib/env-limits.mjs"; import { sleep } from "./lib/sleep.mjs"; +export { readPositiveIntEnv }; + const DEFAULT_NPM_COMMAND_TIMEOUT_MS = 5 * 60 * 1000; const DEFAULT_NPM_COMMAND_MAX_BUFFER_BYTES = 16 * 1024 * 1024; @@ -228,18 +231,6 @@ export function resolveNpmPackFilename(output: string) { return filename; } -export function readPositiveIntEnv(name: string, fallback: number, env = process.env) { - const text = String(env[name] ?? fallback).trim(); - if (!/^\d+$/u.test(text)) { - throw new Error(`invalid ${name}: ${text}`); - } - const value = Number(text); - if (!Number.isSafeInteger(value) || value <= 0) { - throw new Error(`invalid ${name}: ${text}`); - } - return value; -} - export function readPluginNpmCommandOptions(env: NodeJS.ProcessEnv = process.env) { return { encoding: "utf8", diff --git a/src/acp/control-plane/runtime-options.ts b/src/acp/control-plane/runtime-options.ts index e151465fc432..2d88fee988e1 100644 --- a/src/acp/control-plane/runtime-options.ts +++ b/src/acp/control-plane/runtime-options.ts @@ -1,11 +1,11 @@ /** Validation and normalization for ACP session runtime options and config controls. */ import { isAbsolute } from "node:path"; +import { parseStrictPositiveInteger } from "@openclaw/normalization-core/number-coercion"; import { normalizeLowercaseStringOrEmpty, normalizeOptionalString as normalizeText, } from "@openclaw/normalization-core/string-coerce"; import type { AcpSessionRuntimeOptions, SessionAcpMeta } from "../../config/sessions/types.js"; -import { parseStrictPositiveInteger } from "../../infra/parse-finite-number.js"; import { AcpRuntimeError } from "../runtime/errors.js"; export { normalizeOptionalString as normalizeText } from "@openclaw/normalization-core/string-coerce"; diff --git a/src/agents/agent-command.live-model-switch.test.ts b/src/agents/agent-command.live-model-switch.test.ts index 179f3083e171..8de5b119c329 100644 --- a/src/agents/agent-command.live-model-switch.test.ts +++ b/src/agents/agent-command.live-model-switch.test.ts @@ -438,7 +438,7 @@ vi.mock("../routing/session-key.js", async () => { ); return { ...actual, - normalizeAgentId: (id: string) => id, + normalizeAgentId: vi.fn((id: string) => id), normalizeMainKey: (key?: string | null) => key?.trim() || "main", }; }); diff --git a/src/agents/agent-command.ts b/src/agents/agent-command.ts index ead294e62691..6f59cbc2ccea 100644 --- a/src/agents/agent-command.ts +++ b/src/agents/agent-command.ts @@ -1,4 +1,5 @@ /** Main agent command orchestration for sessions, model selection, delivery, and attempts. */ +import { coerceErrorMessage } from "@openclaw/normalization-core/error-coercion"; import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce"; import type { VerboseLevel } from "../auto-reply/thinking.js"; import type { CliDeps } from "../cli/deps.types.js"; @@ -295,9 +296,7 @@ async function agentCommandInternal( throw error; } log.warn( - `delivery preflight failed; continuing model run with requested delivery intent because bestEffortDeliver is enabled: ${ - error instanceof Error ? error.message : String(error) - }`, + `delivery preflight failed; continuing model run with requested delivery intent because bestEffortDeliver is enabled: ${coerceErrorMessage(error)}`, ); } assertAgentRunLifecycleGenerationCurrent(lifecycleGeneration); @@ -388,9 +387,7 @@ async function agentCommandInternal( } } catch (error) { log.warn( - `session diff baseline capture failed; continuing without attribution filtering: ${ - error instanceof Error ? error.message : String(error) - }`, + `session diff baseline capture failed; continuing without attribution filtering: ${coerceErrorMessage(error)}`, ); } } @@ -540,11 +537,7 @@ async function agentCommandInternal( } catch (error) { // Cleanup remains best-effort so a terminal SQLite write failure does // not replace the completed model-run result; the DB layer warns too. - log.warn( - `failed to remove model-run SQLite session: ${ - error instanceof Error ? error.message : String(error) - }`, - ); + log.warn(`failed to remove model-run SQLite session: ${coerceErrorMessage(error)}`); } } } @@ -580,9 +573,7 @@ async function agentCommandInternal( } } catch (error) { log.warn( - `failed to clear restart recovery delivery context for ${sessionKey}: ${ - error instanceof Error ? error.message : String(error) - }`, + `failed to clear restart recovery delivery context for ${sessionKey}: ${coerceErrorMessage(error)}`, ); } } diff --git a/src/agents/agent-tools.params.ts b/src/agents/agent-tools.params.ts index d3de0b10fbb0..bbcfa7163480 100644 --- a/src/agents/agent-tools.params.ts +++ b/src/agents/agent-tools.params.ts @@ -3,8 +3,12 @@ * Converts malformed file-tool arguments into retryable errors and fixes the * specific XML suffix and Office-extension corruption seen in path arguments. */ +import { asOptionalObjectRecord as getToolParamsRecord } from "@openclaw/normalization-core/record-coerce"; import type { AnyAgentTool } from "./agent-tools.types.js"; +/** Return a record view of model-supplied tool params when possible. */ +export { getToolParamsRecord }; + export type RequiredParamGroup = { keys: readonly string[]; allowEmpty?: boolean; @@ -106,11 +110,6 @@ export const REQUIRED_PARAM_GROUPS = { ], } as const; -/** Return a record view of model-supplied tool params when possible. */ -export function getToolParamsRecord(params: unknown): Record | undefined { - return params && typeof params === "object" ? (params as Record) : undefined; -} - /** Strip extra closing markers sometimes produced in XML arg_value path params. */ function stripMalformedXmlArgValueSuffix(value: string): string { return value.includes("") ? value.replace(XML_ARG_VALUE_SUFFIX_RE, "") : value; diff --git a/src/agents/auth-profiles/oauth-manager.test.ts b/src/agents/auth-profiles/oauth-manager.test.ts index 1a28d1253a4c..50e8e85882e4 100644 --- a/src/agents/auth-profiles/oauth-manager.test.ts +++ b/src/agents/auth-profiles/oauth-manager.test.ts @@ -6,10 +6,10 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; +import { MAX_DATE_TIMESTAMP_MS } from "@openclaw/normalization-core/number-coercion"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import type { OpenClawConfig } from "../../config/types.openclaw.js"; import { formatErrorMessage } from "../../infra/errors.js"; -import { MAX_DATE_TIMESTAMP_MS } from "../../shared/number-coercion.js"; import { withEnvAsync } from "../../test-utils/env.js"; import { testing as externalAuthTesting } from "./external-auth.test-support.js"; import { createOAuthManager, OAuthManagerRefreshError } from "./oauth-manager.js"; diff --git a/src/agents/auth-profiles/oauth-manager.ts b/src/agents/auth-profiles/oauth-manager.ts index 3de2653d659b..559d7dd50ab5 100644 --- a/src/agents/auth-profiles/oauth-manager.ts +++ b/src/agents/auth-profiles/oauth-manager.ts @@ -1,3 +1,4 @@ +import { asDateTimestampMs } from "@openclaw/normalization-core/number-coercion"; /** * OAuth credential manager. * Resolves usable access tokens, refreshes expired credentials under global @@ -9,7 +10,6 @@ import { formatErrorMessage } from "../../infra/errors.js"; import { withFileLock } from "../../infra/file-lock.js"; import { redactSensitiveText } from "../../logging/redact.js"; import { KeyedAsyncQueue } from "../../plugin-sdk/keyed-async-queue.js"; -import { asDateTimestampMs } from "../../shared/number-coercion.js"; import { OAUTH_REFRESH_CALL_TIMEOUT_MS, OAUTH_REFRESH_LOCK_OPTIONS, diff --git a/src/agents/auth-profiles/oauth-shared.test.ts b/src/agents/auth-profiles/oauth-shared.test.ts index 781948e92e2d..4e9a1bd5bc6f 100644 --- a/src/agents/auth-profiles/oauth-shared.test.ts +++ b/src/agents/auth-profiles/oauth-shared.test.ts @@ -5,8 +5,8 @@ */ import { expectDefined } from "@openclaw/normalization-core"; +import { MAX_DATE_TIMESTAMP_MS } from "@openclaw/normalization-core/number-coercion"; import { describe, expect, it, vi } from "vitest"; -import { MAX_DATE_TIMESTAMP_MS } from "../../shared/number-coercion.js"; import { overlayRuntimeExternalOAuthProfiles, shouldReplaceStoredOAuthCredential, diff --git a/src/agents/auth-profiles/oauth-shared.ts b/src/agents/auth-profiles/oauth-shared.ts index c2cd9f104405..feef98fa8c28 100644 --- a/src/agents/auth-profiles/oauth-shared.ts +++ b/src/agents/auth-profiles/oauth-shared.ts @@ -3,7 +3,7 @@ * Used by manager, external CLI overlays, and persistence paths to decide when * incoming runtime credentials may replace or bootstrap stored profiles. */ -import { asDateTimestampMs } from "../../shared/number-coercion.js"; +import { asDateTimestampMs } from "@openclaw/normalization-core/number-coercion"; import { cloneAuthProfileStore } from "./clone.js"; import { hasUsableOAuthCredential } from "./credential-state.js"; import { diff --git a/src/agents/auth-profiles/ownership.ts b/src/agents/auth-profiles/ownership.ts index 589be46f4bc2..b2e3fb5f0d37 100644 --- a/src/agents/auth-profiles/ownership.ts +++ b/src/agents/auth-profiles/ownership.ts @@ -1,5 +1,5 @@ import { isDeepStrictEqual } from "node:util"; -import { asDateTimestampMs } from "../../shared/number-coercion.js"; +import { asDateTimestampMs } from "@openclaw/normalization-core/number-coercion"; import { isSafeToAdoptMainStoreOAuthIdentity } from "./oauth-shared.js"; import type { AuthProfileStore } from "./types.js"; diff --git a/src/agents/auth-profiles/usage-state.ts b/src/agents/auth-profiles/usage-state.ts index 3d190a981055..15fa29a72a1e 100644 --- a/src/agents/auth-profiles/usage-state.ts +++ b/src/agents/auth-profiles/usage-state.ts @@ -4,7 +4,7 @@ * predicates used by rotation and failure handling. */ import { normalizeProviderId } from "@openclaw/model-catalog-core/provider-id"; -import { asDateTimestampMs } from "../../shared/number-coercion.js"; +import { asDateTimestampMs } from "@openclaw/normalization-core/number-coercion"; import type { AuthProfileFailureReason, AuthProfileStore, ProfileUsageStats } from "./types.js"; /** Returns true for providers whose auth-profile cooldowns are provider-managed. */ diff --git a/src/agents/auth-profiles/usage.test.ts b/src/agents/auth-profiles/usage.test.ts index 4e7bd680a44a..c0ffa90973d9 100644 --- a/src/agents/auth-profiles/usage.test.ts +++ b/src/agents/auth-profiles/usage.test.ts @@ -1,3 +1,4 @@ +import { MAX_DATE_TIMESTAMP_MS } from "@openclaw/normalization-core/number-coercion"; /** * Usage-state and failure cooldown tests for auth profiles. * Covers unusable-window helpers, provider bypasses, WHAM probes, and store @@ -5,7 +6,6 @@ */ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import type { OpenClawConfig } from "../../config/types.openclaw.js"; -import { MAX_DATE_TIMESTAMP_MS } from "../../shared/number-coercion.js"; import type { AuthProfileStore, ProfileUsageStats } from "./types.js"; import { resolveProfileUnusableUntil } from "./usage-state.js"; import { diff --git a/src/agents/codex-mcp-config.ts b/src/agents/codex-mcp-config.ts index bd0f321f1ea2..2b632cb8cbbe 100644 --- a/src/agents/codex-mcp-config.ts +++ b/src/agents/codex-mcp-config.ts @@ -5,6 +5,7 @@ */ import crypto from "node:crypto"; import { normalizeOptionalLowercaseString } from "@openclaw/normalization-core/string-coerce"; +import { normalizeTrimmedStringList } from "@openclaw/normalization-core/string-normalization"; import { normalizeConfiguredMcpServers } from "../config/mcp-config-normalize.js"; import type { SessionToolOverrides } from "../config/sessions/types.js"; import { @@ -28,16 +29,6 @@ import { shouldCreateBundleMcpRuntimeForAttempt } from "./embedded-agent-runner/ import { resolveProjectedMcpCodexToolApprovalMode } from "./mcp-codex-tool-approval.js"; import { partitionMcpServersByConnectionScope } from "./mcp-connection-resolver.js"; -function normalizeToolFilterList(value: unknown): string[] { - if (!Array.isArray(value)) { - return []; - } - return value - .filter((entry): entry is string => typeof entry === "string") - .map((entry) => entry.trim()) - .filter(Boolean); -} - function assertCodexExactToolFilters( serverName: string, fieldName: "include" | "exclude", @@ -61,8 +52,8 @@ function applyCodexToolFilter( if (!isRecord(server.toolFilter)) { return; } - const include = normalizeToolFilterList(server.toolFilter.include); - const exclude = normalizeToolFilterList(server.toolFilter.exclude); + const include = normalizeTrimmedStringList(server.toolFilter.include); + const exclude = normalizeTrimmedStringList(server.toolFilter.exclude); assertCodexExactToolFilters(name, "include", include); assertCodexExactToolFilters(name, "exclude", exclude); if (include.length > 0) { @@ -85,7 +76,7 @@ export function applyCodexSessionMcpToolDenials( return server; } const toolFilter = isRecord(server.toolFilter) ? server.toolFilter : {}; - const existing = normalizeToolFilterList(toolFilter.exclude); + const existing = normalizeTrimmedStringList(toolFilter.exclude); return { ...server, toolFilter: { diff --git a/src/agents/command/prepare.ts b/src/agents/command/prepare.ts index f664926a3a49..f831a6452d7c 100644 --- a/src/agents/command/prepare.ts +++ b/src/agents/command/prepare.ts @@ -1,3 +1,4 @@ +import { parseStrictNonNegativeInteger } from "@openclaw/normalization-core/number-coercion"; import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce"; import { resolveSessionStableReplyMode } from "../../auto-reply/reply/session-stable-reply-mode.js"; import { isSyntheticSourceReplyTurn } from "../../auto-reply/reply/source-reply-delivery-mode.js"; @@ -10,7 +11,6 @@ import { formatCliCommand } from "../../cli/command-format.js"; import type { OpenClawConfig } from "../../config/types.openclaw.js"; import { resolveAgentExplicitRecipientSession } from "../../infra/outbound/agent-delivery.js"; import { buildOutboundSessionContext } from "../../infra/outbound/session-context.js"; -import { parseStrictNonNegativeInteger } from "../../infra/parse-finite-number.js"; import { normalizePluginsConfig } from "../../plugins/config-state.js"; import { isPluginMetadataSnapshotCompatible, diff --git a/src/agents/embedded-agent-runner.guard.test.ts b/src/agents/embedded-agent-runner.guard.test.ts index 2dc43818a927..12247f864ddf 100644 --- a/src/agents/embedded-agent-runner.guard.test.ts +++ b/src/agents/embedded-agent-runner.guard.test.ts @@ -2,6 +2,7 @@ // redaction. import { readFileSync } from "node:fs"; import { expectDefined } from "@openclaw/normalization-core"; +import { MAX_TIMER_TIMEOUT_MS } from "@openclaw/normalization-core/number-coercion"; import type { AgentMessage } from "openclaw/plugin-sdk/agent-core"; import { SessionManager } from "openclaw/plugin-sdk/agent-sessions"; import { @@ -19,7 +20,6 @@ import { type PersistedUserTurnMessage, } from "../sessions/user-turn-transcript.js"; import { createTestUserTurnTranscriptTarget } from "../sessions/user-turn-transcript.test-support.js"; -import { MAX_TIMER_TIMEOUT_MS } from "../shared/number-coercion.js"; import { flushPendingToolResultsAfterIdle } from "./embedded-agent-runner/wait-for-idle-before-flush.js"; import { guardSessionManager } from "./session-tool-result-guard-wrapper.js"; import { sanitizeToolUseResultPairing } from "./session-transcript-repair.js"; diff --git a/src/agents/embedded-agent-runner/openrouter-model-capabilities.ts b/src/agents/embedded-agent-runner/openrouter-model-capabilities.ts index f98dc3994ac9..dcb1a5ebe53a 100644 --- a/src/agents/embedded-agent-runner/openrouter-model-capabilities.ts +++ b/src/agents/embedded-agent-runner/openrouter-model-capabilities.ts @@ -18,10 +18,10 @@ * capabilities instead of the text-only fallback. */ +import { parseStrictFiniteNumber } from "@openclaw/normalization-core/number-coercion"; import { formatErrorMessage } from "../../infra/errors.js"; import { cancelUnreadResponseBody, readResponseWithLimit } from "../../infra/http-body.js"; import { resolveProxyFetchFromEnv } from "../../infra/net/proxy-fetch.js"; -import { parseStrictFiniteNumber } from "../../infra/parse-finite-number.js"; import { createSubsystemLogger } from "../../logging/subsystem.js"; import { createCorePluginStateSyncKeyedStore } from "../../plugin-state/plugin-state-store.js"; diff --git a/src/agents/embedded-agent-runner/run/attempt-finalize.ts b/src/agents/embedded-agent-runner/run/attempt-finalize.ts index cb0442bccc87..def1cbdb2663 100644 --- a/src/agents/embedded-agent-runner/run/attempt-finalize.ts +++ b/src/agents/embedded-agent-runner/run/attempt-finalize.ts @@ -3,13 +3,13 @@ * It may assume stream execution and transcript writes are settled. */ +import { parseStrictPositiveInteger } from "@openclaw/normalization-core/number-coercion"; import { readActiveTranscriptEntryAnchor } from "../../../config/sessions/session-accessor.js"; import { OPENCLAW_EMBEDDED_CONTEXT_ENGINE_HOST } from "../../../context-engine/host-compat.js"; import type { ContextEngine } from "../../../context-engine/types.js"; import { freezeDiagnosticTraceContext } from "../../../infra/diagnostic-trace-context.js"; import { isFastTestRuntimeEnv } from "../../../infra/env.js"; import { formatErrorMessage } from "../../../infra/errors.js"; -import { parseStrictPositiveInteger } from "../../../infra/parse-finite-number.js"; import type { getGlobalHookRunner } from "../../../plugins/hook-runner-global.js"; import { buildTrajectoryArtifacts } from "../../../trajectory/metadata.js"; import { projectAgentRunAttemptTerminal } from "../../agent-run-terminal-outcome.js"; diff --git a/src/agents/embedded-agent-runner/run/attempt.tool-call-argument-repair.ts b/src/agents/embedded-agent-runner/run/attempt.tool-call-argument-repair.ts index 00c5bb6bcab5..c951fe51a25d 100644 --- a/src/agents/embedded-agent-runner/run/attempt.tool-call-argument-repair.ts +++ b/src/agents/embedded-agent-runner/run/attempt.tool-call-argument-repair.ts @@ -2,6 +2,7 @@ * Repairs malformed tool-call arguments in embedded-agent stream results. */ import { extractBalancedJsonPrefix } from "@openclaw/normalization-core"; +import { safeParseJsonRecord } from "@openclaw/normalization-core/json-coercion"; import { normalizeProviderId } from "../../model-selection.js"; import type { StreamFn } from "../../runtime/index.js"; import type { MutableAssistantMessageEventStream } from "../../stream-compat.js"; @@ -152,17 +153,6 @@ type ToolCallRepairParsedObject = { endIndex: number; }; -function parseUsableObjectJson(raw: string): Record | undefined { - try { - const parsed = JSON.parse(raw) as unknown; - return parsed && typeof parsed === "object" && !Array.isArray(parsed) - ? (parsed as Record) - : undefined; - } catch { - return undefined; - } -} - function findAsciiStringEnd(raw: string, startIndex: number): number { let escaped = false; for (let i = startIndex + 1; i < raw.length; i += 1) { @@ -492,7 +482,7 @@ function tryExtractUsableToolCallArgumentsFromJson( return undefined; } - const parsedExtracted = parseUsableObjectJson(extracted.json); + const parsedExtracted = safeParseJsonRecord(extracted.json); if (!parsedExtracted) { return undefined; } @@ -549,7 +539,7 @@ function tryExtractUsableToolCallArguments( if (!raw.trim()) { return undefined; } - const parsedRaw = parseUsableObjectJson(raw); + const parsedRaw = safeParseJsonRecord(raw); if (parsedRaw) { return { args: parsedRaw, diff --git a/src/agents/embedded-agent-runner/run/preemptive-compaction.ts b/src/agents/embedded-agent-runner/run/preemptive-compaction.ts index c02b4b979dea..513299cc1a99 100644 --- a/src/agents/embedded-agent-runner/run/preemptive-compaction.ts +++ b/src/agents/embedded-agent-runner/run/preemptive-compaction.ts @@ -1,9 +1,9 @@ /** * Estimates prompt pressure and decides pre-prompt compaction routing. */ +import { estimateStringChars } from "@openclaw/normalization-core/cjk-chars"; import { isRecord } from "@openclaw/normalization-core/record-coerce"; import type { SessionContextBudgetStatus } from "../../../config/sessions.js"; -import { estimateStringChars } from "../../../utils/cjk-chars.js"; import { MIN_PROMPT_BUDGET_RATIO, MIN_PROMPT_BUDGET_TOKENS, diff --git a/src/agents/embedded-agent-runner/runs.lifecycle.test.ts b/src/agents/embedded-agent-runner/runs.lifecycle.test.ts index 9305d2e8a11b..637acd865cf2 100644 --- a/src/agents/embedded-agent-runner/runs.lifecycle.test.ts +++ b/src/agents/embedded-agent-runner/runs.lifecycle.test.ts @@ -1,3 +1,4 @@ +import { MAX_TIMER_TIMEOUT_MS } from "@openclaw/normalization-core/number-coercion"; // Embedded run lifecycle tests cover drain/wait behavior, process-global // ownership, abandonment tracking, and snapshots. import { importFreshModule } from "openclaw/plugin-sdk/test-fixtures"; @@ -7,7 +8,6 @@ import { testing as replyRunTesting } from "../../auto-reply/reply/reply-run-reg import { setDiagnosticsEnabledForProcess } from "../../infra/diagnostic-events.js"; import { resetDiagnosticSessionStateForTest } from "../../logging/diagnostic-session-state.js"; import { diagnosticLogger } from "../../logging/diagnostic.js"; -import { MAX_TIMER_TIMEOUT_MS } from "../../shared/number-coercion.js"; import { abortAndDrainEmbeddedAgentRun, clearActiveEmbeddedRun, diff --git a/src/agents/embedded-agent-runner/runs.ts b/src/agents/embedded-agent-runner/runs.ts index 4470df84189a..ac50d4d8a7cf 100644 --- a/src/agents/embedded-agent-runner/runs.ts +++ b/src/agents/embedded-agent-runner/runs.ts @@ -3,6 +3,7 @@ */ import fs from "node:fs"; import path from "node:path"; +import { resolveTimerTimeoutMs } from "@openclaw/normalization-core/number-coercion"; import { abortActiveReplyRuns, abortReplyRunBySessionId, @@ -40,7 +41,6 @@ import { import { logMessageQueuedWithBacklogPolicy } from "../../logging/diagnostic-runtime.js"; import { diagnosticLogger as diag, logSessionStateChange } from "../../logging/diagnostic.js"; import { resolveAgentIdFromSessionKey } from "../../routing/session-key.js"; -import { resolveTimerTimeoutMs } from "../../shared/number-coercion.js"; import { ACTIVE_EMBEDDED_RUNS, ACTIVE_EMBEDDED_RUNS_BY_RUN_ID, diff --git a/src/agents/embedded-agent-runner/tool-result-text-budget.ts b/src/agents/embedded-agent-runner/tool-result-text-budget.ts index 0d12d73b9680..e8e187f73230 100644 --- a/src/agents/embedded-agent-runner/tool-result-text-budget.ts +++ b/src/agents/embedded-agent-runner/tool-result-text-budget.ts @@ -1,5 +1,5 @@ +import { estimateStringChars } from "@openclaw/normalization-core/cjk-chars"; import { sliceUtf16Safe } from "@openclaw/normalization-core/utf16-slice"; -import { estimateStringChars } from "../../utils/cjk-chars.js"; type ToolResultTextBudgetOptions = { minimumRawWeight?: number; diff --git a/src/agents/embedded-agent-runner/tool-result-truncation.test.ts b/src/agents/embedded-agent-runner/tool-result-truncation.test.ts index 7f4e886ab7d6..12c28e7457ff 100644 --- a/src/agents/embedded-agent-runner/tool-result-truncation.test.ts +++ b/src/agents/embedded-agent-runner/tool-result-truncation.test.ts @@ -3,6 +3,7 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; +import { estimateStringChars } from "@openclaw/normalization-core/cjk-chars"; import type { AgentMessage } from "openclaw/plugin-sdk/agent-core"; import { SessionManager } from "openclaw/plugin-sdk/agent-sessions"; import type { AssistantMessage, ToolResultMessage, UserMessage } from "openclaw/plugin-sdk/llm"; @@ -17,7 +18,6 @@ import { } from "../../config/sessions/session-accessor.js"; import type { SessionEntry as SessionStoreEntry } from "../../config/sessions/types.js"; import { onInternalSessionTranscriptUpdate } from "../../sessions/transcript-events.js"; -import { estimateStringChars } from "../../utils/cjk-chars.js"; import { formatFullOutputFooter } from "../sessions/tools/tool-contracts.js"; import { makeAgentAssistantMessage } from "../test-helpers/agent-message-fixtures.js"; import { diff --git a/src/agents/embedded-agent-runner/tool-result-truncation.ts b/src/agents/embedded-agent-runner/tool-result-truncation.ts index c177e93f9bde..fd18f424a8f6 100644 --- a/src/agents/embedded-agent-runner/tool-result-truncation.ts +++ b/src/agents/embedded-agent-runner/tool-result-truncation.ts @@ -1,5 +1,6 @@ import { createHash } from "node:crypto"; import { existsSync } from "node:fs"; +import { estimateStringChars } from "@openclaw/normalization-core/cjk-chars"; import { isRecord } from "@openclaw/normalization-core/record-coerce"; import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce"; import { sliceUtf16Safe } from "@openclaw/normalization-core/utf16-slice"; @@ -9,7 +10,6 @@ import { createDedupeCache } from "../../infra/dedupe.js"; import { formatErrorMessage } from "../../infra/errors.js"; import type { TextContent } from "../../llm/types.js"; import { emitSessionTranscriptUpdate } from "../../sessions/transcript-events.js"; -import { estimateStringChars } from "../../utils/cjk-chars.js"; import { compileGlobPatterns, matchesAnyGlobPattern } from "../glob-pattern.js"; import type { AgentMessage } from "../runtime/index.js"; import { SessionManager } from "../sessions/index.js"; diff --git a/src/agents/embedded-agent-runner/wait-for-idle-before-flush.ts b/src/agents/embedded-agent-runner/wait-for-idle-before-flush.ts index 8785c1986610..21e8370966b0 100644 --- a/src/agents/embedded-agent-runner/wait-for-idle-before-flush.ts +++ b/src/agents/embedded-agent-runner/wait-for-idle-before-flush.ts @@ -1,7 +1,7 @@ /** * Waits for tool-result streams to become idle before flushing output. */ -import { resolveTimerTimeoutMs } from "../../shared/number-coercion.js"; +import { resolveTimerTimeoutMs } from "@openclaw/normalization-core/number-coercion"; type IdleAwareAgent = { waitForIdle?: (() => Promise) | undefined; diff --git a/src/agents/live-cache-test-support.ts b/src/agents/live-cache-test-support.ts index 5bce9348c808..ccd0adfd241a 100644 --- a/src/agents/live-cache-test-support.ts +++ b/src/agents/live-cache-test-support.ts @@ -1,9 +1,9 @@ +import { parseStrictInteger } from "@openclaw/normalization-core/number-coercion"; /** * Shared helpers for live prompt-cache integration tests. */ import { getRuntimeConfig } from "../config/config.js"; import { isTruthyEnvValue } from "../infra/env.js"; -import { parseStrictInteger } from "../infra/parse-finite-number.js"; import { completeSimple } from "../llm/stream.js"; import type { Api, AssistantMessage, Model } from "../llm/types.js"; import { discoverAuthStorage, discoverModels } from "./agent-model-discovery.js"; diff --git a/src/agents/mcp-auth-profile.ts b/src/agents/mcp-auth-profile.ts index aae5f0e335e6..919ca3459b2c 100644 --- a/src/agents/mcp-auth-profile.ts +++ b/src/agents/mcp-auth-profile.ts @@ -3,7 +3,7 @@ */ import crypto from "node:crypto"; import type { FetchLike } from "@modelcontextprotocol/sdk/shared/transport.js"; -import { isRecord } from "@openclaw/normalization-core/record-coerce"; +import { filterStringRecord, isRecord } from "@openclaw/normalization-core/record-coerce"; import type { OpenClawConfig } from "../config/types.openclaw.js"; import type { BundleMcpConfig, BundleMcpServerConfig } from "../plugins/bundle-mcp.js"; import { resolveApiKeyForProfile } from "./auth-profiles/oauth.js"; @@ -22,16 +22,6 @@ type McpAuthProfileOptions = { agentDir?: string; }; -function normalizeStringHeaders(value: unknown): Record | undefined { - if (!isRecord(value)) { - return undefined; - } - const entries = Object.entries(value).filter( - (entry): entry is [string, string] => typeof entry[1] === "string", - ); - return entries.length > 0 ? Object.fromEntries(entries) : undefined; -} - /** Returns the refresh-capable auth profile selected for one MCP server. */ export function resolveMcpAuthProfileId(rawServer: unknown): string | undefined { if (!isRecord(rawServer) || rawServer.auth !== "oauth" || !isRecord(rawServer.oauth)) { @@ -229,7 +219,7 @@ export async function resolveMcpBearerBundleConfig( nextEnv[envVar] = token; authorization = `Bearer \${${envVar}}`; } - const headers = withoutMcpAuthorizationHeader(normalizeStringHeaders(server.headers)); + const headers = withoutMcpAuthorizationHeader(filterStringRecord(server.headers)); nextServers ??= { ...params.config.mcpServers }; nextServers[serverName] = stripOpenClawOnlyOAuthConfig({ ...server, diff --git a/src/agents/minimax-vlm.ts b/src/agents/minimax-vlm.ts index 3ba6594f8491..ae43cc8fe177 100644 --- a/src/agents/minimax-vlm.ts +++ b/src/agents/minimax-vlm.ts @@ -1,3 +1,4 @@ +import { resolvePositiveTimerTimeoutMs } from "@openclaw/normalization-core/number-coercion"; import { readResponseBodySnippet } from "../infra/http-error-body.js"; /** * Adapts MiniMax VLM image-understanding requests for agent image inputs. @@ -6,7 +7,6 @@ import { postJsonRequest, resolveProviderHttpRequestConfigWithOriginTrust, } from "../media-understanding/shared.js"; -import { resolvePositiveTimerTimeoutMs } from "../shared/number-coercion.js"; import { isRecord } from "../utils.js"; import { normalizeSecretInput } from "../utils/normalize-secret-input.js"; import { readProviderJsonResponse } from "./provider-http-errors.js"; diff --git a/src/agents/model-catalog-browse.test.ts b/src/agents/model-catalog-browse.test.ts index d75080c83adf..fefd1f17e6b3 100644 --- a/src/agents/model-catalog-browse.test.ts +++ b/src/agents/model-catalog-browse.test.ts @@ -1,10 +1,10 @@ +import { MAX_TIMER_TIMEOUT_MS } from "@openclaw/normalization-core/number-coercion"; /** * Regression coverage for model catalog browsing. * Verifies filtered catalog output and pending load behavior. */ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import type { OpenClawConfig } from "../config/types.openclaw.js"; -import { MAX_TIMER_TIMEOUT_MS } from "../shared/number-coercion.js"; import { buildProviderConfigModelCatalogForBrowse, loadPreparedModelCatalogSnapshotForBrowse, diff --git a/src/agents/openclaw-tools.media-factory-plan.ts b/src/agents/openclaw-tools.media-factory-plan.ts index a81b01eb45eb..b802ab571143 100644 --- a/src/agents/openclaw-tools.media-factory-plan.ts +++ b/src/agents/openclaw-tools.media-factory-plan.ts @@ -1,12 +1,12 @@ import { uniqueStrings } from "@openclaw/normalization-core/string-normalization"; import { findCapabilityProviderById } from "../../packages/media-generation-core/src/capability-model-ref.js"; +import { normalizeMediaProviderId } from "../../packages/media-understanding-common/src/provider-id.js"; import { resolveAgentModelFallbackValues, resolveAgentModelPrimaryValue, } from "../config/model-input.js"; import type { AgentModelConfig } from "../config/types.agents-shared.js"; import type { OpenClawConfig } from "../config/types.openclaw.js"; -import { normalizeMediaProviderId } from "../media-understanding/provider-id.js"; import type { PluginMetadataSnapshot } from "../plugins/plugin-metadata-snapshot.types.js"; import { listProfilesForProvider } from "./auth-profiles/profile-list.js"; import type { AuthProfileStore } from "./auth-profiles/types.js"; diff --git a/src/agents/prepared-model-runtime.build.ts b/src/agents/prepared-model-runtime.build.ts index 302e604c0f8f..5e6d2b3baece 100644 --- a/src/agents/prepared-model-runtime.build.ts +++ b/src/agents/prepared-model-runtime.build.ts @@ -1,4 +1,5 @@ import { performance } from "node:perf_hooks"; +import { toStringifiedError } from "@openclaw/normalization-core/error-coercion"; import pLimit from "p-limit"; import { runAbortableTimeout } from "../node-host/with-timeout.js"; import { runTasksWithConcurrency } from "../utils/run-with-concurrency.js"; @@ -6,10 +7,7 @@ import { resolveUsableAgentCredentialModes } from "./agent-auth-credentials.js"; import { getPreparedRuntimeAuthMaterializations } from "./auth-profiles/runtime-materializations.js"; import type { ModelCatalogSnapshot } from "./model-catalog.types.js"; import { setPreparedModelRuntimeAuthMaterializations } from "./prepared-model-runtime-auth.js"; -import { - PreparedModelRuntimePublicationSupersededError, - toPreparedModelRuntimeError, -} from "./prepared-model-runtime.errors.js"; +import { PreparedModelRuntimePublicationSupersededError } from "./prepared-model-runtime.errors.js"; import { fingerprintPreparedRuntimeFacts, prepareAgentCatalogSource, @@ -344,7 +342,7 @@ async function buildSnapshotBatch( if (sourceBuild.hasError) { // A superseded owner is lifecycle control flow. Preserve any genuine in-flight sibling // failure so auth refresh diagnostics do not disappear behind that expected cancellation. - throw toPreparedModelRuntimeError( + throw toStringifiedError( sourceErrors.find( (error) => !(error instanceof PreparedModelRuntimePublicationSupersededError), ) ?? sourceBuild.firstError, diff --git a/src/agents/prepared-model-runtime.errors.ts b/src/agents/prepared-model-runtime.errors.ts index 01bf8abd0f36..88103b50abca 100644 --- a/src/agents/prepared-model-runtime.errors.ts +++ b/src/agents/prepared-model-runtime.errors.ts @@ -1,9 +1,3 @@ -import { toStringifiedError } from "@openclaw/normalization-core/error-coercion"; - export class PreparedModelRuntimeOwnerNotPublishedError extends Error {} export class PreparedModelRuntimePublicationSupersededError extends PreparedModelRuntimeOwnerNotPublishedError {} - -export function toPreparedModelRuntimeError(error: unknown): Error { - return toStringifiedError(error); -} diff --git a/src/agents/prepared-model-runtime.owner.ts b/src/agents/prepared-model-runtime.owner.ts index aa5f1095086e..d9ec8f5cd393 100644 --- a/src/agents/prepared-model-runtime.owner.ts +++ b/src/agents/prepared-model-runtime.owner.ts @@ -1,4 +1,5 @@ import path from "node:path"; +import { toStringifiedError } from "@openclaw/normalization-core/error-coercion"; import { hashRuntimeConfigValue } from "../config/runtime-snapshot.js"; import type { OpenClawConfig } from "../config/types.openclaw.js"; import { isReservedSystemAgentId } from "../system-agent/agent-id.js"; @@ -25,7 +26,6 @@ import { import { PreparedModelRuntimeOwnerNotPublishedError, PreparedModelRuntimePublicationSupersededError, - toPreparedModelRuntimeError, } from "./prepared-model-runtime.errors.js"; import type { PreparedModelRuntimeBuildStats, @@ -495,7 +495,7 @@ export async function publishPreparedModelRuntimeOwnerBatch(params: { } break; } catch (error) { - const refreshError = toPreparedModelRuntimeError(error); + const refreshError = toStringifiedError(error); const lostCandidate = attempt.some((candidate) => !candidate.isCurrent()); if ( !(refreshError instanceof PreparedModelRuntimePublicationSupersededError) || @@ -524,7 +524,7 @@ export async function publishPreparedModelRuntimeOwnerBatch(params: { candidate.owner.needsRefresh = false; } } catch (error) { - const refreshError = toPreparedModelRuntimeError(error); + const refreshError = toStringifiedError(error); for (const candidate of candidates) { if (!candidate.isCurrent()) { continue; @@ -602,7 +602,7 @@ export async function publishModelRuntimeSnapshot( owner.needsRefresh = false; return result.snapshot; } catch (error) { - const refreshError = toPreparedModelRuntimeError(error); + const refreshError = toStringifiedError(error); if (owner.generation === generation && owners.get(key) === owner) { owner.pending = undefined; owner.needsRefresh = true; diff --git a/src/agents/prepared-model-runtime.ts b/src/agents/prepared-model-runtime.ts index 5ff7c04a30a2..8fa1c31601b4 100644 --- a/src/agents/prepared-model-runtime.ts +++ b/src/agents/prepared-model-runtime.ts @@ -1,10 +1,10 @@ /** Lifecycle-owned auth/model discovery snapshots for agent runs. */ +import { toStringifiedError } from "@openclaw/normalization-core/error-coercion"; import type { OpenClawConfig } from "../config/types.openclaw.js"; import { createSubsystemLogger } from "../logging/subsystem.js"; import { isReservedSystemAgentId } from "../system-agent/agent-id.js"; import { registerRuntimeAuthProfileStoreMutationListener } from "./auth-profiles/runtime-snapshots.js"; import { registerPreparedRuntimeAuthMaterializationPublisher } from "./prepared-model-runtime-materializations.js"; -import { toPreparedModelRuntimeError } from "./prepared-model-runtime.errors.js"; import { PreparedModelRuntimeOwnerNotPublishedError, PreparedModelRuntimeOwnerRetention, @@ -513,7 +513,7 @@ export function rejectPendingPreparedModelRuntimeReplacement( return; } pendingModelRuntimeReplacement = undefined; - const replacementError = toPreparedModelRuntimeError(error); + const replacementError = toStringifiedError(error); replacement.reject(replacementError); notifyPreparedModelRuntimePublication({ phase: "failed", error: replacementError }); } @@ -633,7 +633,7 @@ export function refreshPreparedModelRuntimeSnapshots( } }, (error: unknown) => { - const refreshError = toPreparedModelRuntimeError(error); + const refreshError = toStringifiedError(error); if (requestEpoch === refreshRequestEpoch) { // Candidate and queued auth builds may finish independently. A failed transaction must // leave no owner from its partially published generation request-visible. @@ -739,7 +739,7 @@ function invalidateForAuthMutation(event: AuthMutationEvent): void { if (error instanceof PreparedModelRuntimePublicationSupersededError) { return; } - const refreshError = toPreparedModelRuntimeError(error); + const refreshError = toStringifiedError(error); notifyPreparedModelRuntimePublication({ phase: "failed", error: refreshError }); log.warn(`auth-triggered model runtime refresh failed: ${String(refreshError)}`); }); diff --git a/src/agents/run-cleanup-timeout.ts b/src/agents/run-cleanup-timeout.ts index e8937b1872fa..41dc2898afb9 100644 --- a/src/agents/run-cleanup-timeout.ts +++ b/src/agents/run-cleanup-timeout.ts @@ -3,10 +3,12 @@ * * Bounds cleanup steps so run completion cannot hang forever while preserving late-failure diagnostics. */ -import { resolveOptionalIntegerOption } from "@openclaw/normalization-core/number-coercion"; +import { + parseStrictPositiveInteger, + resolveOptionalIntegerOption, +} from "@openclaw/normalization-core/number-coercion"; import { truncateUtf16Safe } from "@openclaw/normalization-core/utf16-slice"; import { formatErrorMessage } from "../infra/errors.js"; -import { parseStrictPositiveInteger } from "../infra/parse-finite-number.js"; // Cleanup steps must not block run completion forever. This module bounds each // cleanup step and logs enough context to debug late failures. diff --git a/src/agents/sandbox/fs-bridge-stat-parse.ts b/src/agents/sandbox/fs-bridge-stat-parse.ts index 792de5820837..364553b9ea54 100644 --- a/src/agents/sandbox/fs-bridge-stat-parse.ts +++ b/src/agents/sandbox/fs-bridge-stat-parse.ts @@ -3,8 +3,10 @@ * * Handles GNU/BSD size and mtime formats returned through backend shell commands. */ -import { parseStrictNonNegativeInteger } from "../../infra/parse-finite-number.js"; -import { asDateTimestampMs } from "../../shared/number-coercion.js"; +import { + asDateTimestampMs, + parseStrictNonNegativeInteger, +} from "@openclaw/normalization-core/number-coercion"; export function hasMultipleHardlinks(raw: string): boolean { const linkCount = parseStrictNonNegativeInteger(raw); diff --git a/src/agents/sandbox/prune.ts b/src/agents/sandbox/prune.ts index 86b45bef65ad..b0276398b93e 100644 --- a/src/agents/sandbox/prune.ts +++ b/src/agents/sandbox/prune.ts @@ -1,3 +1,4 @@ +import { asDateTimestampMs } from "@openclaw/normalization-core/number-coercion"; /** * Sandbox registry pruning. * @@ -5,7 +6,6 @@ */ import { getRuntimeConfig } from "../../config/config.js"; import { defaultRuntime } from "../../runtime.js"; -import { asDateTimestampMs } from "../../shared/number-coercion.js"; import { getSandboxBackendManager } from "./backend.js"; import { stopCachedBrowserBridgesForContainer } from "./browser-bridges.js"; import { dockerSandboxBackendManager } from "./docker-backend.js"; diff --git a/src/agents/session-suspension.test.ts b/src/agents/session-suspension.test.ts index ca0d8c3fddbb..7f258c4a04ad 100644 --- a/src/agents/session-suspension.test.ts +++ b/src/agents/session-suspension.test.ts @@ -1,10 +1,10 @@ +import { MAX_TIMER_TIMEOUT_MS } from "@openclaw/normalization-core/number-coercion"; // Verifies quota suspension records recovery state without blocking shared work. import { afterEach, describe, expect, it, vi } from "vitest"; import type { OpenClawConfig } from "../config/types.openclaw.js"; import { enqueueCommandInLane, getCommandLaneSnapshot } from "../process/command-queue.js"; import { resetCommandQueueStateForTest } from "../process/command-queue.test-support.js"; import { CommandLane } from "../process/lanes.js"; -import { MAX_TIMER_TIMEOUT_MS } from "../shared/number-coercion.js"; const sessionAccessorMocks = vi.hoisted(() => ({ patchSessionEntryCore: vi.fn(), diff --git a/src/agents/session-suspension.ts b/src/agents/session-suspension.ts index eedebdc497ce..08fa419e73f5 100644 --- a/src/agents/session-suspension.ts +++ b/src/agents/session-suspension.ts @@ -4,15 +4,15 @@ * Records quota/manual/circuit suspensions for diagnostics and recovery flows. */ import { AsyncLocalStorage } from "node:async_hooks"; +import { + resolveExpiresAtMsFromDurationMs, + resolveTimerTimeoutMs, +} from "@openclaw/normalization-core/number-coercion"; import { patchSessionEntryCore } from "../config/sessions/session-accessor.js"; import type { QuotaSuspension } from "../config/sessions/types.js"; import type { OpenClawConfig } from "../config/types.openclaw.js"; import { createSubsystemLogger } from "../logging/subsystem.js"; import { resolveGlobalSingleton } from "../shared/global-singleton.js"; -import { - resolveExpiresAtMsFromDurationMs, - resolveTimerTimeoutMs, -} from "../shared/number-coercion.js"; import { resolveRegisteredAgentIdForDir } from "./agent-dir-registry.js"; import { resolveStoredSessionKeyForSessionId } from "./command/session.js"; import type { FailoverReason } from "./failover/signal.js"; diff --git a/src/agents/sessions/http-dispatcher.ts b/src/agents/sessions/http-dispatcher.ts index a0945f16ac2d..026ae8d43db6 100644 --- a/src/agents/sessions/http-dispatcher.ts +++ b/src/agents/sessions/http-dispatcher.ts @@ -3,7 +3,7 @@ * * Parses idle-timeout values shared by server and config surfaces. */ -import { parseStrictNonNegativeInteger } from "../../infra/parse-finite-number.js"; +import { parseStrictNonNegativeInteger } from "@openclaw/normalization-core/number-coercion"; export const DEFAULT_HTTP_IDLE_TIMEOUT_MS = 300_000; diff --git a/src/agents/subagents/registry/subagent-run-timeout.test.ts b/src/agents/subagents/registry/subagent-run-timeout.test.ts index 68fc8c23e38c..ca05515e5b3d 100644 --- a/src/agents/subagents/registry/subagent-run-timeout.test.ts +++ b/src/agents/subagents/registry/subagent-run-timeout.test.ts @@ -1,7 +1,7 @@ +import { MAX_TIMER_TIMEOUT_MS } from "@openclaw/normalization-core/number-coercion"; // Subagent run timeout tests keep semantic deadlines separate from the maximum // delay that Node timers can safely schedule. import { describe, expect, it } from "vitest"; -import { MAX_TIMER_TIMEOUT_MS } from "../../../shared/number-coercion.js"; import { resolveSubagentRunDeadlineMs, resolveSubagentRunDurationMs, diff --git a/src/agents/subagents/registry/subagent-run-timeout.ts b/src/agents/subagents/registry/subagent-run-timeout.ts index 9e48c382e65a..d750e29cbb3a 100644 --- a/src/agents/subagents/registry/subagent-run-timeout.ts +++ b/src/agents/subagents/registry/subagent-run-timeout.ts @@ -6,7 +6,7 @@ import { asDateTimestampMs, finiteSecondsToTimerSafeMilliseconds, -} from "../../../shared/number-coercion.js"; +} from "@openclaw/normalization-core/number-coercion"; import type { SubagentRunRecord } from "./subagent-registry.types.js"; type SubagentRunDeadlineRecord = Pick< diff --git a/src/agents/subagents/spawn/subagent-depth.ts b/src/agents/subagents/spawn/subagent-depth.ts index 0e1b4eb95ca4..80f82908e85d 100644 --- a/src/agents/subagents/spawn/subagent-depth.ts +++ b/src/agents/subagents/spawn/subagent-depth.ts @@ -1,3 +1,4 @@ +import { parseStrictNonNegativeInteger } from "@openclaw/normalization-core/number-coercion"; /** * Subagent spawn-depth lookup helpers. * @@ -7,7 +8,6 @@ import { normalizeOptionalString } from "@openclaw/normalization-core/string-coe import { resolveSessionStorePathCore } from "../../../config/sessions/paths.js"; import { listSessionEntriesReadOnly } from "../../../config/sessions/session-accessor.js"; import type { OpenClawConfig } from "../../../config/types.openclaw.js"; -import { parseStrictNonNegativeInteger } from "../../../infra/parse-finite-number.js"; import { getSubagentDepth, parseAgentSessionKey } from "../../../sessions/session-key-utils.js"; import { resolveDefaultAgentId } from "../../agent-scope.js"; diff --git a/src/agents/tool-display-common.ts b/src/agents/tool-display-common.ts index b43aac41ff28..4bbbe4d304eb 100644 --- a/src/agents/tool-display-common.ts +++ b/src/agents/tool-display-common.ts @@ -1,3 +1,4 @@ +import { parseStrictFiniteNumber } from "@openclaw/normalization-core/number-coercion"; /** * Shared compact tool-call display helpers. * Redacts and summarizes arguments into short labels/details for chat and UI @@ -9,7 +10,6 @@ import { normalizeOptionalString, } from "@openclaw/normalization-core/string-coerce"; import { sliceUtf16Safe } from "@openclaw/normalization-core/utf16-slice"; -import { parseStrictFiniteNumber } from "../infra/parse-finite-number.js"; import { redactToolPayloadText } from "../logging/redact.js"; import { resolveExecDetail, type ToolDetailMode } from "./tool-display-exec.js"; diff --git a/src/agents/tools/image-tool.ts b/src/agents/tools/image-tool.ts index d389ab39a366..6cc6edfea429 100644 --- a/src/agents/tools/image-tool.ts +++ b/src/agents/tools/image-tool.ts @@ -1,6 +1,7 @@ import { resolve, isAbsolute } from "node:path"; import { Type } from "typebox"; import { findCapabilityProviderById } from "../../../packages/media-generation-core/src/capability-model-ref.js"; +import { normalizeMediaProviderId } from "../../../packages/media-understanding-common/src/provider-id.js"; import type { OpenClawConfig } from "../../config/types.openclaw.js"; import type { MediaUnderstandingModelConfig } from "../../config/types.tools.js"; import { @@ -9,7 +10,6 @@ import { resolveDefaultMediaModel, } from "../../media-understanding/defaults.js"; import { matchesMediaEntryCapability } from "../../media-understanding/entry-capabilities.js"; -import { normalizeMediaProviderId } from "../../media-understanding/provider-id.js"; import { buildMediaUnderstandingRegistry as buildProviderRegistry, getMediaUnderstandingProvider, diff --git a/src/auto-reply/reply/commands-context-report.ts b/src/auto-reply/reply/commands-context-report.ts index 80f1625e43dc..237be5df53af 100644 --- a/src/auto-reply/reply/commands-context-report.ts +++ b/src/auto-reply/reply/commands-context-report.ts @@ -1,4 +1,5 @@ // Builds structured context reports for context command responses. +import { estimateTokensFromChars } from "@openclaw/normalization-core/cjk-chars"; import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce"; import { resolveSessionAgentIds } from "../../agents/agent-scope.js"; import { analyzeBootstrapBudget } from "../../agents/bootstrap-budget.js"; @@ -20,7 +21,6 @@ import { type SessionSystemPromptReport, } from "../../config/sessions/types.js"; import { readSessionMessagesAsync } from "../../gateway/session-transcript-readers.js"; -import { estimateTokensFromChars } from "../../utils/cjk-chars.js"; import type { ReplyPayload } from "../types.js"; import type { HandleCommandsParams } from "./commands-types.js"; import { renderContextTreemapPng } from "./context-treemap.js"; diff --git a/src/auto-reply/reply/commands-session.ts b/src/auto-reply/reply/commands-session.ts index 8f6df44fc662..83663088419f 100644 --- a/src/auto-reply/reply/commands-session.ts +++ b/src/auto-reply/reply/commands-session.ts @@ -1,5 +1,7 @@ // Implements session commands for list, show, fork, reset, and routing state. import { + asDateTimestampMs, + resolveExpiresAtMsFromDurationMs, resolveNonNegativeIntegerOption, resolveOptionalIntegerOption, timestampMsToIsoString, @@ -35,10 +37,6 @@ import { import { scheduleGatewaySigusr1Restart, triggerOpenClawRestart } from "../../infra/restart.js"; import { loadCostUsageSummary, loadSessionCostSummary } from "../../infra/session-cost-usage.js"; import { DEFAULT_AGENT_ID, isUnscopedSessionKeySentinel } from "../../routing/session-key.js"; -import { - asDateTimestampMs, - resolveExpiresAtMsFromDurationMs, -} from "../../shared/number-coercion.js"; import { formatTokenCount, formatUsd } from "../../utils/usage-format.js"; import { parseActivationCommand } from "../group-activation.js"; import { parseSendPolicyCommand } from "../send-policy.js"; diff --git a/src/auto-reply/reply/context-treemap.ts b/src/auto-reply/reply/context-treemap.ts index b45c4b49309a..ff0bff4557d0 100644 --- a/src/auto-reply/reply/context-treemap.ts +++ b/src/auto-reply/reply/context-treemap.ts @@ -4,9 +4,9 @@ import { writeFile } from "node:fs/promises"; import path from "node:path"; import zlib from "node:zlib"; import { expectDefined } from "@openclaw/normalization-core"; +import { estimateTokensFromChars } from "@openclaw/normalization-core/cjk-chars"; import type { SessionSystemPromptReport } from "../../config/sessions/types.js"; import { resolvePreferredOpenClawTmpDir } from "../../infra/tmp-openclaw-dir.js"; -import { estimateTokensFromChars } from "../../utils/cjk-chars.js"; /** PNG treemap renderer for visualizing prompt context size by section. */ type Rect = { diff --git a/src/auto-reply/reply/directive-handling.auth.test.ts b/src/auto-reply/reply/directive-handling.auth.test.ts index b8cc40e70c4a..7abf46c8a6bf 100644 --- a/src/auto-reply/reply/directive-handling.auth.test.ts +++ b/src/auto-reply/reply/directive-handling.auth.test.ts @@ -1,8 +1,8 @@ +import { MAX_DATE_TIMESTAMP_MS } from "@openclaw/normalization-core/number-coercion"; // Tests auth profile directive handling and provider override selection. import { beforeEach, describe, expect, it, vi } from "vitest"; import type { AuthProfileStore } from "../../agents/auth-profiles.js"; import type { OpenClawConfig } from "../../config/config.js"; -import { MAX_DATE_TIMESTAMP_MS } from "../../shared/number-coercion.js"; let mockStore: AuthProfileStore; let mockOrder: string[]; diff --git a/src/auto-reply/reply/directive-handling.auth.ts b/src/auto-reply/reply/directive-handling.auth.ts index e4f075327630..9fe3c8c39847 100644 --- a/src/auto-reply/reply/directive-handling.auth.ts +++ b/src/auto-reply/reply/directive-handling.auth.ts @@ -1,3 +1,4 @@ +import { asDateTimestampMs } from "@openclaw/normalization-core/number-coercion"; // Handles auth directives that choose provider auth profiles for a reply. import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce"; import { formatRemainingShort } from "../../agents/auth-health.js"; @@ -18,7 +19,6 @@ import { findNormalizedProviderValue, normalizeProviderId } from "../../agents/m import type { OpenClawConfig } from "../../config/types.openclaw.js"; import { coerceSecretRef } from "../../config/types.secrets.js"; import { maskApiKey } from "../../security/secret-mask.js"; -import { asDateTimestampMs } from "../../shared/number-coercion.js"; import { shortenHomePath } from "../../utils.js"; /** Controls how much auth provenance is shown in directive status output. */ diff --git a/src/auto-reply/reply/get-reply-directives.target-session.test.ts b/src/auto-reply/reply/get-reply-directives.target-session.test.ts index 1e2c80710ce1..a1e1c0b4ba0a 100644 --- a/src/auto-reply/reply/get-reply-directives.target-session.test.ts +++ b/src/auto-reply/reply/get-reply-directives.target-session.test.ts @@ -188,7 +188,7 @@ vi.mock("../../agents/thinking-runtime.js", () => ({ })); vi.mock("../../routing/session-key.js", () => ({ - normalizeAgentId: (value: string) => value, + normalizeAgentId: vi.fn((value: string) => value), })); vi.mock("../commands-text-routing.js", () => ({ diff --git a/src/auto-reply/reply/get-reply-run.media-only.test.ts b/src/auto-reply/reply/get-reply-run.media-only.test.ts index 148f8a38cbbd..99d103095a03 100644 --- a/src/auto-reply/reply/get-reply-run.media-only.test.ts +++ b/src/auto-reply/reply/get-reply-run.media-only.test.ts @@ -98,7 +98,7 @@ vi.mock(import("../../routing/session-key.js"), async (importOriginal) => { return { ...actual, normalizeMainKey: () => "main", - normalizeAgentId: (id: string | undefined | null) => id ?? "default", + normalizeAgentId: vi.fn((id: string | undefined | null) => id ?? "default"), }; }); diff --git a/src/auto-reply/reply/private-message-tool-final.test.ts b/src/auto-reply/reply/private-message-tool-final.test.ts index 4af41ac151fa..db5486a016b6 100644 --- a/src/auto-reply/reply/private-message-tool-final.test.ts +++ b/src/auto-reply/reply/private-message-tool-final.test.ts @@ -1,6 +1,6 @@ // Tests private message-tool final delivery and visibility suppression. +import { estimateStringChars } from "@openclaw/normalization-core/cjk-chars"; import { describe, expect, it } from "vitest"; -import { estimateStringChars } from "../../utils/cjk-chars.js"; import { classifyPrivateMessageToolFinal } from "./private-message-tool-final.js"; const base = { diff --git a/src/auto-reply/reply/private-message-tool-final.ts b/src/auto-reply/reply/private-message-tool-final.ts index 5d322653a046..d57002719db5 100644 --- a/src/auto-reply/reply/private-message-tool-final.ts +++ b/src/auto-reply/reply/private-message-tool-final.ts @@ -1,6 +1,6 @@ /** Detects and logs long private finals when message-tool-only delivery was expected. */ +import { estimateStringChars } from "@openclaw/normalization-core/cjk-chars"; import { createSubsystemLogger } from "../../logging/subsystem.js"; -import { estimateStringChars } from "../../utils/cjk-chars.js"; import type { SourceReplyDeliveryMode } from "../get-reply-options.types.js"; import { isSilentReplyText } from "../tokens.js"; diff --git a/src/auto-reply/reply/queue/directive.ts b/src/auto-reply/reply/queue/directive.ts index 2758e8395151..42804b5fd44a 100644 --- a/src/auto-reply/reply/queue/directive.ts +++ b/src/auto-reply/reply/queue/directive.ts @@ -1,8 +1,8 @@ +import { parseStrictPositiveInteger } from "@openclaw/normalization-core/number-coercion"; // Converts queue directives into normalized queue settings. import { normalizeOptionalLowercaseString } from "@openclaw/normalization-core/string-coerce"; import type { QueueMode } from "../../../../packages/gateway-protocol/src/schema/logs-chat.js"; import { parseDurationMs } from "../../../cli/parse-duration.js"; -import { parseStrictPositiveInteger } from "../../../infra/parse-finite-number.js"; import { skipDirectiveArgPrefix, takeDirectiveToken } from "../directive-parsing.js"; import { normalizeQueueDropPolicy, normalizeQueueMode } from "./normalize.js"; import type { QueueDropPolicy } from "./types.js"; diff --git a/src/auto-reply/reply/reply-run-finalization-lease.ts b/src/auto-reply/reply/reply-run-finalization-lease.ts index a0fdeb4609cd..083040d5bdbf 100644 --- a/src/auto-reply/reply/reply-run-finalization-lease.ts +++ b/src/auto-reply/reply/reply-run-finalization-lease.ts @@ -1,4 +1,4 @@ -import { resolveTimerTimeoutMs } from "../../shared/number-coercion.js"; +import { resolveTimerTimeoutMs } from "@openclaw/normalization-core/number-coercion"; const REPLY_RUN_FINALIZATION_SETTLE_TIMEOUT_MS = 60_000; export type ReplyOperationStaleReason = diff --git a/src/auto-reply/reply/reply-run-registry.registry.ts b/src/auto-reply/reply/reply-run-registry.registry.ts index 8d9445d0bb24..d5df3bfee644 100644 --- a/src/auto-reply/reply/reply-run-registry.registry.ts +++ b/src/auto-reply/reply/reply-run-registry.registry.ts @@ -1,10 +1,10 @@ +import { resolveTimerTimeoutMs } from "@openclaw/normalization-core/number-coercion"; // Tracks active reply runs so stop, queue, and status commands can coordinate. import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce"; import { isAgentEventLifecycleGenerationCurrent, registerAgentEventLifecycleRotationHandler, } from "../../infra/agent-events.js"; -import { resolveTimerTimeoutMs } from "../../shared/number-coercion.js"; import * as replyRunSettle from "./reply-run-finalization-lease.js"; import { replyMessageInjectionTargetOperation, diff --git a/src/auto-reply/reply/reply-run-registry.state.ts b/src/auto-reply/reply/reply-run-registry.state.ts index 752c70b67070..e80013630ca6 100644 --- a/src/auto-reply/reply/reply-run-registry.state.ts +++ b/src/auto-reply/reply/reply-run-registry.state.ts @@ -1,3 +1,4 @@ +import { resolveTimerTimeoutMs } from "@openclaw/normalization-core/number-coercion"; import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce"; import { createAbortError } from "../../infra/abort-signal.js"; import { @@ -7,7 +8,6 @@ import { } from "../../logging/diagnostic-run-activity.js"; import { createDeferredCore } from "../../shared/deferred.js"; import { resolveGlobalSingleton } from "../../shared/global-singleton.js"; -import { resolveTimerTimeoutMs } from "../../shared/number-coercion.js"; import type { ReplyFollowupAdmissionBarrierTimeoutPolicy } from "./reply-dispatcher.types.js"; import type { ReplyOperationStaleReason } from "./reply-run-finalization-lease.js"; import { diff --git a/src/auto-reply/reply/reply-run-registry.test.ts b/src/auto-reply/reply/reply-run-registry.test.ts index 74b65c50a979..3dcb24728048 100644 --- a/src/auto-reply/reply/reply-run-registry.test.ts +++ b/src/auto-reply/reply/reply-run-registry.test.ts @@ -1,3 +1,4 @@ +import { MAX_TIMER_TIMEOUT_MS } from "@openclaw/normalization-core/number-coercion"; // Tests active reply run registry add, lookup, and cleanup behavior. import { afterEach, describe, expect, it, vi } from "vitest"; import { createDeferred } from "../../../test/helpers/promise.js"; @@ -12,7 +13,6 @@ import { markDiagnosticModelStartedForTest } from "../../logging/diagnostic-run- import { diagnosticLogger } from "../../logging/diagnostic-runtime.js"; import { enqueueCommandInLane, setCommandLaneConcurrency } from "../../process/command-queue.js"; import { resetCommandQueueStateForTest } from "../../process/command-queue.test-support.js"; -import { MAX_TIMER_TIMEOUT_MS } from "../../shared/number-coercion.js"; import { beginReplyOperationFinalizationWork } from "./reply-run-finalization-lease.js"; import { abortActiveReplyRuns, diff --git a/src/auto-reply/reply/session-updates.test.ts b/src/auto-reply/reply/session-updates.test.ts index 5802869b9558..e85b7ad0294c 100644 --- a/src/auto-reply/reply/session-updates.test.ts +++ b/src/auto-reply/reply/session-updates.test.ts @@ -75,7 +75,7 @@ vi.mock("../../config/sessions/session-accessor.js", () => ({ })); vi.mock("../../routing/session-key.js", () => ({ - normalizeAgentId: (id: string) => id, + normalizeAgentId: vi.fn((id: string) => id), normalizeMainKey: (key?: string) => key ?? "main", resolveAgentIdFromSessionKey: resolveAgentIdFromSessionKeyMock, })); diff --git a/src/auto-reply/reply/typing-persistence.test.ts b/src/auto-reply/reply/typing-persistence.test.ts index 3d1eadcc9136..d1273f4aa64c 100644 --- a/src/auto-reply/reply/typing-persistence.test.ts +++ b/src/auto-reply/reply/typing-persistence.test.ts @@ -1,6 +1,6 @@ +import { MAX_TIMER_TIMEOUT_MS } from "@openclaw/normalization-core/number-coercion"; // Tests typing mode persistence across session updates and reply turns. import { describe, it, expect, vi, beforeEach, afterEach, type Mock } from "vitest"; -import { MAX_TIMER_TIMEOUT_MS } from "../../shared/number-coercion.js"; import { createTypingController } from "./typing.js"; describe("typing persistence bug fix", () => { diff --git a/src/channels/draft-stream-loop.test.ts b/src/channels/draft-stream-loop.test.ts index 2a4f83bb6de4..ff4a70563c0a 100644 --- a/src/channels/draft-stream-loop.test.ts +++ b/src/channels/draft-stream-loop.test.ts @@ -1,7 +1,7 @@ // Draft stream loop tests cover incremental draft updates while channel replies stream. import { setImmediate as nextMacrotask } from "node:timers/promises"; +import { MAX_TIMER_TIMEOUT_MS } from "@openclaw/normalization-core/number-coercion"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { MAX_TIMER_TIMEOUT_MS } from "../shared/number-coercion.js"; import { createDraftStreamLoop } from "./draft-stream-loop.js"; const flushMicrotasks = async () => { diff --git a/src/channels/draft-stream-loop.ts b/src/channels/draft-stream-loop.ts index adb2cd31fcc1..27c2c61e17c2 100644 --- a/src/channels/draft-stream-loop.ts +++ b/src/channels/draft-stream-loop.ts @@ -3,7 +3,7 @@ * * Sends the latest pending draft text with single-flight edit semantics. */ -import { resolveTimerTimeoutMs } from "../shared/number-coercion.js"; +import { resolveTimerTimeoutMs } from "@openclaw/normalization-core/number-coercion"; /** Throttled draft-stream sender used by channels that edit in-progress replies. */ export type DraftStreamLoop = { diff --git a/src/channels/plugins/chat-target-prefixes.ts b/src/channels/plugins/chat-target-prefixes.ts index db1677b3b639..bda1311e0e8b 100644 --- a/src/channels/plugins/chat-target-prefixes.ts +++ b/src/channels/plugins/chat-target-prefixes.ts @@ -1,3 +1,4 @@ +import { parseStrictInteger } from "@openclaw/normalization-core/number-coercion"; /** * Chat target prefix parsers. * @@ -8,7 +9,6 @@ import { normalizeOptionalString, } from "@openclaw/normalization-core/string-coerce"; import { normalizeStringEntries } from "@openclaw/normalization-core/string-normalization"; -import { parseStrictInteger } from "../../infra/parse-finite-number.js"; import { resolveAllowlistMatchByCandidates } from "../allowlist-match.js"; /** diff --git a/src/channels/plugins/setup-contract.ts b/src/channels/plugins/setup-contract.ts index d173a63d5b39..7ebd42eb9e12 100644 --- a/src/channels/plugins/setup-contract.ts +++ b/src/channels/plugins/setup-contract.ts @@ -1,7 +1,7 @@ +import { parseStrictNonNegativeInteger } from "@openclaw/normalization-core/number-coercion"; import { isRecord } from "@openclaw/normalization-core/record-coerce"; import { Option } from "commander"; import type { OpenClawConfig } from "../../config/types.openclaw.js"; -import { parseStrictNonNegativeInteger } from "../../infra/parse-finite-number.js"; import type { RuntimeEnv } from "../../runtime.js"; import type { ChannelSetupAdapter } from "./setup-adapter.types.js"; import type { ChannelSetupInput } from "./setup-input.js"; diff --git a/src/channels/thread-bindings-policy.test.ts b/src/channels/thread-bindings-policy.test.ts index abd183acb1c2..1b929c42fef7 100644 --- a/src/channels/thread-bindings-policy.test.ts +++ b/src/channels/thread-bindings-policy.test.ts @@ -1,7 +1,7 @@ +import { MAX_DATE_TIMESTAMP_MS } from "@openclaw/normalization-core/number-coercion"; // Thread binding policy tests cover how channel thread bindings are created and reused. import { beforeEach, describe, expect, it } from "vitest"; import { setActivePluginRegistry } from "../plugins/runtime.js"; -import { MAX_DATE_TIMESTAMP_MS } from "../shared/number-coercion.js"; import { createChannelTestPluginBase, createTestRegistry } from "../test-utils/channel-plugins.js"; import { requiresNativeThreadContextForThreadHere, diff --git a/src/channels/thread-bindings-policy.ts b/src/channels/thread-bindings-policy.ts index 0271d38658fc..c62b4f36d9be 100644 --- a/src/channels/thread-bindings-policy.ts +++ b/src/channels/thread-bindings-policy.ts @@ -10,6 +10,7 @@ import { resolveThreadBindingLifecycle as resolveSharedThreadBindingLifecycle, type ThreadBindingLifecycleRecord, } from "../shared/thread-binding-lifecycle.js"; +import { asBoolean } from "../utils/boolean.js"; import { getLoadedChannelPlugin } from "./plugins/index.js"; import { resolveBundledChannelThreadBindingDefaultPlacement } from "./plugins/thread-binding-api.js"; @@ -84,13 +85,6 @@ function resolveDefaultTopLevelPlacement(channel: string): "current" | "child" { ); } -function normalizeBoolean(value: unknown): boolean | undefined { - if (typeof value !== "boolean") { - return undefined; - } - return value; -} - function normalizeThreadBindingHours(raw: unknown): number | undefined { return asNonNegativeFiniteNumber(raw); } @@ -141,9 +135,7 @@ export function resolveThreadBindingsEnabled(params: { channelEnabledRaw: unknown; sessionEnabledRaw: unknown; }): boolean { - return ( - normalizeBoolean(params.channelEnabledRaw) ?? normalizeBoolean(params.sessionEnabledRaw) ?? true - ); + return asBoolean(params.channelEnabledRaw) ?? asBoolean(params.sessionEnabledRaw) ?? true; } function resolveChannelThreadBindings(params: { @@ -180,14 +172,14 @@ export function resolveThreadBindingSpawnPolicy(params: { const accountId = normalizeAccountId(params.accountId); const { root, account } = resolveChannelThreadBindings({ cfg: params.cfg, channel, accountId }); const enabled = - normalizeBoolean(account?.enabled) ?? - normalizeBoolean(root?.enabled) ?? - normalizeBoolean(params.cfg.session?.threadBindings?.enabled) ?? + asBoolean(account?.enabled) ?? + asBoolean(root?.enabled) ?? + asBoolean(params.cfg.session?.threadBindings?.enabled) ?? true; const spawnEnabledRaw = - normalizeBoolean(account?.spawnSessions) ?? - normalizeBoolean(root?.spawnSessions) ?? - normalizeBoolean(params.cfg.session?.threadBindings?.spawnSessions); + asBoolean(account?.spawnSessions) ?? + asBoolean(root?.spawnSessions) ?? + asBoolean(params.cfg.session?.threadBindings?.spawnSessions); const spawnEnabled = spawnEnabledRaw ?? true; const defaultSpawnContext = normalizeSpawnContext(account?.defaultSpawnContext) ?? diff --git a/src/channels/typing.test.ts b/src/channels/typing.test.ts index 58797915bfec..ac66bbdaa780 100644 --- a/src/channels/typing.test.ts +++ b/src/channels/typing.test.ts @@ -1,6 +1,6 @@ +import { MAX_TIMER_TIMEOUT_MS } from "@openclaw/normalization-core/number-coercion"; // Typing tests cover typing indicator start, update, and cleanup behavior. import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { MAX_TIMER_TIMEOUT_MS } from "../shared/number-coercion.js"; import { createTypingCallbacks } from "./typing.js"; type TypingCallbackOverrides = Partial[0]>; diff --git a/src/cli/argv.ts b/src/cli/argv.ts index c3e61a4f618e..91314724f873 100644 --- a/src/cli/argv.ts +++ b/src/cli/argv.ts @@ -1,3 +1,4 @@ +import { parseStrictPositiveInteger } from "@openclaw/normalization-core/number-coercion"; // Low-level CLI argv helpers for root options, help/version detection, and command paths. import { isExperimentalClawsEnabled } from "../claws/experimental.js"; import { isBunRuntime, isNodeRuntime } from "../daemon/runtime-binary.js"; @@ -7,7 +8,6 @@ import { getRootOptionAwareCommandPath, isValueToken, } from "../infra/cli-root-options.js"; -import { parseStrictPositiveInteger } from "../infra/parse-finite-number.js"; import { CORE_CLI_COMMAND_DESCRIPTORS } from "./program/core-command-descriptors.js"; import { SUB_CLI_DESCRIPTORS } from "./program/subcli-descriptors.js"; diff --git a/src/cli/attach-cli.ts b/src/cli/attach-cli.ts index c8edb9dd5747..4c50c3c0f5be 100644 --- a/src/cli/attach-cli.ts +++ b/src/cli/attach-cli.ts @@ -2,9 +2,9 @@ import { spawn } from "node:child_process"; import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; import { constants as osConstants, tmpdir } from "node:os"; import { join } from "node:path"; +import { parseStrictPositiveInteger } from "@openclaw/normalization-core/number-coercion"; import type { Command } from "commander"; import { getRuntimeConfig } from "../config/io.js"; -import { parseStrictPositiveInteger } from "../infra/parse-finite-number.js"; import { defaultRuntime } from "../runtime.js"; import { callSessionTargetGateway, diff --git a/src/cli/capability-cli/shared.ts b/src/cli/capability-cli/shared.ts index db1690f6f405..45572467e773 100644 --- a/src/cli/capability-cli/shared.ts +++ b/src/cli/capability-cli/shared.ts @@ -1,3 +1,7 @@ +import { + parseStrictFiniteNumber, + parseStrictPositiveInteger, +} from "@openclaw/normalization-core/number-coercion"; import { resolveAgentDir, resolveDefaultAgentId } from "../../agents/agent-scope.js"; import { listProfilesForProvider, @@ -9,10 +13,6 @@ import { setRuntimeConfigSnapshot, } from "../../config/config.js"; import type { OpenClawConfig } from "../../config/types.openclaw.js"; -import { - parseStrictFiniteNumber, - parseStrictPositiveInteger, -} from "../../infra/parse-finite-number.js"; import { writeRuntimeJson, defaultRuntime, type RuntimeEnv } from "../../runtime.js"; import { getProviderEnvVars } from "../../secrets/provider-env-vars.js"; import { resolveCommandConfigWithSecrets } from "../command-config-resolution.js"; diff --git a/src/cli/config-cli-input.ts b/src/cli/config-cli-input.ts index 6b3520fcec10..519c253ea1f0 100644 --- a/src/cli/config-cli-input.ts +++ b/src/cli/config-cli-input.ts @@ -1,4 +1,5 @@ import { readByteStreamWithLimit } from "@openclaw/media-core/read-byte-stream-with-limit"; +import { parseStrictPositiveInteger } from "@openclaw/normalization-core/number-coercion"; import { isRecord as isPlainRecord } from "@openclaw/normalization-core/record-coerce"; import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce"; import { normalizeStringEntries } from "@openclaw/normalization-core/string-normalization"; @@ -12,7 +13,6 @@ import { } from "../config/types.secrets.js"; import { SecretProviderSchema } from "../config/zod-schema.core.js"; import { hasErrnoCode } from "../infra/errors.js"; -import { parseStrictPositiveInteger } from "../infra/parse-finite-number.js"; import { formatExecSecretRefIdValidationMessage, isValidFileSecretRefId, diff --git a/src/cli/cron-cli/register.cron-add.ts b/src/cli/cron-cli/register.cron-add.ts index 6a5b683fcbb8..0966c8a32764 100644 --- a/src/cli/cron-cli/register.cron-add.ts +++ b/src/cli/cron-cli/register.cron-add.ts @@ -1,4 +1,5 @@ // Cron status/list/add command registration and create-payload normalization. +import { parseStrictPositiveInteger } from "@openclaw/normalization-core/number-coercion"; import { normalizeLowercaseStringOrEmpty, normalizeOptionalString, @@ -11,7 +12,6 @@ import { sanitizeAgentId } from "../../routing/session-key.js"; import { defaultRuntime } from "../../runtime.js"; import type { GatewayRpcOpts } from "../gateway-rpc.js"; import { addGatewayClientOptions, callGatewayFromCli } from "../gateway-rpc.js"; -import { parseStrictPositiveIntOrUndefined } from "../program/helpers.js"; import { listCronJobsFromGateway } from "./list-jobs.js"; import { resolveCronCreateScheduleFromArgs } from "./schedule-options.js"; import { @@ -255,13 +255,11 @@ export function registerCronAddCommand(cron: Command) { }; } if (scriptPath) { - const scriptTimeoutSeconds = parseStrictPositiveIntOrUndefined( - opts.scriptTimeoutSeconds, - ); + const scriptTimeoutSeconds = parseStrictPositiveInteger(opts.scriptTimeoutSeconds); if (opts.scriptTimeoutSeconds !== undefined && scriptTimeoutSeconds === undefined) { throw new Error("Invalid --script-timeout-seconds (must be a positive integer)."); } - const scriptToolBudget = parseStrictPositiveIntOrUndefined(opts.scriptToolBudget); + const scriptToolBudget = parseStrictPositiveInteger(opts.scriptToolBudget); if (opts.scriptToolBudget !== undefined && scriptToolBudget === undefined) { throw new Error("Invalid --script-tool-budget (must be a positive integer)."); } @@ -273,7 +271,7 @@ export function registerCronAddCommand(cron: Command) { toolsAllow, }; } - const timeoutSeconds = parseStrictPositiveIntOrUndefined(opts.timeoutSeconds); + const timeoutSeconds = parseStrictPositiveInteger(opts.timeoutSeconds); if (opts.timeoutSeconds !== undefined && timeoutSeconds === undefined) { throw new Error("Invalid --timeout-seconds (must be a positive integer)."); } @@ -285,7 +283,7 @@ export function registerCronAddCommand(cron: Command) { ? opts.outputTimeoutSeconds : undefined); const noOutputTimeoutSeconds = - parseStrictPositiveIntOrUndefined(rawNoOutputTimeoutSeconds); + parseStrictPositiveInteger(rawNoOutputTimeoutSeconds); if ( rawNoOutputTimeoutSeconds !== undefined && noOutputTimeoutSeconds === undefined @@ -294,7 +292,7 @@ export function registerCronAddCommand(cron: Command) { "Invalid --no-output-timeout-seconds (must be a positive integer).", ); } - const outputMaxBytes = parseStrictPositiveIntOrUndefined(opts.outputMaxBytes); + const outputMaxBytes = parseStrictPositiveInteger(opts.outputMaxBytes); if (opts.outputMaxBytes !== undefined && outputMaxBytes === undefined) { throw new Error("Invalid --output-max-bytes (must be a positive integer)."); } diff --git a/src/cli/cron-cli/register.cron-edit-options.ts b/src/cli/cron-cli/register.cron-edit-options.ts index fa17a6de596a..6de48c3db2bb 100644 --- a/src/cli/cron-cli/register.cron-edit-options.ts +++ b/src/cli/cron-cli/register.cron-edit-options.ts @@ -1,6 +1,6 @@ +import { parseStrictPositiveInteger } from "@openclaw/normalization-core/number-coercion"; import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce"; import type { CronJob } from "../../cron/types.js"; -import { parseStrictPositiveInteger } from "../../infra/parse-finite-number.js"; import { parseCronCommandArgv, parseCronCommandEnv, diff --git a/src/cli/cron-cli/register.cron-edit.ts b/src/cli/cron-cli/register.cron-edit.ts index 2dfd86aba287..ba06d15201e4 100644 --- a/src/cli/cron-cli/register.cron-edit.ts +++ b/src/cli/cron-cli/register.cron-edit.ts @@ -1,3 +1,4 @@ +import { parseStrictPositiveInteger } from "@openclaw/normalization-core/number-coercion"; // Cron edit command registration and patch construction for existing jobs. import { normalizeOptionalLowercaseString, @@ -7,7 +8,6 @@ import type { Command } from "commander"; import { THINKING_LEVELS_HELP } from "../../auto-reply/thinking.shared.js"; import type { CronJob } from "../../cron/types.js"; import { danger } from "../../globals.js"; -import { parseStrictPositiveInteger } from "../../infra/parse-finite-number.js"; import { sanitizeAgentId } from "../../routing/session-key.js"; import { defaultRuntime } from "../../runtime.js"; import { diff --git a/src/cli/cron-cli/register.cron-scratch.ts b/src/cli/cron-cli/register.cron-scratch.ts index 8951e3316085..622798b6032a 100644 --- a/src/cli/cron-cli/register.cron-scratch.ts +++ b/src/cli/cron-cli/register.cron-scratch.ts @@ -1,6 +1,6 @@ +import { parseStrictNonNegativeInteger } from "@openclaw/normalization-core/number-coercion"; // Cron scratch CLI: private per-job prompt context reads and compare-and-swap writes. import type { Command } from "commander"; -import { parseStrictNonNegativeInteger } from "../../infra/parse-finite-number.js"; import { addGatewayClientOptions, callGatewayFromCli } from "../gateway-rpc.js"; import { handleCronCliError, printCronJson } from "./shared.js"; import { readCronScratchContent } from "./trigger-options.js"; diff --git a/src/cli/cron-cli/register.cron-simple.ts b/src/cli/cron-cli/register.cron-simple.ts index e339259c0d56..cce22319e47a 100644 --- a/src/cli/cron-cli/register.cron-simple.ts +++ b/src/cli/cron-cli/register.cron-simple.ts @@ -1,10 +1,10 @@ // Cron simple command registration: remove, toggle, show, runs, and run-now. import { + parseStrictPositiveInteger, resolvePositiveTimerTimeoutMs, resolveTimerTimeoutMs, } from "@openclaw/normalization-core/number-coercion"; import type { Command } from "commander"; -import { parseStrictPositiveInteger } from "../../infra/parse-finite-number.js"; import { defaultRuntime } from "../../runtime.js"; import { sleep } from "../../utils/sleep.js"; import type { GatewayRpcOpts } from "../gateway-rpc.js"; diff --git a/src/cli/daemon-cli/status.gather.ts b/src/cli/daemon-cli/status.gather.ts index 0015b9d4a566..c5990480ce4e 100644 --- a/src/cli/daemon-cli/status.gather.ts +++ b/src/cli/daemon-cli/status.gather.ts @@ -1,5 +1,6 @@ // Collects daemon status from service files, config snapshots, ports, probes, and plugin drift. import fs from "node:fs/promises"; +import { parseStrictPositiveInteger } from "@openclaw/normalization-core/number-coercion"; import { asNonArrayRecord } from "@openclaw/normalization-core/record-coerce"; import { uniqueStrings } from "@openclaw/normalization-core/string-normalization"; import JSON5 from "json5"; @@ -41,7 +42,6 @@ import { inspectBestEffortPrimaryTailnetIPv4, resolveBestEffortGatewayBindHostForDisplay, } from "../../infra/network-discovery-display.js"; -import { parseStrictPositiveInteger } from "../../infra/parse-finite-number.js"; import { formatPortDiagnostics } from "../../infra/ports-format.js"; import { inspectPortConnections, diff --git a/src/cli/directory-cli.ts b/src/cli/directory-cli.ts index dd76b3100ffc..92390900e81d 100644 --- a/src/cli/directory-cli.ts +++ b/src/cli/directory-cli.ts @@ -1,3 +1,4 @@ +import { parseStrictPositiveInteger } from "@openclaw/normalization-core/number-coercion"; // Directory CLI for chat-channel identity lookup: self, peers, groups, and group members. import { normalizeOptionalString, @@ -13,7 +14,6 @@ import { getRuntimeConfig, readConfigFileSnapshot, replaceConfigFile } from "../ import { applyPluginAutoEnable } from "../config/plugin-auto-enable.js"; import { danger } from "../globals.js"; import { resolveMessageChannelSelection } from "../infra/outbound/channel-selection.js"; -import { parseStrictPositiveInteger } from "../infra/parse-finite-number.js"; import { commitConfigWithPendingPluginInstalls } from "../plugins/install-record-commit.js"; import { defaultRuntime } from "../runtime.js"; import { formatHelpExamples } from "./help-format.js"; diff --git a/src/cli/fleet-cli/register.ts b/src/cli/fleet-cli/register.ts index b670d0e8c0f0..fb92a162bd48 100644 --- a/src/cli/fleet-cli/register.ts +++ b/src/cli/fleet-cli/register.ts @@ -1,6 +1,6 @@ +import { parseStrictFiniteNumber } from "@openclaw/normalization-core/number-coercion"; import { InvalidArgumentError, type Command } from "commander"; import { validateDiskSize } from "../../fleet/cell-profile.js"; -import { parseStrictFiniteNumber } from "../../infra/parse-finite-number.js"; import { createLazyImportLoader } from "../../shared/lazy-promise.js"; import { collectOption, parseStrictPositiveIntOption } from "../program/helpers.js"; diff --git a/src/cli/gateway-cli/register-restart-handoff.ts b/src/cli/gateway-cli/register-restart-handoff.ts index f739057831e8..2e17b1d94344 100644 --- a/src/cli/gateway-cli/register-restart-handoff.ts +++ b/src/cli/gateway-cli/register-restart-handoff.ts @@ -1,6 +1,6 @@ +import { parseStrictPositiveInteger } from "@openclaw/normalization-core/number-coercion"; // Hidden machine-facing gateway restart-handoff commands for external supervisors. import type { Command } from "commander"; -import { parseStrictPositiveInteger } from "../../infra/parse-finite-number.js"; import { createGatewayRestartHandoffCapabilities, GATEWAY_RESTART_HANDOFF_PROTOCOL, diff --git a/src/cli/gateway-cli/register.ts b/src/cli/gateway-cli/register.ts index 4643b0a9e85c..ca988f299806 100644 --- a/src/cli/gateway-cli/register.ts +++ b/src/cli/gateway-cli/register.ts @@ -1,10 +1,10 @@ // Commander registration for gateway status, health, diagnostics, discovery, and run commands. import { formatByteSize } from "@openclaw/normalization-core"; +import { parseStrictPositiveInteger } from "@openclaw/normalization-core/number-coercion"; import type { Command } from "commander"; import { formatDocsLink } from "../../../packages/terminal-core/src/links.js"; import { colorize, isRich, theme } from "../../../packages/terminal-core/src/theme.js"; import type { HealthSummary } from "../../commands/health.js"; -import { parseStrictPositiveInteger } from "../../infra/parse-finite-number.js"; import type { CostUsageSummary } from "../../infra/session-cost-usage.js"; import type { DiagnosticStabilityBundle, diff --git a/src/cli/gateway-cli/run.ts b/src/cli/gateway-cli/run.ts index 9b1bbeea4f28..464fb0ec4071 100644 --- a/src/cli/gateway-cli/run.ts +++ b/src/cli/gateway-cli/run.ts @@ -4,6 +4,7 @@ import { request as httpRequest } from "node:http"; import { request as httpsRequest } from "node:https"; import { TLSSocket } from "node:tls"; import { expectDefined } from "@openclaw/normalization-core"; +import { parseStrictPositiveInteger } from "@openclaw/normalization-core/number-coercion"; import { normalizeOptionalLowercaseString, normalizeOptionalString, @@ -52,7 +53,6 @@ import { findVerifiedGatewayListenerPidsOnPortSync, formatGatewayPidList, } from "../../infra/gateway-processes.js"; -import { parseStrictPositiveInteger } from "../../infra/parse-finite-number.js"; import type { RespawnSupervisor } from "../../infra/supervisor-markers.js"; import { normalizeFingerprint } from "../../infra/tls/fingerprint.js"; import { setConsoleSubsystemFilter, setConsoleTimestampPrefix } from "../../logging/console.js"; diff --git a/src/cli/gateway-port-option.ts b/src/cli/gateway-port-option.ts index 6e2bd3921d84..ab77e2935ee5 100644 --- a/src/cli/gateway-port-option.ts +++ b/src/cli/gateway-port-option.ts @@ -1,5 +1,5 @@ // Shared parser for CLI flags that select a local Gateway TCP port. -import { parseStrictPositiveInteger } from "../infra/parse-finite-number.js"; +import { parseStrictPositiveInteger } from "@openclaw/normalization-core/number-coercion"; const MAX_TCP_PORT = 65_535; diff --git a/src/cli/logs-cli.ts b/src/cli/logs-cli.ts index 70bfa59a93d8..493e64a84fa4 100644 --- a/src/cli/logs-cli.ts +++ b/src/cli/logs-cli.ts @@ -5,7 +5,10 @@ import { coerceErrorMessage as normalizeErrorMessage, toStringifiedError, } from "@openclaw/normalization-core/error-coercion"; -import { resolveIntegerOption } from "@openclaw/normalization-core/number-coercion"; +import { + parseStrictPositiveInteger, + resolveIntegerOption, +} from "@openclaw/normalization-core/number-coercion"; import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce"; import type { Command } from "commander"; import { @@ -26,7 +29,6 @@ import { projectGatewayConnectionDetailsForDiagnostics } from "../gateway/connec import { isLoopbackHost } from "../gateway/net.js"; import { computeBackoff } from "../infra/backoff.js"; import { formatErrorMessage } from "../infra/errors.js"; -import { parseStrictPositiveInteger } from "../infra/parse-finite-number.js"; import { readConfiguredLogTail } from "../logging/log-tail.js"; import { parseLogLine } from "../logging/parse-log-line.js"; import { redactSensitiveLines, resolveRedactOptions } from "../logging/redact.js"; diff --git a/src/cli/nodes-cli/rpc.ts b/src/cli/nodes-cli/rpc.ts index b18d131dd714..1da7a559ff9f 100644 --- a/src/cli/nodes-cli/rpc.ts +++ b/src/cli/nodes-cli/rpc.ts @@ -1,5 +1,10 @@ // Gateway RPC helpers for node CLI commands, including lazy runtime loading and option parsing. import { randomUUID } from "node:crypto"; +import { + parseStrictFiniteNumber, + parseStrictNonNegativeInteger, + parseStrictPositiveInteger, +} from "@openclaw/normalization-core/number-coercion"; import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce"; import type { Command } from "commander"; import { @@ -9,11 +14,6 @@ import { import { readConnectErrorDetailCode } from "../../../packages/gateway-protocol/src/connect-error-details.js"; import { readMissingScopeError } from "../../../packages/gateway-protocol/src/gateway-error-details.js"; import type { OperatorScope } from "../../gateway/method-scopes.js"; -import { - parseStrictFiniteNumber, - parseStrictNonNegativeInteger, - parseStrictPositiveInteger, -} from "../../infra/parse-finite-number.js"; import { resolveNodeFromNodeList } from "../../shared/node-resolve.js"; import { callGatewayFromCliWithTransport } from "../gateway-rpc.js"; import { parseTimeoutMsWithFallback } from "../parse-timeout.js"; diff --git a/src/cli/parse-timeout.ts b/src/cli/parse-timeout.ts index 997f097cc14a..110f3852614b 100644 --- a/src/cli/parse-timeout.ts +++ b/src/cli/parse-timeout.ts @@ -1,5 +1,5 @@ // Shared CLI timeout parsers for millisecond flags and config-backed fallbacks. -import { parseStrictPositiveInteger } from "../infra/parse-finite-number.js"; +import { parseStrictPositiveInteger } from "@openclaw/normalization-core/number-coercion"; /** Parse a positive millisecond timeout, returning undefined for absent or invalid input. */ export function parseTimeoutMs(raw: unknown): number | undefined { diff --git a/src/cli/ports.ts b/src/cli/ports.ts index 93ed61a2cda8..a940d9802433 100644 --- a/src/cli/ports.ts +++ b/src/cli/ports.ts @@ -1,13 +1,16 @@ // Port inspection and force-free helpers used by gateway run/install flows. import { execFileSync } from "node:child_process"; import { createServer } from "node:net"; +import { + parseStrictPositiveInteger, + resolvePositiveTimerTimeoutMs, + resolveTimerTimeoutMs, +} from "@openclaw/normalization-core/number-coercion"; import { formatErrorMessage } from "../infra/errors.js"; -import { parseStrictPositiveInteger } from "../infra/parse-finite-number.js"; import { resolveLsofCommandSync } from "../infra/ports-lsof.js"; import { parseWindowsNetstatListeners } from "../infra/ports-netstat.js"; import { probePortUsage } from "../infra/ports-probe.js"; import { getWindowsSystem32ExePath } from "../infra/windows-install-roots.js"; -import { resolvePositiveTimerTimeoutMs, resolveTimerTimeoutMs } from "../shared/number-coercion.js"; import { sleep } from "../utils.js"; type PortProcess = { pid: number; command?: string }; diff --git a/src/cli/program/helpers.test.ts b/src/cli/program/helpers.test.ts index 13570161cd37..cb49bebd45df 100644 --- a/src/cli/program/helpers.test.ts +++ b/src/cli/program/helpers.test.ts @@ -4,7 +4,6 @@ import { collectOption, parsePositiveIntOrUndefined, parseStrictPositiveIntOption, - parseStrictPositiveIntOrUndefined, } from "./helpers.js"; describe("program helpers", () => { @@ -32,27 +31,6 @@ describe("program helpers", () => { expect(parsePositiveIntOrUndefined(value)).toBe(expected); }); - it.each([ - { value: undefined, expected: undefined }, - { value: null, expected: undefined }, - { value: "", expected: undefined }, - { value: 5, expected: 5 }, - { value: 5.9, expected: undefined }, - { value: 0, expected: undefined }, - { value: -1, expected: undefined }, - { value: Number.NaN, expected: undefined }, - { value: "10", expected: 10 }, - { value: " 10 ", expected: 10 }, - { value: "+10", expected: 10 }, - { value: "10ms", expected: undefined }, - { value: "1.5", expected: undefined }, - { value: "0", expected: undefined }, - { value: "nope", expected: undefined }, - { value: true, expected: undefined }, - ])("parseStrictPositiveIntOrUndefined(%j)", ({ value, expected }) => { - expect(parseStrictPositiveIntOrUndefined(value)).toBe(expected); - }); - it("parseStrictPositiveIntOption rejects partial numeric strings", () => { expect(parseStrictPositiveIntOption("10", "--limit")).toBe(10); expect(() => parseStrictPositiveIntOption("10ms", "--limit")).toThrow( diff --git a/src/cli/program/helpers.ts b/src/cli/program/helpers.ts index ad8aae2c5c7e..7547bc6025b1 100644 --- a/src/cli/program/helpers.ts +++ b/src/cli/program/helpers.ts @@ -1,6 +1,6 @@ // Shared Commander registration helpers for repeated options and positive integers. +import { parseStrictPositiveInteger } from "@openclaw/normalization-core/number-coercion"; import { InvalidArgumentError } from "commander"; -import { parseStrictPositiveInteger } from "../../infra/parse-finite-number.js"; /** Commander option collector for repeatable string flags. */ export function collectOption(value: string, previous: string[] = []): string[] { @@ -15,11 +15,6 @@ export function parsePositiveIntOrUndefined(value: unknown): number | undefined return parseStrictPositiveInteger(value); } -/** Parse a positive integer without treating empty values specially. */ -export function parseStrictPositiveIntOrUndefined(value: unknown): number | undefined { - return parseStrictPositiveInteger(value); -} - /** Commander argument parser for required positive integer options. */ export function parseStrictPositiveIntOption(value: string, flag: string): number { const parsed = parseStrictPositiveInteger(value); diff --git a/src/cli/program/message/helpers.ts b/src/cli/program/message/helpers.ts index 9387b053181b..0080cb1904ca 100644 --- a/src/cli/program/message/helpers.ts +++ b/src/cli/program/message/helpers.ts @@ -1,3 +1,7 @@ +import { + parseStrictNonNegativeInteger, + parseStrictPositiveInteger, +} from "@openclaw/normalization-core/number-coercion"; // Shared helpers for message CLI actions: common flags, plugin preload, numeric validation, and stop hooks. import type { Command } from "commander"; import { getChannelPlugin } from "../../../channels/plugins/index.js"; @@ -10,10 +14,6 @@ import { messageCommand } from "../../../commands/message.js"; import { getRuntimeConfig } from "../../../config/config.js"; import { danger, setVerbose } from "../../../globals.js"; import { CHANNEL_TARGET_DESCRIPTION } from "../../../infra/outbound/channel-target.js"; -import { - parseStrictNonNegativeInteger, - parseStrictPositiveInteger, -} from "../../../infra/parse-finite-number.js"; import { withActivatedPluginIds } from "../../../plugins/activation-context.js"; import { resolveConfiguredChannelPluginIds, diff --git a/src/cli/program/register.migrate.ts b/src/cli/program/register.migrate.ts index 97c60b8efef2..fcb76ca9b83c 100644 --- a/src/cli/program/register.migrate.ts +++ b/src/cli/program/register.migrate.ts @@ -1,3 +1,4 @@ +import { normalizeOptionalTrimmedStringList } from "@openclaw/normalization-core/string-normalization"; // Migration command registration: list, plan, and apply migration providers. import type { Command } from "commander"; import { theme } from "../../../packages/terminal-core/src/theme.js"; @@ -23,38 +24,9 @@ function collectMigrationItem(value: string, previous: string[] | undefined): st return [...(previous ?? []), value]; } -function readMigrationSkills(value: unknown): string[] | undefined { - if (!Array.isArray(value)) { - return undefined; - } - const skills = value - .filter((item): item is string => typeof item === "string") - .map((item) => item.trim()) - .filter((item) => item.length > 0); - return skills.length > 0 ? skills : undefined; -} - -function readMigrationPlugins(value: unknown): string[] | undefined { - if (!Array.isArray(value)) { - return undefined; - } - const plugins = value - .filter((item): item is string => typeof item === "string") - .map((item) => item.trim()) - .filter((item) => item.length > 0); - return plugins.length > 0 ? plugins : undefined; -} - function readMigrationItems(value: unknown, command?: Command): string[] | undefined { const selected = Array.isArray(value) ? value : command?.parent?.opts().item; - if (!Array.isArray(selected)) { - return undefined; - } - const items = selected - .filter((item): item is string => typeof item === "string") - .map((item) => item.trim()) - .filter((item) => item.length > 0); - return items.length > 0 ? items : undefined; + return normalizeOptionalTrimmedStringList(selected); } function addMigrationSkillOption(command: Command): Command { @@ -183,8 +155,8 @@ export function registerMigrateCommand(program: Command) { includeSecrets: opts.includeSecrets === true ? true : undefined, authCredentials: opts.authCredentials as boolean | undefined, overwrite: Boolean(opts.overwrite), - skills: readMigrationSkills(opts.skill), - plugins: readMigrationPlugins(opts.plugin), + skills: normalizeOptionalTrimmedStringList(opts.skill), + plugins: normalizeOptionalTrimmedStringList(opts.plugin), itemIds: readMigrationItems(opts.item), verifyPluginApps: readVerifyPluginApps(opts.verifyPluginApps), dryRun: Boolean(opts.dryRun), @@ -220,8 +192,8 @@ export function registerMigrateCommand(program: Command) { includeSecrets: opts.includeSecrets === true ? true : undefined, authCredentials: opts.authCredentials as boolean | undefined, overwrite: Boolean(opts.overwrite), - skills: readMigrationSkills(opts.skill), - plugins: readMigrationPlugins(opts.plugin), + skills: normalizeOptionalTrimmedStringList(opts.skill), + plugins: normalizeOptionalTrimmedStringList(opts.plugin), itemIds: readMigrationItems(opts.item, command), verifyPluginApps: readVerifyPluginApps(opts.verifyPluginApps), json: Boolean(opts.json), @@ -245,8 +217,8 @@ export function registerMigrateCommand(program: Command) { includeSecrets: opts.includeSecrets === true ? true : undefined, authCredentials: opts.authCredentials as boolean | undefined, overwrite: Boolean(opts.overwrite), - skills: readMigrationSkills(opts.skill), - plugins: readMigrationPlugins(opts.plugin), + skills: normalizeOptionalTrimmedStringList(opts.skill), + plugins: normalizeOptionalTrimmedStringList(opts.plugin), itemIds: readMigrationItems(opts.item, command), verifyPluginApps: readVerifyPluginApps(opts.verifyPluginApps), yes: Boolean(opts.yes), diff --git a/src/cli/program/register.status-health-sessions.ts b/src/cli/program/register.status-health-sessions.ts index 475ac36410cb..8b4ced03b83e 100644 --- a/src/cli/program/register.status-health-sessions.ts +++ b/src/cli/program/register.status-health-sessions.ts @@ -1,4 +1,5 @@ // Status, health, sessions, and task/flow command registration. +import { parseStrictPositiveInteger } from "@openclaw/normalization-core/number-coercion"; import type { Command } from "commander"; import { formatDocsLink } from "../../../packages/terminal-core/src/links.js"; import { theme } from "../../../packages/terminal-core/src/theme.js"; @@ -6,7 +7,7 @@ import { setVerbose } from "../../globals.js"; import { defaultRuntime } from "../../runtime.js"; import { runCommandWithRuntime } from "../cli-utils.js"; import { formatHelpExamples } from "../help-format.js"; -import { parsePositiveIntOrUndefined, parseStrictPositiveIntOrUndefined } from "./helpers.js"; +import { parsePositiveIntOrUndefined } from "./helpers.js"; function resolveVerbose(opts: { verbose?: boolean; debug?: boolean }): boolean { return Boolean(opts.verbose || opts.debug); @@ -178,7 +179,7 @@ function registerSessionsLifecycleCommand( ) { return; } - const timeoutMs = parseStrictPositiveIntOrUndefined(opts.timeout); + const timeoutMs = parseStrictPositiveInteger(opts.timeout); if (opts.timeout !== undefined && timeoutMs === undefined) { defaultRuntime.error("--timeout must be a positive integer (milliseconds)."); defaultRuntime.exit(1); @@ -218,7 +219,7 @@ function parseTimeoutMs(timeout: unknown): number | null | undefined { } function parseTasksAuditLimit(limit: unknown): number | null | undefined { - const parsed = parseStrictPositiveIntOrUndefined(limit); + const parsed = parseStrictPositiveInteger(limit); if (limit !== undefined && parsed === undefined) { defaultRuntime.error("--limit must be a positive integer, for example --limit 25."); defaultRuntime.exit(1); @@ -555,13 +556,13 @@ export function registerStatusHealthSessionsCommands(program: Command) { ) { return; } - const maxLines = parseStrictPositiveIntOrUndefined(opts.maxLines); + const maxLines = parseStrictPositiveInteger(opts.maxLines); if (opts.maxLines !== undefined && maxLines === undefined) { defaultRuntime.error("--max-lines must be a positive integer."); defaultRuntime.exit(1); return; } - const timeoutMs = parseStrictPositiveIntOrUndefined(opts.timeout); + const timeoutMs = parseStrictPositiveInteger(opts.timeout); if (opts.timeout !== undefined && timeoutMs === undefined) { defaultRuntime.error("--timeout must be a positive integer (milliseconds)."); defaultRuntime.exit(1); diff --git a/src/cli/program/route-args.ts b/src/cli/program/route-args.ts index 5721b107e3fb..c909966a592f 100644 --- a/src/cli/program/route-args.ts +++ b/src/cli/program/route-args.ts @@ -1,4 +1,5 @@ // Route-first argv parsers for commands that can skip full Commander startup. +import { parseStrictPositiveInteger } from "@openclaw/normalization-core/number-coercion"; import { isValueToken } from "../../infra/cli-root-options.js"; import { getCommandPositionalsWithRootOptions, @@ -9,7 +10,6 @@ import { } from "../argv.js"; import { parseGatewayPortOption } from "../gateway-port-option.js"; import { MODELS_PARENT_BOOLEAN_FLAGS, MODELS_PARENT_VALUE_FLAGS } from "../parent-command-path.js"; -import { parseStrictPositiveIntOrUndefined } from "./helpers.js"; type OptionalFlagParse = { ok: boolean; @@ -201,10 +201,7 @@ export function parseGatewayHealthRouteArgs(argv: string[]) { if (!url.ok || !token.ok || !password.ok || !timeout.ok || !port.ok) { return null; } - if ( - timeout.value !== undefined && - parseStrictPositiveIntOrUndefined(timeout.value) === undefined - ) { + if (timeout.value !== undefined && parseStrictPositiveInteger(timeout.value) === undefined) { return null; } let localPortOverride: number | undefined; @@ -550,7 +547,7 @@ export function parseTasksAuditRouteArgs(argv: string[]) { if (rawLimit === null) { return null; } - const limit = rawLimit === undefined ? undefined : parseStrictPositiveIntOrUndefined(rawLimit); + const limit = rawLimit === undefined ? undefined : parseStrictPositiveInteger(rawLimit); if (rawLimit !== undefined && limit === undefined) { return null; } diff --git a/src/cli/progress.test.ts b/src/cli/progress.test.ts index db7af9a9b199..2ca1aa697cbb 100644 --- a/src/cli/progress.test.ts +++ b/src/cli/progress.test.ts @@ -1,6 +1,6 @@ +import { MAX_TIMER_TIMEOUT_MS } from "@openclaw/normalization-core/number-coercion"; // Progress tests cover CLI progress rendering and lifecycle cleanup. import { beforeEach, describe, expect, it, vi } from "vitest"; -import { MAX_TIMER_TIMEOUT_MS } from "../shared/number-coercion.js"; import { createCliProgress, shouldUseInteractiveProgressSpinner } from "./progress.js"; const clackMocks = vi.hoisted(() => { diff --git a/src/cli/progress.ts b/src/cli/progress.ts index 58d592d5a365..374a0c9e2a7f 100644 --- a/src/cli/progress.ts +++ b/src/cli/progress.ts @@ -1,5 +1,6 @@ // Terminal progress reporter used by long-running CLI commands. import { spinner } from "@clack/prompts"; +import { resolveTimerTimeoutMs } from "@openclaw/normalization-core/number-coercion"; import { createOscProgressController, supportsOscProgress, @@ -10,7 +11,6 @@ import { unregisterActiveProgressLine, } from "../../packages/terminal-core/src/progress-line.js"; import { theme } from "../../packages/terminal-core/src/theme.js"; -import { resolveTimerTimeoutMs } from "../shared/number-coercion.js"; const DEFAULT_DELAY_MS = 0; // Only one active progress renderer may own the terminal line at a time. diff --git a/src/cli/proxy-cli.ts b/src/cli/proxy-cli.ts index 07dc408f2f06..bf36ef042485 100644 --- a/src/cli/proxy-cli.ts +++ b/src/cli/proxy-cli.ts @@ -1,6 +1,6 @@ +import { parseStrictInteger } from "@openclaw/normalization-core/number-coercion"; // Commander registration for debug proxy capture, validation, query, and blob commands. import { InvalidArgumentError, type Command } from "commander"; -import { parseStrictInteger } from "../infra/parse-finite-number.js"; import type { CaptureQueryPreset } from "../proxy-capture/types.js"; import { createLazyImportLoader } from "../shared/lazy-promise.js"; import { setCommandJsonMode } from "./program/json-mode.js"; diff --git a/src/cli/tagline.ts b/src/cli/tagline.ts index ee79127b7d81..5cec31899cf3 100644 --- a/src/cli/tagline.ts +++ b/src/cli/tagline.ts @@ -1,6 +1,6 @@ import { expectDefined } from "@openclaw/normalization-core"; // CLI tagline selection helpers, including deterministic random/default/holiday modes. -import { parseStrictNonNegativeInteger } from "../infra/parse-finite-number.js"; +import { parseStrictNonNegativeInteger } from "@openclaw/normalization-core/number-coercion"; const DEFAULT_TAGLINE = "All your chats, one OpenClaw."; export type TaglineMode = "random" | "default" | "off"; diff --git a/src/cli/tui-cli.ts b/src/cli/tui-cli.ts index 9b49c82a8472..692e5e9fcda1 100644 --- a/src/cli/tui-cli.ts +++ b/src/cli/tui-cli.ts @@ -1,9 +1,9 @@ +import { parseStrictPositiveInteger } from "@openclaw/normalization-core/number-coercion"; // Registers the terminal UI subcommand and normalizes its local-vs-gateway options. import type { Command } from "commander"; import { CHAT_HISTORY_MAX_ENTRIES } from "../../packages/gateway-protocol/src/schema/chat-history-constants.js"; import { formatDocsLink } from "../../packages/terminal-core/src/links.js"; import { theme } from "../../packages/terminal-core/src/theme.js"; -import { parseStrictPositiveInteger } from "../infra/parse-finite-number.js"; import { defaultRuntime } from "../runtime.js"; import { parseTimeoutMs } from "./parse-timeout.js"; import { resolveSessionTarget } from "./session-target.js"; diff --git a/src/cli/update-cli/shared.ts b/src/cli/update-cli/shared.ts index 48b1f0dd10fe..8807852ca3ea 100644 --- a/src/cli/update-cli/shared.ts +++ b/src/cli/update-cli/shared.ts @@ -3,13 +3,13 @@ import { spawnSync } from "node:child_process"; import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; +import { parseStrictPositiveInteger } from "@openclaw/normalization-core/number-coercion"; import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce"; import { theme } from "../../../packages/terminal-core/src/theme.js"; import { resolveRequiredHomeDir } from "../../infra/home-dir.js"; import { resolveOpenClawPackageRoot } from "../../infra/openclaw-root.js"; import { readPackageName, readPackageVersion } from "../../infra/package-json.js"; import { normalizePackageTagInput } from "../../infra/package-tag.js"; -import { parseStrictPositiveInteger } from "../../infra/parse-finite-number.js"; import { trimLogTail } from "../../infra/restart-sentinel.js"; import { parseSemver } from "../../infra/runtime-guard.js"; import { fetchNpmTagVersion } from "../../infra/update-check.js"; diff --git a/src/cli/update-cli/update-command-post-core.ts b/src/cli/update-cli/update-command-post-core.ts index 1549ce98cef1..7895b057325d 100644 --- a/src/cli/update-cli/update-command-post-core.ts +++ b/src/cli/update-cli/update-command-post-core.ts @@ -3,6 +3,7 @@ import { spawn, type ChildProcess } from "node:child_process"; import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; +import { parseStrictPositiveInteger } from "@openclaw/normalization-core/number-coercion"; import { isRecord } from "@openclaw/normalization-core/record-coerce"; import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce"; import { theme } from "../../../packages/terminal-core/src/theme.js"; @@ -21,7 +22,6 @@ import type { PluginInstallRecord } from "../../config/types.plugins.js"; import { resolveGatewayInstallEntrypoint } from "../../daemon/gateway-entrypoint.js"; import { hasErrnoCode } from "../../infra/errors.js"; import { readJsonIfExists, writeJson } from "../../infra/json-files.js"; -import { parseStrictPositiveInteger } from "../../infra/parse-finite-number.js"; import { DEFAULT_PACKAGE_CHANNEL, EXTENDED_STABLE_TAG_UNSUPPORTED_REASON, diff --git a/src/cli/update-cli/update-command-service.ts b/src/cli/update-cli/update-command-service.ts index 9a02cf886c8c..f12f2e3a73a8 100644 --- a/src/cli/update-cli/update-command-service.ts +++ b/src/cli/update-cli/update-command-service.ts @@ -3,6 +3,7 @@ import fs from "node:fs/promises"; import path from "node:path"; import { Writable } from "node:stream"; import { confirm, isCancel } from "@clack/prompts"; +import { parseStrictPositiveInteger } from "@openclaw/normalization-core/number-coercion"; import { err as resultError, ok, type Result } from "@openclaw/normalization-core/result"; import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce"; import { stylePromptMessage } from "../../../packages/terminal-core/src/prompt-style.js"; @@ -30,7 +31,6 @@ import { summarizeGatewayServiceLayout } from "../../daemon/service-layout.js"; import type { GatewayServiceCommandConfig } from "../../daemon/service-types.js"; import { readGatewayServiceState, resolveGatewayService } from "../../daemon/service.js"; import { assertGatewayServiceMutationAllowed } from "../../infra/gateway-supervision.js"; -import { parseStrictPositiveInteger } from "../../infra/parse-finite-number.js"; import { getSelfAndAncestorPidsSync } from "../../infra/restart-stale-pids.js"; import { nodeVersionSatisfiesEngine } from "../../infra/runtime-guard.js"; import { fetchNpmPackageTargetStatus } from "../../infra/update-check-package-target.js"; diff --git a/src/cli/webhooks-cli.ts b/src/cli/webhooks-cli.ts index 6696e4bd7f8e..57f331ef0648 100644 --- a/src/cli/webhooks-cli.ts +++ b/src/cli/webhooks-cli.ts @@ -1,3 +1,4 @@ +import { parseStrictPositiveInteger } from "@openclaw/normalization-core/number-coercion"; // Webhook CLI registrations, currently Gmail Pub/Sub setup and service runner commands. import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce"; import type { Command } from "commander"; @@ -20,7 +21,6 @@ import { DEFAULT_GMAIL_SUBSCRIPTION, DEFAULT_GMAIL_TOPIC, } from "../hooks/gmail.js"; -import { parseStrictPositiveInteger } from "../infra/parse-finite-number.js"; import { defaultRuntime } from "../runtime.js"; import { formatCliCommand } from "./command-format.js"; diff --git a/src/commands/agent-exec.ts b/src/commands/agent-exec.ts index 55ad214a53a6..d3c60428f1e7 100644 --- a/src/commands/agent-exec.ts +++ b/src/commands/agent-exec.ts @@ -5,6 +5,7 @@ import os from "node:os"; import path from "node:path"; import { TextDecoder } from "node:util"; import { readByteStreamWithLimit } from "@openclaw/media-core/read-byte-stream-with-limit"; +import { parseStrictNonNegativeInteger } from "@openclaw/normalization-core/number-coercion"; import { findAgentRunTerminalOutcome } from "../agents/agent-run-terminal-error.js"; import type { EmbeddedAgentRunMeta } from "../agents/embedded-agent.js"; import { isExecutionIdentityCollectionEnabled } from "../audit/audit-config.js"; @@ -17,7 +18,6 @@ import type { } from "../infra/embedded-state-lock.js"; import { formatErrorMessage } from "../infra/errors.js"; import type { GatewayLockIdentity, GatewayLockOptions } from "../infra/gateway-lock.js"; -import { parseStrictNonNegativeInteger } from "../infra/parse-finite-number.js"; import { writeRuntimeJson, writeRuntimeStdout, type RuntimeEnv } from "../runtime.js"; const AGENT_EXEC_MESSAGE_MAX_BYTES = 4 * 1024 * 1024; diff --git a/src/commands/agent-via-gateway.ts b/src/commands/agent-via-gateway.ts index e197e4185cd8..53eb6d7f5cf8 100644 --- a/src/commands/agent-via-gateway.ts +++ b/src/commands/agent-via-gateway.ts @@ -1,7 +1,10 @@ // Gateway-first agent CLI implementation with explicit --local embedded execution. import fs from "node:fs/promises"; import { TextDecoder } from "node:util"; -import { resolveTimerTimeoutMs } from "@openclaw/normalization-core/number-coercion"; +import { + parseStrictNonNegativeInteger, + resolveTimerTimeoutMs, +} from "@openclaw/normalization-core/number-coercion"; import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce"; import { GATEWAY_CLIENT_MODES, @@ -36,7 +39,6 @@ import { type EmbeddedStateSignalProcess, } from "../infra/embedded-state-lock.js"; import type { GatewayLockIdentity, GatewayLockOptions } from "../infra/gateway-lock.js"; -import { parseStrictNonNegativeInteger } from "../infra/parse-finite-number.js"; import { routeLogsToStderr } from "../logging/console.js"; import { startOneShotDiagnosticsExporters, diff --git a/src/commands/audit.ts b/src/commands/audit.ts index 6fd6f4cd510e..1610dec24e3c 100644 --- a/src/commands/audit.ts +++ b/src/commands/audit.ts @@ -1,5 +1,8 @@ /** Operator CLI for bounded metadata-only activity audit pages. */ -import { timestampMsToIsoString } from "@openclaw/normalization-core/number-coercion"; +import { + parseStrictPositiveInteger, + timestampMsToIsoString, +} from "@openclaw/normalization-core/number-coercion"; import { truncateUtf16Safe } from "@openclaw/normalization-core/utf16-slice"; import type { AuditActivityListParams, @@ -15,7 +18,6 @@ import type { import { sanitizeTerminalText } from "../../packages/terminal-core/src/safe-text.js"; import { parseAbsoluteTimeMs } from "../cron/parse.js"; import { callGateway } from "../gateway/call.js"; -import { parseStrictPositiveInteger } from "../infra/parse-finite-number.js"; import { type RuntimeEnv, writeRuntimeJson } from "../runtime.js"; const DEFAULT_AUDIT_LIMIT = 100; diff --git a/src/commands/auth-choice.apply-helpers.ts b/src/commands/auth-choice.apply-helpers.ts deleted file mode 100644 index 5221d0d854f5..000000000000 --- a/src/commands/auth-choice.apply-helpers.ts +++ /dev/null @@ -1,5 +0,0 @@ -// Public re-export of provider auth input normalizers used by legacy apply flows. -export { - normalizeSecretInputModeInput, - normalizeTokenProviderInput, -} from "../plugins/provider-auth-input.js"; diff --git a/src/commands/auth-choice.apply.api-providers.ts b/src/commands/auth-choice.apply.api-providers.ts index 5cc8b53c5ac0..c5514b6dc7a2 100644 --- a/src/commands/auth-choice.apply.api-providers.ts +++ b/src/commands/auth-choice.apply.api-providers.ts @@ -1,8 +1,8 @@ // Token-provider normalization hooks for provider-backed auth choices. import { resolveProviderMatch } from "../plugins/provider-auth-choice-helpers.js"; import { resolvePluginProviders } from "../plugins/provider-auth-choice.runtime.js"; +import { normalizeTokenProviderInput } from "../plugins/provider-auth-input.js"; import type { ProviderAuthKind } from "../plugins/types.js"; -import { normalizeTokenProviderInput } from "./auth-choice.apply-helpers.js"; import type { ApplyAuthChoiceParams } from "./auth-choice.apply.types.js"; import type { AuthChoice } from "./onboard-types.js"; diff --git a/src/commands/channels/add.ts b/src/commands/channels/add.ts index 0a026b2fdbd1..10a94be8f040 100644 --- a/src/commands/channels/add.ts +++ b/src/commands/channels/add.ts @@ -1,3 +1,4 @@ +import { parseStrictNonNegativeInteger } from "@openclaw/normalization-core/number-coercion"; // Implements guided and non-interactive `openclaw channels add` account setup. import { normalizeOptionalLowercaseString } from "@openclaw/normalization-core/string-coerce"; import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../../agents/agent-scope.js"; @@ -17,7 +18,6 @@ import { formatUnsupportedChannelActionMessage, } from "../../cli/error-format.js"; import type { OpenClawConfig } from "../../config/config.js"; -import { parseStrictNonNegativeInteger } from "../../infra/parse-finite-number.js"; import { commitConfigWithPendingPluginInstalls } from "../../plugins/install-record-commit.js"; import { refreshPluginRegistryAfterConfigMutation } from "../../plugins/registry-refresh.js"; import { defaultRuntime, type RuntimeEnv } from "../../runtime.js"; diff --git a/src/commands/channels/dead-letters.ts b/src/commands/channels/dead-letters.ts index 10753c52e2da..4bb7ae1f90c0 100644 --- a/src/commands/channels/dead-letters.ts +++ b/src/commands/channels/dead-letters.ts @@ -1,9 +1,9 @@ +import { parseStrictPositiveInteger } from "@openclaw/normalization-core/number-coercion"; // Operator commands for inspecting and resubmitting failed channel ingress events. import { sanitizeTerminalText } from "../../../packages/terminal-core/src/safe-text.js"; import { theme } from "../../../packages/terminal-core/src/theme.js"; import { createChannelIngressQueue } from "../../channels/message/ingress-queue.js"; import { formatDurationHuman } from "../../infra/format-time/format-duration.js"; -import { parseStrictPositiveInteger } from "../../infra/parse-finite-number.js"; import { defaultRuntime, type RuntimeEnv, writeRuntimeJson } from "../../runtime.js"; type ChannelsDeadLettersOptions = { diff --git a/src/commands/channels/logs.ts b/src/commands/channels/logs.ts index 8173560a7459..1175a94ae478 100644 --- a/src/commands/channels/logs.ts +++ b/src/commands/channels/logs.ts @@ -1,10 +1,10 @@ // Implements channel-scoped tailing of the OpenClaw log file. import fs from "node:fs/promises"; +import { parseStrictPositiveInteger } from "@openclaw/normalization-core/number-coercion"; import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce"; import { theme } from "../../../packages/terminal-core/src/theme.js"; import { normalizeChatChannelId as normalizeBundledChannelId } from "../../channels/registry.js"; import { readFileWindowFully } from "../../infra/file-read.js"; -import { parseStrictPositiveInteger } from "../../infra/parse-finite-number.js"; import { getResolvedLoggerSettings } from "../../logging.js"; import { resolveLogFile } from "../../logging/log-tail.js"; import { parseLogLine } from "../../logging/parse-log-line.js"; diff --git a/src/commands/doctor/shared/allowfrom-fallback-migration.ts b/src/commands/doctor/shared/allowfrom-fallback-migration.ts index 2ad2b90a586f..b949ffa9de03 100644 --- a/src/commands/doctor/shared/allowfrom-fallback-migration.ts +++ b/src/commands/doctor/shared/allowfrom-fallback-migration.ts @@ -1,12 +1,12 @@ import { expectDefined } from "@openclaw/normalization-core"; // Doctor migration from legacy DM allowFrom fallback to explicit groupAllowFrom lists. +import { asNullableRecord } from "@openclaw/normalization-core/record-coerce"; import { normalizeUniqueStringEntries } from "@openclaw/normalization-core/string-normalization"; import { resolveChannelDmAllowFrom } from "../../../channels/plugins/dm-access.js"; import { normalizeAnyChannelId } from "../../../channels/registry.js"; import { GENERATED_BUNDLED_CHANNEL_CONFIG_METADATA } from "../../../config/bundled-channel-config-metadata.generated.js"; import type { OpenClawConfig } from "../../../config/types.openclaw.js"; import { getDoctorChannelCapabilities } from "../channel-capabilities.js"; -import { asObjectRecord } from "./object.js"; const PSEUDO_CHANNEL_KEYS = new Set(["defaults", "modelByChannel", "tools"]); const ACCOUNT_SCHEMA_WILDCARD = "*"; @@ -68,7 +68,7 @@ function schemaAllowsConfigPath(schema: unknown, path: SchemaPath): boolean { if (path.length === 0) { return true; } - const node = asObjectRecord(schema); + const node = asNullableRecord(schema); if (!node) { return true; } @@ -90,7 +90,7 @@ function schemaAllowsConfigPath(schema: unknown, path: SchemaPath): boolean { const segment = expectDefined(path[0], "schema path segment"); const rest = path.slice(1); - const properties = asObjectRecord(node.properties); + const properties = asNullableRecord(node.properties); if (segment !== ACCOUNT_SCHEMA_WILDCARD && properties && Object.hasOwn(properties, segment)) { return schemaAllowsConfigPath(expectDefined(properties[segment], "schema property"), rest); } @@ -151,7 +151,7 @@ export function maybeRepairGroupAllowFromFallback(cfg: OpenClawConfig): { config: OpenClawConfig; changes: string[]; } { - const channels = asObjectRecord(cfg.channels); + const channels = asNullableRecord(cfg.channels); if (!channels) { return { config: cfg, changes: [] }; } @@ -188,7 +188,7 @@ export function maybeRepairGroupAllowFromFallback(cfg: OpenClawConfig): { prefix: `channels.${channelName}`, }); - const accounts = asObjectRecord(channelConfig.accounts); + const accounts = asNullableRecord(channelConfig.accounts); if (!accounts) { continue; } @@ -197,7 +197,7 @@ export function maybeRepairGroupAllowFromFallback(cfg: OpenClawConfig): { ACCOUNT_GROUP_ALLOW_FROM_PATH, ); for (const [accountId, accountConfig] of Object.entries(accounts)) { - const account = asObjectRecord(accountConfig); + const account = asNullableRecord(accountConfig); if (!account || isDisabled(account)) { continue; } diff --git a/src/commands/doctor/shared/allowlist-policy-repair.ts b/src/commands/doctor/shared/allowlist-policy-repair.ts index c87a88adf752..d2df2eda3b0d 100644 --- a/src/commands/doctor/shared/allowlist-policy-repair.ts +++ b/src/commands/doctor/shared/allowlist-policy-repair.ts @@ -1,4 +1,5 @@ // Doctor repair for dmPolicy allowlists whose sender entries only exist in pairing stores. +import { asNullableRecord } from "@openclaw/normalization-core/record-coerce"; import { normalizeOptionalLowercaseString } from "@openclaw/normalization-core/string-coerce"; import { normalizeUniqueStringEntries } from "@openclaw/normalization-core/string-normalization"; import { normalizeChatChannelId } from "../../../channels/ids.js"; @@ -8,7 +9,6 @@ import { readChannelAllowFromStore } from "../../../pairing/pairing-store.js"; import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../../../routing/session-key.js"; import { resolveAllowFromMode, type AllowFromMode } from "./allow-from-mode.js"; import { hasAllowFromEntries } from "./allowlist.js"; -import { asObjectRecord } from "./object.js"; /** Restore missing allowFrom entries for allowlist DM policies from persisted pairing stores. */ export async function maybeRepairAllowlistPolicyAllowFrom(cfg: OpenClawConfig): Promise<{ @@ -103,7 +103,7 @@ export async function maybeRepairAllowlistPolicyAllowFrom(cfg: OpenClawConfig): prefix: `channels.${channelName}`, }); - const accounts = asObjectRecord(channelConfig.accounts); + const accounts = asNullableRecord(channelConfig.accounts); if (!accounts) { continue; } diff --git a/src/commands/doctor/shared/bundled-plugin-load-paths.ts b/src/commands/doctor/shared/bundled-plugin-load-paths.ts index ec7d1ea11d2f..248fbd71ee79 100644 --- a/src/commands/doctor/shared/bundled-plugin-load-paths.ts +++ b/src/commands/doctor/shared/bundled-plugin-load-paths.ts @@ -1,5 +1,6 @@ // Doctor warnings and repairs for redundant bundled plugin load path aliases. import path from "node:path"; +import { asNullableRecord } from "@openclaw/normalization-core/record-coerce"; import { sanitizeForLog } from "../../../../packages/terminal-core/src/ansi.js"; import { resolveAgentWorkspaceDir, tryResolveDefaultAgentId } from "../../../agents/agent-scope.js"; import type { OpenClawConfig } from "../../../config/types.openclaw.js"; @@ -11,7 +12,6 @@ import { } from "../../../plugins/bundled-load-path-aliases.js"; import { resolveBundledPluginSources } from "../../../plugins/bundled-sources.js"; import { resolveUserPath } from "../../../utils.js"; -import { asObjectRecord } from "./object.js"; type BundledPluginLoadPathHit = { pluginId: string; @@ -37,8 +37,8 @@ export function scanBundledPluginLoadPathMigrations( cfg: OpenClawConfig, env: NodeJS.ProcessEnv = process.env, ): BundledPluginLoadPathHit[] { - const plugins = asObjectRecord(cfg.plugins); - const load = asObjectRecord(plugins?.load); + const plugins = asNullableRecord(cfg.plugins); + const load = asNullableRecord(plugins?.load); const rawPaths = Array.isArray(load?.paths) ? load.paths : []; if (rawPaths.length === 0) { return []; diff --git a/src/commands/doctor/shared/configured-provider-selection-ids.ts b/src/commands/doctor/shared/configured-provider-selection-ids.ts index c033a20a1a19..73cd89e21640 100644 --- a/src/commands/doctor/shared/configured-provider-selection-ids.ts +++ b/src/commands/doctor/shared/configured-provider-selection-ids.ts @@ -1,8 +1,8 @@ // Reads provider ids selected by auth, model, channel, and media configuration. import { collectConfiguredModelRefs } from "@openclaw/model-catalog-core/configured-model-refs"; +import { asNullableRecord } from "@openclaw/normalization-core/record-coerce"; import { normalizeNullableString as normalizeId } from "@openclaw/normalization-core/string-coerce"; import type { OpenClawConfig } from "../../../config/types.openclaw.js"; -import { asObjectRecord } from "./object.js"; function collectConfiguredProviderIds(cfg: OpenClawConfig): Set { const ids = new Set(); @@ -12,16 +12,16 @@ function collectConfiguredProviderIds(cfg: OpenClawConfig): Set { ids.add(id.toLowerCase()); } }; - for (const profile of Object.values(asObjectRecord(cfg.auth?.profiles) ?? {})) { - add(asObjectRecord(profile)?.provider); + for (const profile of Object.values(asNullableRecord(cfg.auth?.profiles) ?? {})) { + add(asNullableRecord(profile)?.provider); } - for (const providerId of Object.keys(asObjectRecord(cfg.models?.providers) ?? {})) { + for (const providerId of Object.keys(asNullableRecord(cfg.models?.providers) ?? {})) { add(providerId); } - const modelByChannel = asObjectRecord(cfg.channels?.modelByChannel); + const modelByChannel = asNullableRecord(cfg.channels?.modelByChannel); for (const [providerId, channelMap] of Object.entries(modelByChannel ?? {})) { add(providerId); - for (const modelRef of Object.values(asObjectRecord(channelMap) ?? {})) { + for (const modelRef of Object.values(asNullableRecord(channelMap) ?? {})) { if (typeof modelRef !== "string") { continue; } @@ -55,7 +55,7 @@ function collectConfiguredMediaProviderIds(cfg: OpenClawConfig): Set { return; } for (const model of value) { - add(asObjectRecord(model)?.provider); + add(asNullableRecord(model)?.provider); } }; const media = cfg.tools?.media; diff --git a/src/commands/doctor/shared/default-account-warnings.ts b/src/commands/doctor/shared/default-account-warnings.ts index b980a467a7d4..1b5cb6b38651 100644 --- a/src/commands/doctor/shared/default-account-warnings.ts +++ b/src/commands/doctor/shared/default-account-warnings.ts @@ -1,4 +1,5 @@ // Doctor warnings for multi-account channels missing explicit default account routing. +import { asNullableRecord } from "@openclaw/normalization-core/record-coerce"; import { normalizeLowercaseStringOrEmpty, normalizeOptionalString, @@ -16,7 +17,6 @@ import { normalizeAccountId, normalizeOptionalAccountId, } from "../../../routing/session-key.js"; -import { asObjectRecord } from "./object.js"; type ChannelMissingDefaultAccountContext = { channelKey: string; @@ -35,18 +35,18 @@ function normalizeBindingChannelKey(raw?: string | null): string { function collectChannelsMissingDefaultAccount( cfg: OpenClawConfig, ): ChannelMissingDefaultAccountContext[] { - const channels = asObjectRecord(cfg.channels); + const channels = asNullableRecord(cfg.channels); if (!channels) { return []; } const contexts: ChannelMissingDefaultAccountContext[] = []; for (const [channelKey, rawChannel] of Object.entries(channels)) { - const channel = asObjectRecord(rawChannel); + const channel = asNullableRecord(rawChannel); if (!channel) { continue; } - const accounts = asObjectRecord(channel.accounts); + const accounts = asNullableRecord(channel.accounts); if (!accounts) { continue; } @@ -78,11 +78,11 @@ export function collectMissingDefaultAccountBindingWarnings(cfg: OpenClawConfig) let hasWildcardBinding = false; const coveredAccountIds = new Set(); for (const binding of bindings) { - const bindingRecord = asObjectRecord(binding); + const bindingRecord = asNullableRecord(binding); if (!bindingRecord) { continue; } - const match = asObjectRecord(bindingRecord.match); + const match = asNullableRecord(bindingRecord.match); if (!match) { continue; } diff --git a/src/commands/doctor/shared/empty-allowlist-scan.ts b/src/commands/doctor/shared/empty-allowlist-scan.ts index f7b97bb38113..47545480c226 100644 --- a/src/commands/doctor/shared/empty-allowlist-scan.ts +++ b/src/commands/doctor/shared/empty-allowlist-scan.ts @@ -1,4 +1,5 @@ // Doctor scanner for empty allowlist policies across configured channels and accounts. +import { asNullableRecord } from "@openclaw/normalization-core/record-coerce"; import type { ChannelDoctorEmptyAllowlistAccountContext } from "../../../channels/plugins/types.adapters.js"; import type { OpenClawConfig } from "../../../config/types.openclaw.js"; import { @@ -8,7 +9,6 @@ import { import type { DoctorAccountRecord, DoctorAllowFromList } from "../types.js"; import { hasAllowFromEntries } from "./allowlist.js"; import { collectEmptyAllowlistPolicyWarningsForAccount } from "./empty-allowlist-policy.js"; -import { asObjectRecord } from "./object.js"; type ScanEmptyAllowlistPolicyWarningsParams = { doctorFixCommand: string; @@ -44,8 +44,8 @@ export function scanEmptyAllowlistPolicyWarnings( parent?: DoctorAccountRecord, options: { suppressGroupAllowlistWarning?: boolean } = {}, ) => { - const accountDm = asObjectRecord(account.dm); - const parentDm = asObjectRecord(parent?.dm); + const accountDm = asNullableRecord(account.dm); + const parentDm = asNullableRecord(parent?.dm); const dmPolicy = (account.dmPolicy as string | undefined) ?? (accountDm?.policy as string | undefined) ?? @@ -95,7 +95,7 @@ export function scanEmptyAllowlistPolicyWarnings( if (isDisabledRecord(channelConfig)) { continue; } - const accounts = asObjectRecord(channelConfig.accounts); + const accounts = asNullableRecord(channelConfig.accounts); const activeAccounts = accounts ? Object.values(accounts).filter((account): account is DoctorAccountRecord => Boolean(account && typeof account === "object" && !isDisabledRecord(account)), @@ -127,8 +127,8 @@ export function scanEmptyAllowlistPolicyWarnings( if (!getDoctorChannelCapabilities(channelName).groupAllowFromFallbackToAllowFrom) { return false; } - const accountDm = asObjectRecord(account.dm); - const parentDm = asObjectRecord(channelConfig.dm); + const accountDm = asNullableRecord(account.dm); + const parentDm = asNullableRecord(channelConfig.dm); const effectiveAllowFrom = (account.allowFrom as DoctorAllowFromList | undefined) ?? (channelConfig.allowFrom as DoctorAllowFromList | undefined) ?? diff --git a/src/commands/doctor/shared/exec-safe-bins.ts b/src/commands/doctor/shared/exec-safe-bins.ts index 70456118ee7f..2d2465adabb4 100644 --- a/src/commands/doctor/shared/exec-safe-bins.ts +++ b/src/commands/doctor/shared/exec-safe-bins.ts @@ -1,4 +1,5 @@ // Doctor checks and repairs for exec safeBins profiles and trusted binary directories. +import { asNullableRecord } from "@openclaw/normalization-core/record-coerce"; import { sanitizeForLog } from "../../../../packages/terminal-core/src/ansi.js"; import { listAgentEntriesWithSource } from "../../../agents/agent-scope-config.js"; import type { OpenClawConfig } from "../../../config/types.openclaw.js"; @@ -16,7 +17,6 @@ import { normalizeSafeBinName, } from "../../../infra/exec-safe-bin-semantics.js"; import { getTrustedSafeBinDirs, isTrustedSafeBinPath } from "../../../infra/exec-safe-bin-trust.js"; -import { asObjectRecord } from "./object.js"; type ExecSafeBinCoverageHit = { /** Config scope that owns the safeBins entry. */ @@ -50,7 +50,7 @@ type ExecSafeBinTrustedDirHintHit = { function collectExecSafeBinScopes(cfg: OpenClawConfig): ExecSafeBinScopeRef[] { const scopes: ExecSafeBinScopeRef[] = []; - const globalExec = asObjectRecord(cfg.tools?.exec); + const globalExec = asNullableRecord(cfg.tools?.exec); const globalTrustedDirs = normalizeConfiguredTrustedSafeBinDirs(globalExec?.safeBinTrustedDirs); if (globalExec) { const safeBins = normalizeConfiguredSafeBins(globalExec.safeBins); @@ -70,7 +70,7 @@ function collectExecSafeBinScopes(cfg: OpenClawConfig): ExecSafeBinScopeRef[] { } } for (const { entry: agent, source } of listAgentEntriesWithSource(cfg)) { - const agentExec = asObjectRecord(agent.tools?.exec); + const agentExec = asNullableRecord(agent.tools?.exec); if (!agentExec) { continue; } @@ -267,7 +267,7 @@ export function maybeRepairExecSafeBinProfiles(cfg: OpenClawConfig): { continue; } const profileHolder = - asObjectRecord(scope.exec.safeBinProfiles) ?? (scope.exec.safeBinProfiles = {}); + asNullableRecord(scope.exec.safeBinProfiles) ?? (scope.exec.safeBinProfiles = {}); for (const bin of missingBins) { if (interpreterBins.has(bin)) { warnings.push( diff --git a/src/commands/doctor/shared/invalid-plugin-config.ts b/src/commands/doctor/shared/invalid-plugin-config.ts index 14e3fc223a9b..7e7d7166ec46 100644 --- a/src/commands/doctor/shared/invalid-plugin-config.ts +++ b/src/commands/doctor/shared/invalid-plugin-config.ts @@ -1,8 +1,8 @@ // Doctor quarantine for plugin entries whose config fails plugin-aware validation. +import { asNullableRecord } from "@openclaw/normalization-core/record-coerce"; import { sanitizeForLog } from "../../../../packages/terminal-core/src/ansi.js"; import type { OpenClawConfig } from "../../../config/types.openclaw.js"; import { validateConfigObjectWithPlugins } from "../../../config/validation.js"; -import { asObjectRecord } from "./object.js"; type InvalidPluginConfigHit = { pluginId: string; @@ -47,14 +47,14 @@ export function maybeRepairInvalidPluginConfig(cfg: OpenClawConfig): { } const next = structuredClone(cfg); - const entries = asObjectRecord(next.plugins?.entries); + const entries = asNullableRecord(next.plugins?.entries); if (!entries) { return { config: cfg, changes: [] }; } const quarantined: string[] = []; for (const hit of hits) { - const entry = asObjectRecord(entries[hit.pluginId]); + const entry = asNullableRecord(entries[hit.pluginId]); if (!entry) { continue; } diff --git a/src/commands/doctor/shared/legacy-tools-by-sender.ts b/src/commands/doctor/shared/legacy-tools-by-sender.ts index bf0b18c53419..3f92e068bce6 100644 --- a/src/commands/doctor/shared/legacy-tools-by-sender.ts +++ b/src/commands/doctor/shared/legacy-tools-by-sender.ts @@ -1,9 +1,9 @@ // Doctor scanner and repair for legacy untyped toolsBySender sender keys. +import { asNullableRecord } from "@openclaw/normalization-core/record-coerce"; import { sanitizeForLog } from "../../../../packages/terminal-core/src/ansi.js"; import type { OpenClawConfig } from "../../../config/types.openclaw.js"; import { parseToolsBySenderTypedKey } from "../../../config/types.tools.js"; import { formatConfigKeyPath, resolveConfigPathTarget } from "../../doctor-config-analysis.js"; -import { asObjectRecord } from "./object.js"; type LegacyToolsBySenderKeyHit = { /** Path parts pointing to the containing toolsBySender object. */ @@ -27,12 +27,12 @@ function collectLegacyToolsBySenderKeyHits( } return; } - const record = asObjectRecord(value); + const record = asNullableRecord(value); if (!record) { return; } - const toolsBySender = asObjectRecord(record.toolsBySender); + const toolsBySender = asNullableRecord(record.toolsBySender); if (toolsBySender) { const path = [...pathParts, "toolsBySender"]; const pathLabel = formatConfigKeyPath(path); @@ -99,7 +99,7 @@ export function maybeRepairLegacyToolsBySenderKeys(cfg: OpenClawConfig): { let changed = false; for (const hit of hits) { - const toolsBySender = asObjectRecord(resolveConfigPathTarget(next, hit.toolsBySenderPath)); + const toolsBySender = asNullableRecord(resolveConfigPathTarget(next, hit.toolsBySenderPath)); if (!toolsBySender || !(hit.key in toolsBySender)) { continue; } diff --git a/src/commands/doctor/shared/missing-configured-plugin-install.ids.ts b/src/commands/doctor/shared/missing-configured-plugin-install.ids.ts index 87ae3c31c4f5..b6f9c706bc23 100644 --- a/src/commands/doctor/shared/missing-configured-plugin-install.ids.ts +++ b/src/commands/doctor/shared/missing-configured-plugin-install.ids.ts @@ -1,3 +1,4 @@ +import { asNullableRecord } from "@openclaw/normalization-core/record-coerce"; import { normalizeOptionalLowercaseString } from "@openclaw/normalization-core/string-coerce"; import { listRawChannelPluginCatalogEntries } from "../../../channels/plugins/catalog.js"; import type { OpenClawConfig } from "../../../config/types.openclaw.js"; @@ -16,7 +17,6 @@ import { import { listDoctorConfiguredChannelIds } from "./configured-channel-ids.js"; import { collectConfiguredProviderPluginIds } from "./configured-provider-plugin-installs.js"; import { collectConfiguredRuntimePluginIds } from "./configured-runtime-plugin-installs.js"; -import { asObjectRecord } from "./object.js"; function addConfiguredPluginId(ids: Set, value: unknown): void { if (typeof value !== "string") { @@ -99,13 +99,13 @@ export function collectConfiguredPluginIds( env?: NodeJS.ProcessEnv, ): Set { const ids = new Set(); - const plugins = asObjectRecord(cfg.plugins); + const plugins = asNullableRecord(cfg.plugins); if (plugins?.enabled === false) { return ids; } - const entries = asObjectRecord(plugins?.entries); + const entries = asNullableRecord(plugins?.entries); for (const [pluginId, entry] of Object.entries(entries ?? {})) { - if (asObjectRecord(entry)?.enabled === false) { + if (asNullableRecord(entry)?.enabled === false) { continue; } addConfiguredPluginId(ids, pluginId); @@ -144,9 +144,9 @@ export function collectBlockedPluginIds(cfg: OpenClawConfig): Set { } } } - const entries = asObjectRecord(cfg.plugins?.entries); + const entries = asNullableRecord(cfg.plugins?.entries); for (const [pluginId, entry] of Object.entries(entries ?? {})) { - if (pluginId.trim() && asObjectRecord(entry)?.enabled === false) { + if (pluginId.trim() && asNullableRecord(entry)?.enabled === false) { ids.add(pluginId.trim()); } } @@ -157,7 +157,7 @@ export function collectConfiguredChannelIds( cfg: OpenClawConfig, env?: NodeJS.ProcessEnv, ): Set { - if (asObjectRecord(cfg.plugins)?.enabled === false) { + if (asNullableRecord(cfg.plugins)?.enabled === false) { return new Set(); } const candidateChannelIds = listRawChannelPluginCatalogEntries({ diff --git a/src/commands/doctor/shared/object.ts b/src/commands/doctor/shared/object.ts deleted file mode 100644 index 6a4c2fa56d5c..000000000000 --- a/src/commands/doctor/shared/object.ts +++ /dev/null @@ -1,2 +0,0 @@ -// Shared nullable record guard for doctor config walkers. -export { asNullableRecord as asObjectRecord } from "@openclaw/normalization-core/record-coerce"; diff --git a/src/commands/doctor/shared/open-policy-allowfrom.ts b/src/commands/doctor/shared/open-policy-allowfrom.ts index d7218000e6ec..026502add2b4 100644 --- a/src/commands/doctor/shared/open-policy-allowfrom.ts +++ b/src/commands/doctor/shared/open-policy-allowfrom.ts @@ -1,10 +1,10 @@ // Doctor repair for open DM policies that still need explicit allowFrom wildcards. +import { asNullableRecord } from "@openclaw/normalization-core/record-coerce"; import { sanitizeForLog } from "../../../../packages/terminal-core/src/ansi.js"; import { ensureOpenDmPolicyAllowFromWildcard } from "../../../channels/plugins/dm-access.js"; import type { OpenClawConfig } from "../../../config/types.openclaw.js"; import { getDoctorChannelCapabilities } from "../channel-capabilities.js"; import type { AllowFromMode } from "./allow-from-mode.js"; -import { asObjectRecord } from "./object.js"; /** Format doctor warnings for open DM policies missing allowFrom wildcards. */ export function collectOpenPolicyAllowFromWarnings(params: { @@ -59,7 +59,7 @@ export function maybeRepairOpenPolicyAllowFrom(cfg: OpenClawConfig): { const allowFromMode = capabilities.dmAllowFromMode; ensureWildcard(channelConfig, `channels.${channelName}`, allowFromMode); - const accounts = asObjectRecord(channelConfig.accounts); + const accounts = asNullableRecord(channelConfig.accounts); if (!accounts) { continue; } diff --git a/src/commands/doctor/shared/release-configured-plugin-installs.ts b/src/commands/doctor/shared/release-configured-plugin-installs.ts index 35cf26da45dd..1327bb9d2fb6 100644 --- a/src/commands/doctor/shared/release-configured-plugin-installs.ts +++ b/src/commands/doctor/shared/release-configured-plugin-installs.ts @@ -1,4 +1,5 @@ // Release-era repair for configs that imply official plugin installs before install records existed. +import { asNullableRecord } from "@openclaw/normalization-core/record-coerce"; import { normalizeNullableString as normalizeId } from "@openclaw/normalization-core/string-coerce"; import { collectConfiguredAgentHarnessRuntimes } from "../../../agents/harness-runtimes.js"; import { normalizeChatChannelId } from "../../../channels/registry.js"; @@ -24,7 +25,6 @@ import { VERSION } from "../../../version.js"; import { listDoctorConfiguredChannelIds } from "./configured-channel-ids.js"; import { collectConfiguredProviderPluginIds } from "./configured-provider-plugin-installs.js"; import { repairMissingPluginInstallsForIds } from "./missing-configured-plugin-install.js"; -import { asObjectRecord } from "./object.js"; import { shouldDeferConfiguredPluginInstallRepair } from "./update-phase.js"; const CONFIGURED_PLUGIN_INSTALL_RELEASE_VERSION = "2026.5.2-beta.1"; @@ -59,9 +59,9 @@ function collectBlockedPluginIds(cfg: OpenClawConfig): string[] { } } } - const entries = asObjectRecord(cfg.plugins?.entries); + const entries = asNullableRecord(cfg.plugins?.entries); for (const [pluginId, entry] of Object.entries(entries ?? {})) { - if (asObjectRecord(entry)?.enabled === false && pluginId.trim()) { + if (asNullableRecord(entry)?.enabled === false && pluginId.trim()) { ids.add(pluginId.trim()); } } @@ -73,8 +73,8 @@ function isPluginEntryDisabled(cfg: OpenClawConfig, pluginId: string): boolean { } function isChannelDisabled(cfg: OpenClawConfig, channelId: string): boolean { - const channels = asObjectRecord(cfg.channels); - const entry = asObjectRecord(channels?.[channelId]); + const channels = asNullableRecord(cfg.channels); + const entry = asNullableRecord(channels?.[channelId]); return entry?.enabled === false; } @@ -87,22 +87,22 @@ function isDisabled(cfg: OpenClawConfig, pluginId: string): boolean { } function hasMaterialPluginEntry(entry: unknown): boolean { - const record = asObjectRecord(entry); + const record = asNullableRecord(entry); if (!record) { return false; } return ( record.enabled === true || - asObjectRecord(record.config) !== null || - asObjectRecord(record.hooks) !== null || - asObjectRecord(record.subagent) !== null || + asNullableRecord(record.config) !== null || + asNullableRecord(record.hooks) !== null || + asNullableRecord(record.subagent) !== null || record.apiKey !== undefined || record.env !== undefined ); } function collectMaterialPluginEntryIds(cfg: OpenClawConfig): string[] { - const entries = asObjectRecord(cfg.plugins?.entries); + const entries = asNullableRecord(cfg.plugins?.entries); if (!entries) { return []; } @@ -113,7 +113,7 @@ function collectMaterialPluginEntryIds(cfg: OpenClawConfig): string[] { } function collectSlotPluginIds(cfg: OpenClawConfig): string[] { - const slots = asObjectRecord(cfg.plugins?.slots); + const slots = asNullableRecord(cfg.plugins?.slots); return ["memory", "contextEngine"] .map((key) => normalizeId(slots?.[key])) .filter( @@ -197,13 +197,13 @@ function collectSpeechPluginIds(cfg: OpenClawConfig): string[] { } function collectAcpRuntimePluginIds(cfg: OpenClawConfig): string[] { - const acp = asObjectRecord(cfg.acp); + const acp = asNullableRecord(cfg.acp); if (!acp) { return []; } const backend = normalizeId(acp.backend)?.toLowerCase() ?? ""; const configured = - acp.enabled === true || asObjectRecord(acp.dispatch)?.enabled === true || backend === "acpx"; + acp.enabled === true || asNullableRecord(acp.dispatch)?.enabled === true || backend === "acpx"; if (!configured || (backend && backend !== "acpx")) { return []; } diff --git a/src/commands/doctor/shared/stale-plugin-config.ts b/src/commands/doctor/shared/stale-plugin-config.ts index fcb64f03253c..eefbf6cfaea1 100644 --- a/src/commands/doctor/shared/stale-plugin-config.ts +++ b/src/commands/doctor/shared/stale-plugin-config.ts @@ -1,4 +1,5 @@ // Doctor scanner and repair for plugin/channel config that references missing plugins. +import { asNullableRecord } from "@openclaw/normalization-core/record-coerce"; import { sanitizeForLog } from "../../../../packages/terminal-core/src/ansi.js"; import { resolveAgentWorkspaceDir, tryResolveDefaultAgentId } from "../../../agents/agent-scope.js"; import { CHANNEL_IDS } from "../../../channels/ids.js"; @@ -12,7 +13,6 @@ import { } from "../../../plugins/official-external-plugin-catalog.js"; import { defaultSlotIdForKey, type PluginSlotKey } from "../../../plugins/slots.js"; import { listMutableCodexRouteAgentEntries } from "./codex-route-agent-entries.js"; -import { asObjectRecord } from "./object.js"; import { filterRepairableStalePluginHits, type StalePluginSurface, @@ -118,7 +118,7 @@ function scanStalePluginConfigWithState( cfg: OpenClawConfig, registryState: StalePluginRegistryState, ): StalePluginConfigHit[] { - const plugins = asObjectRecord(cfg.plugins); + const plugins = asNullableRecord(cfg.plugins); const { knownIds, officialIds } = registryState; const hits: StalePluginConfigHit[] = []; const staleEvidenceIds = new Set(registryState.missingInstalledIds); @@ -143,7 +143,7 @@ function scanStalePluginConfigWithState( } } - const entries = asObjectRecord(plugins?.entries); + const entries = asNullableRecord(plugins?.entries); if (entries) { for (const rawPluginId of Object.keys(entries)) { const pluginId = normalizePluginId(rawPluginId); @@ -164,7 +164,7 @@ function scanStalePluginConfigWithState( } } - const slots = asObjectRecord(plugins?.slots); + const slots = asNullableRecord(plugins?.slots); if (slots) { for (const slotKey of ["memory", "contextEngine"] as const satisfies readonly PluginSlotKey[]) { const rawPluginId = slots[slotKey]; @@ -214,7 +214,7 @@ function collectDanglingChannelIds(params: { registryState: StalePluginRegistryState; staleEvidenceIds: ReadonlySet; }): string[] { - const channels = asObjectRecord(params.cfg.channels); + const channels = asNullableRecord(params.cfg.channels); if (!channels) { return []; } @@ -257,7 +257,7 @@ function collectDependentChannelConfigHits( }); } for (const { agent, path } of listMutableCodexRouteAgentEntries(cfg)) { - const heartbeat = asObjectRecord(agent.heartbeat); + const heartbeat = asNullableRecord(agent.heartbeat); const target = heartbeat?.target; if (typeof target !== "string" || !staleChannelIds.has(normalizePluginId(target))) { continue; @@ -269,10 +269,10 @@ function collectDependentChannelConfigHits( }); } - const modelByChannel = asObjectRecord(cfg.channels?.modelByChannel); + const modelByChannel = asNullableRecord(cfg.channels?.modelByChannel); if (modelByChannel) { for (const [providerId, channelMap] of Object.entries(modelByChannel)) { - const channels = asObjectRecord(channelMap); + const channels = asNullableRecord(channelMap); if (!channels) { continue; } @@ -377,7 +377,7 @@ export function maybeRepairStalePluginConfig( } const next = structuredClone(cfg); - const nextPlugins = asObjectRecord(next.plugins); + const nextPlugins = asNullableRecord(next.plugins); const allowIds = hits.filter((hit) => hit.surface === "allow").map((hit) => hit.pluginId); if (allowIds.length > 0 && Array.isArray(nextPlugins?.allow)) { @@ -397,7 +397,7 @@ export function maybeRepairStalePluginConfig( const entryIds = hits.filter((hit) => hit.surface === "entries").map((hit) => hit.pluginId); if (entryIds.length > 0) { - const entries = asObjectRecord(nextPlugins?.entries); + const entries = asNullableRecord(nextPlugins?.entries); if (entries) { const staleEntryIds = new Set(entryIds.map((pluginId) => normalizePluginId(pluginId))); for (const pluginId of Object.keys(entries)) { @@ -413,7 +413,7 @@ export function maybeRepairStalePluginConfig( hit.surface === "slot" && hit.slotKey !== undefined, ); if (slotHits.length > 0) { - const slots = asObjectRecord(nextPlugins?.slots); + const slots = asNullableRecord(nextPlugins?.slots); if (slots) { for (const hit of slotHits) { slots[hit.slotKey] = defaultSlotIdForKey(hit.slotKey); @@ -476,7 +476,7 @@ export function maybeRepairStalePluginConfig( function removeDanglingChannelReferences(config: OpenClawConfig, channelIds: readonly string[]) { const staleChannelIds = new Set(channelIds.map((channelId) => normalizePluginId(channelId))); - const channels = asObjectRecord(config.channels); + const channels = asNullableRecord(config.channels); if (channels) { for (const channelId of Object.keys(channels)) { if (CHANNEL_CONFIG_META_KEYS.has(channelId)) { @@ -487,10 +487,10 @@ function removeDanglingChannelReferences(config: OpenClawConfig, channelIds: rea } } - const modelByChannel = asObjectRecord(channels.modelByChannel); + const modelByChannel = asNullableRecord(channels.modelByChannel); if (modelByChannel) { for (const [providerId, channelMap] of Object.entries(modelByChannel)) { - const channelsForProvider = asObjectRecord(channelMap); + const channelsForProvider = asNullableRecord(channelMap); if (!channelsForProvider) { continue; } @@ -518,7 +518,7 @@ function removeDanglingChannelReferences(config: OpenClawConfig, channelIds: rea delete defaultsHeartbeat.target; } for (const { agent } of listMutableCodexRouteAgentEntries(config)) { - const heartbeat = asObjectRecord(agent.heartbeat); + const heartbeat = asNullableRecord(agent.heartbeat); if ( heartbeat && typeof heartbeat.target === "string" && diff --git a/src/commands/gateway-status/helpers.ts b/src/commands/gateway-status/helpers.ts index 9a900284d4a9..f11842183fe9 100644 --- a/src/commands/gateway-status/helpers.ts +++ b/src/commands/gateway-status/helpers.ts @@ -1,3 +1,4 @@ +import { parseStrictInteger } from "@openclaw/normalization-core/number-coercion"; /** Shared helpers for gateway status target selection, auth, summaries, and probe rendering. */ import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce"; import { colorize, theme } from "../../../packages/terminal-core/src/theme.js"; @@ -8,7 +9,6 @@ import { resolveGatewayProbeSurfaceAuth } from "../../gateway/auth-surface-resol import { isLoopbackHost } from "../../gateway/net.js"; import type { GatewayProbeCapability, GatewayProbeResult } from "../../gateway/probe.js"; import { inspectBestEffortPrimaryTailnetIPv4 } from "../../infra/network-discovery-display.js"; -import { parseStrictInteger } from "../../infra/parse-finite-number.js"; const LEGACY_MISSING_SCOPE_PATTERN = /\bmissing scope:\s*[a-z0-9._-]+/i; diff --git a/src/commands/health.snapshot.test.ts b/src/commands/health.snapshot.test.ts index 51d719513903..62f4add80d30 100644 --- a/src/commands/health.snapshot.test.ts +++ b/src/commands/health.snapshot.test.ts @@ -2,12 +2,12 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; +import { MAX_TIMER_TIMEOUT_MS } from "@openclaw/normalization-core/number-coercion"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import type { ChannelAccountSnapshot } from "../channels/plugins/types.public.js"; import type { ChannelPlugin } from "../channels/plugins/types.public.js"; import type { HealthSummary } from "../gateway/health/types.js"; import { createPluginRecord } from "../plugins/status.test-fixtures.js"; -import { MAX_TIMER_TIMEOUT_MS } from "../shared/number-coercion.js"; import { createOpenClawTestState } from "../test-utils/openclaw-test-state.js"; import { createLegacyHealthSnapshotCollector, diff --git a/src/commands/models/list.status-command.ts b/src/commands/models/list.status-command.ts index 9d1fa4e3536b..f7802994b5bd 100644 --- a/src/commands/models/list.status-command.ts +++ b/src/commands/models/list.status-command.ts @@ -1,5 +1,9 @@ /** Implementation of `openclaw models status`. */ import path from "node:path"; +import { + parseStrictFiniteNumber, + parseStrictPositiveInteger, +} from "@openclaw/normalization-core/number-coercion"; import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce"; import { colorize, theme } from "../../../packages/terminal-core/src/theme.js"; import { @@ -69,10 +73,6 @@ import { } from "../../config/model-input.js"; import { parseModelPolicyWildcardRef } from "../../config/model-policy-ref.js"; import { resolveMergedModelProviderConfig } from "../../config/model-provider-config.js"; -import { - parseStrictFiniteNumber, - parseStrictPositiveInteger, -} from "../../infra/parse-finite-number.js"; import { getShellEnvAppliedKeys, shouldEnableShellEnvFallback } from "../../infra/shell-env.js"; import type { ProviderModelRouteCandidate } from "../../plugin-sdk/provider-model-types.js"; import { diff --git a/src/commands/models/scan.ts b/src/commands/models/scan.ts index a7a4ce119784..4aa4c35fd613 100644 --- a/src/commands/models/scan.ts +++ b/src/commands/models/scan.ts @@ -1,6 +1,10 @@ /** OpenRouter free-model scanner and fallback updater for model commands. */ import { cancel, multiselect as clackMultiselect, isCancel } from "@clack/prompts"; import { getEnvApiKey } from "@openclaw/ai/internal/runtime"; +import { + parseStrictFiniteNumber, + parseStrictPositiveInteger, +} from "@openclaw/normalization-core/number-coercion"; import { styleSelectParams } from "../../../packages/terminal-core/src/prompt-select-styled-params.js"; import { stylePromptTitle } from "../../../packages/terminal-core/src/prompt-style.js"; import { sanitizeTerminalText } from "../../../packages/terminal-core/src/safe-text.js"; @@ -10,10 +14,6 @@ import { formatCliCommand } from "../../cli/command-format.js"; import { withProgressTotals } from "../../cli/progress.js"; import { logConfigUpdated } from "../../config/logging.js"; import { toAgentModelListLike } from "../../config/model-input.js"; -import { - parseStrictFiniteNumber, - parseStrictPositiveInteger, -} from "../../infra/parse-finite-number.js"; import { type RuntimeEnv, writeRuntimeJson } from "../../runtime.js"; import { padTerminalCell, truncate } from "./list.format.js"; import { loadModelsConfig } from "./load-config.js"; diff --git a/src/commands/onboard-non-interactive/local/auth-choice.ts b/src/commands/onboard-non-interactive/local/auth-choice.ts index c9d455455ba7..33758053698f 100644 --- a/src/commands/onboard-non-interactive/local/auth-choice.ts +++ b/src/commands/onboard-non-interactive/local/auth-choice.ts @@ -10,6 +10,7 @@ import type { OpenClawConfig } from "../../../config/types.openclaw.js"; import type { SecretInput } from "../../../config/types.secrets.js"; import { formatErrorMessage } from "../../../infra/errors.js"; import { resolveManifestDeprecatedProviderAuthChoice } from "../../../plugins/provider-auth-choices.js"; +import { normalizeSecretInputModeInput } from "../../../plugins/provider-auth-input.js"; import { resolveDeprecatedProviderInstallCatalogEntry } from "../../../plugins/provider-install-catalog.js"; import type { RuntimeEnv } from "../../../runtime.js"; import { resolveDefaultSecretProviderAlias } from "../../../secrets/ref-contract.js"; @@ -19,7 +20,6 @@ import { resolveDeprecatedAuthChoiceReplacement, } from "../../auth-choice-legacy.js"; import { formatAuthChoiceChoicesForCli } from "../../auth-choice-options.js"; -import { normalizeSecretInputModeInput } from "../../auth-choice.apply-helpers.js"; import { normalizeApiKeyTokenProviderAuthChoice } from "../../auth-choice.apply.api-providers.js"; import type { OnboardingAgentTarget } from "../../onboard-agent-target.js"; import { diff --git a/src/commands/onboard-remote.ts b/src/commands/onboard-remote.ts index 8a20208cbd9e..d6120fef099f 100644 --- a/src/commands/onboard-remote.ts +++ b/src/commands/onboard-remote.ts @@ -1,3 +1,4 @@ +import { parseStrictNonNegativeInteger } from "@openclaw/normalization-core/number-coercion"; /** * Interactive remote gateway onboarding. * @@ -12,7 +13,6 @@ import { buildGatewayDiscoveryLabel, buildGatewayDiscoveryTarget, } from "../infra/gateway-discovery-targets.js"; -import { parseStrictNonNegativeInteger } from "../infra/parse-finite-number.js"; import { resolveWideAreaDiscoveryDomain } from "../infra/widearea-dns.js"; import { resolveSecretInputModeForEnvSelection } from "../plugins/provider-auth-mode.js"; import { promptSecretRefForSetup } from "../plugins/provider-auth-ref.js"; diff --git a/src/commands/sessions-tail.ts b/src/commands/sessions-tail.ts index 351ae8bbf259..56ead822e43c 100644 --- a/src/commands/sessions-tail.ts +++ b/src/commands/sessions-tail.ts @@ -1,3 +1,4 @@ +import { parseStrictNonNegativeInteger } from "@openclaw/normalization-core/number-coercion"; /** * Session trajectory tail command. * @@ -11,7 +12,6 @@ import { listSessionEntriesReadOnly } from "../config/sessions/session-accessor. import type { SessionEntry } from "../config/sessions/types.js"; import { resolveStoredSessionKeyForAgentStore } from "../gateway/session-store-key.js"; import { formatErrorMessage } from "../infra/errors.js"; -import { parseStrictNonNegativeInteger } from "../infra/parse-finite-number.js"; import { resolveAgentIdFromSessionKey } from "../routing/session-key.js"; import type { RuntimeEnv } from "../runtime.js"; import { loadSqliteTrajectoryRuntimeEventRowsSync } from "../trajectory/runtime-store.sqlite.js"; diff --git a/src/commands/sessions.ts b/src/commands/sessions.ts index 3cc75e04ed91..ece1c55d75a0 100644 --- a/src/commands/sessions.ts +++ b/src/commands/sessions.ts @@ -1,3 +1,4 @@ +import { parseStrictPositiveInteger } from "@openclaw/normalization-core/number-coercion"; /** * Session listing command. * @@ -26,7 +27,6 @@ import type { SessionEntry } from "../config/sessions/types.js"; import type { OpenClawConfig } from "../config/types.openclaw.js"; import { resolveStoredSessionKeyForAgentStore } from "../gateway/session-store-key.js"; import { info } from "../globals.js"; -import { parseStrictPositiveInteger } from "../infra/parse-finite-number.js"; import { parseAgentSessionKey } from "../routing/session-key.js"; import { type RuntimeEnv, writeRuntimeJson } from "../runtime.js"; import { classifySessionKind, type SessionKind } from "../sessions/classify-session-kind.js"; diff --git a/src/cron/parse.ts b/src/cron/parse.ts index d0acfb664f56..f7034f4f095c 100644 --- a/src/cron/parse.ts +++ b/src/cron/parse.ts @@ -1,5 +1,5 @@ /** Parses cron schedule timestamps from user-facing absolute time strings. */ -import { parseStrictPositiveInteger } from "../infra/parse-finite-number.js"; +import { parseStrictPositiveInteger } from "@openclaw/normalization-core/number-coercion"; import { hasValidIsoCalendarComponents } from "../shared/iso-time.js"; const ISO_TZ_RE = /(Z|[+-]\d{2}:?\d{2})$/i; diff --git a/src/cron/stagger.ts b/src/cron/stagger.ts index 932ea8bcbead..48d71574e5df 100644 --- a/src/cron/stagger.ts +++ b/src/cron/stagger.ts @@ -2,9 +2,9 @@ import { expectDefined } from "@openclaw/normalization-core"; import { asSafeIntegerInRange, MAX_DATE_TIMESTAMP_MS, + parseStrictNonNegativeInteger, } from "@openclaw/normalization-core/number-coercion"; /** Resolves deterministic cron stagger windows for recurring schedules. */ -import { parseStrictNonNegativeInteger } from "../infra/parse-finite-number.js"; import type { CronSchedule } from "./types.js"; /** Default jitter window applied to recurring top-of-hour cron schedules. */ diff --git a/src/daemon/launchd-runtime.ts b/src/daemon/launchd-runtime.ts index 3986c149b749..e7885e8ed742 100644 --- a/src/daemon/launchd-runtime.ts +++ b/src/daemon/launchd-runtime.ts @@ -1,7 +1,10 @@ /** launchctl state parsing, inspection, and bootstrap primitives. */ import fs from "node:fs/promises"; +import { + parseStrictInteger, + parseStrictPositiveInteger, +} from "@openclaw/normalization-core/number-coercion"; import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce"; -import { parseStrictInteger, parseStrictPositiveInteger } from "../infra/parse-finite-number.js"; import { parseTcpPort, parseTcpPortFromArgs } from "../infra/tcp-port.js"; import { sleep } from "../utils.js"; import { resolveGatewayServiceProbeHosts } from "./gateway-service-probe-hosts.js"; diff --git a/src/daemon/launchd-update-jobs.ts b/src/daemon/launchd-update-jobs.ts index be17cd4d3fed..be7164441047 100644 --- a/src/daemon/launchd-update-jobs.ts +++ b/src/daemon/launchd-update-jobs.ts @@ -1,6 +1,9 @@ /** Discovery and shutdown of stale OpenClaw launchd updater jobs. */ import path from "node:path"; -import { parseStrictInteger, parseStrictPositiveInteger } from "../infra/parse-finite-number.js"; +import { + parseStrictInteger, + parseStrictPositiveInteger, +} from "@openclaw/normalization-core/number-coercion"; import { GATEWAY_SERVICE_KIND, GATEWAY_SERVICE_MARKER, diff --git a/src/daemon/systemd-runtime.ts b/src/daemon/systemd-runtime.ts index c1b2e551ce4b..fb5e61b75a77 100644 --- a/src/daemon/systemd-runtime.ts +++ b/src/daemon/systemd-runtime.ts @@ -1,11 +1,11 @@ -/** systemd service enabled-state and runtime inspection. */ -import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce"; -import { formatErrorMessage } from "../infra/errors.js"; import { parseStrictInteger, parseStrictNonNegativeInteger, parseStrictPositiveInteger, -} from "../infra/parse-finite-number.js"; +} from "@openclaw/normalization-core/number-coercion"; +/** systemd service enabled-state and runtime inspection. */ +import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce"; +import { formatErrorMessage } from "../infra/errors.js"; import { parseKeyValueOutput } from "./runtime-parse.js"; import type { GatewayServiceRuntime } from "./service-runtime.js"; import type { diff --git a/src/gateway/auth-rate-limit.test.ts b/src/gateway/auth-rate-limit.test.ts index d31b7160ef6b..d736c70f5ff1 100644 --- a/src/gateway/auth-rate-limit.test.ts +++ b/src/gateway/auth-rate-limit.test.ts @@ -1,7 +1,7 @@ +import { MAX_TIMER_TIMEOUT_MS } from "@openclaw/normalization-core/number-coercion"; // Auth rate-limit tests cover sliding-window, lockout, scope, loopback, and // cleanup behavior shared by gateway secret and device-token authentication. import { afterEach, describe, expect, it, vi } from "vitest"; -import { MAX_TIMER_TIMEOUT_MS } from "../shared/number-coercion.js"; import { AUTH_RATE_LIMIT_SCOPE_DEVICE_TOKEN, AUTH_RATE_LIMIT_SCOPE_HOOK_AUTH, diff --git a/src/gateway/auth-rate-limit.ts b/src/gateway/auth-rate-limit.ts index 85c704013866..ffeb608a1a3e 100644 --- a/src/gateway/auth-rate-limit.ts +++ b/src/gateway/auth-rate-limit.ts @@ -17,7 +17,10 @@ * {@link createAuthRateLimiter} and pass it where needed. */ -import { resolveIntegerOption, resolveTimerTimeoutMs } from "../shared/number-coercion.js"; +import { + resolveIntegerOption, + resolveTimerTimeoutMs, +} from "@openclaw/normalization-core/number-coercion"; import { isLoopbackAddress, resolveClientIp } from "./net.js"; // --------------------------------------------------------------------------- diff --git a/src/gateway/channel-health-monitor.test.ts b/src/gateway/channel-health-monitor.test.ts index 7b10341cb684..6040413e0084 100644 --- a/src/gateway/channel-health-monitor.test.ts +++ b/src/gateway/channel-health-monitor.test.ts @@ -1,10 +1,10 @@ +import { MAX_TIMER_TIMEOUT_MS } from "@openclaw/normalization-core/number-coercion"; /** * Channel health monitor regression tests. */ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import type { ChannelId } from "../channels/plugins/types.public.js"; import type { ChannelAccountSnapshot } from "../channels/plugins/types.public.js"; -import { MAX_TIMER_TIMEOUT_MS } from "../shared/number-coercion.js"; import { startChannelHealthMonitor } from "./channel-health-monitor.js"; import type { ChannelRuntimeSnapshot } from "./server-channel-runtime.types.js"; import type { ChannelManager } from "./server-channels.js"; diff --git a/src/gateway/channel-health-monitor.ts b/src/gateway/channel-health-monitor.ts index 0c0ebc901c98..fb35fc81f86e 100644 --- a/src/gateway/channel-health-monitor.ts +++ b/src/gateway/channel-health-monitor.ts @@ -1,8 +1,8 @@ +import { resolveTimerTimeoutMs } from "@openclaw/normalization-core/number-coercion"; // Gateway channel health monitor. // Periodically evaluates channel account health and restarts stale runtimes. import type { ChannelId } from "../channels/plugins/types.public.js"; import { createSubsystemLogger } from "../logging/subsystem.js"; -import { resolveTimerTimeoutMs } from "../shared/number-coercion.js"; import { DEFAULT_CHANNEL_CONNECT_GRACE_MS, DEFAULT_CHANNEL_STALE_EVENT_THRESHOLD_MS, diff --git a/src/gateway/control-ui-github-api.ts b/src/gateway/control-ui-github-api.ts index 1db8ceaff620..ffbd040c5c7a 100644 --- a/src/gateway/control-ui-github-api.ts +++ b/src/gateway/control-ui-github-api.ts @@ -3,6 +3,7 @@ // bounded bodies, and normalized upstream error statuses. export { isRecord } from "@openclaw/normalization-core/record-coerce"; import { asFiniteNumber } from "@openclaw/normalization-core/number-coercion"; +import { readNonBlankString } from "@openclaw/normalization-core/string-coerce"; import { readResponseWithLimit } from "../infra/http-body.js"; export const GITHUB_API_ORIGIN = "https://api.github.com"; @@ -22,8 +23,8 @@ export class ControlUiGitHubError extends Error { } export function requiredString(record: Record, key: string): string { - const value = record[key]; - if (typeof value !== "string" || !value.trim()) { + const value = readNonBlankString(record[key]); + if (value === undefined) { throw new ControlUiGitHubError(502, `GitHub response omitted ${key}`); } return value; @@ -33,8 +34,7 @@ export function readOptionalGitHubString( record: Record, key: string, ): string | undefined { - const value = record[key]; - return typeof value === "string" && value.trim() ? value : undefined; + return readNonBlankString(record[key]); } export function optionalNumber(record: Record, key: string): number | undefined { diff --git a/src/gateway/control-ui.ts b/src/gateway/control-ui.ts index 503573d2875f..385269f78510 100644 --- a/src/gateway/control-ui.ts +++ b/src/gateway/control-ui.ts @@ -229,7 +229,7 @@ function respondControlUiAssetsUnavailable( respondPlainText(res, 503, message); } -function isValidAgentId(agentId: string): boolean { +function isValidAgentPathSegment(agentId: string): boolean { return /^[a-z0-9][a-z0-9_-]{0,63}$/i.test(agentId); } @@ -816,7 +816,7 @@ export async function handleControlUiAvatarRequest( applyControlUiSecurityHeaders(res); const agentIdParts = pathname.slice(pathWithBase.length).split("/").filter(Boolean); const agentId = agentIdParts[0] ?? ""; - if (agentIdParts.length !== 1 || !agentId || !isValidAgentId(agentId)) { + if (agentIdParts.length !== 1 || !agentId || !isValidAgentPathSegment(agentId)) { respondControlUiNotFound(res); return true; } diff --git a/src/gateway/exec-approval-manager.test.ts b/src/gateway/exec-approval-manager.test.ts index 67f6e5153515..46d7871e94a3 100644 --- a/src/gateway/exec-approval-manager.test.ts +++ b/src/gateway/exec-approval-manager.test.ts @@ -4,10 +4,10 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; +import { MAX_TIMER_TIMEOUT_MS } from "@openclaw/normalization-core/number-coercion"; import { afterEach, describe, expect, it, vi } from "vitest"; import type { ExecApprovalDecision, ExecApprovalRequestPayload } from "../infra/exec-approvals.js"; import type { PluginApprovalRequestPayload } from "../infra/plugin-approvals.js"; -import { MAX_TIMER_TIMEOUT_MS } from "../shared/number-coercion.js"; import { closeOpenClawStateDatabase, openOpenClawStateDatabase, diff --git a/src/gateway/exec-approval-manager.ts b/src/gateway/exec-approval-manager.ts index 8698a3a97f3d..06b24f996614 100644 --- a/src/gateway/exec-approval-manager.ts +++ b/src/gateway/exec-approval-manager.ts @@ -2,7 +2,10 @@ // Tracks pending operator decisions and short-lived resolved approval records. import { randomUUID } from "node:crypto"; import { expectDefined } from "@openclaw/normalization-core"; -import { resolveExpiresAtMsFromDurationMs } from "@openclaw/normalization-core/number-coercion"; +import { + resolveExpiresAtMsFromDurationMs, + resolveTimerTimeoutMs, +} from "@openclaw/normalization-core/number-coercion"; import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce"; import type { ExecutionIdentityAdmissionToken } from "../audit/execution-identity-admission.js"; import { buildApprovalPresentation } from "../infra/approval-presentation.js"; @@ -11,7 +14,6 @@ import type { ExecApprovalDecision, ExecApprovalRequestPayload as InfraExecApprovalRequestPayload, } from "../infra/exec-approvals.js"; -import { resolveTimerTimeoutMs } from "../shared/number-coercion.js"; import type { OpenClawStateDatabaseOptions } from "../state/openclaw-state-db.js"; import type { AgentRuntimeDelegatedAuthority } from "./agent-runtime-identity-token.js"; import { diff --git a/src/gateway/gateway-cli-backend.live-probe-helpers.ts b/src/gateway/gateway-cli-backend.live-probe-helpers.ts index 3f11348d6523..56441175abc7 100644 --- a/src/gateway/gateway-cli-backend.live-probe-helpers.ts +++ b/src/gateway/gateway-cli-backend.live-probe-helpers.ts @@ -1,13 +1,13 @@ // CLI backend live probe helpers run cron/MCP/image probes through the gateway // CLI backend and poll for externally visible live results. import { randomUUID } from "node:crypto"; +import { parseStrictPositiveInteger } from "@openclaw/normalization-core/number-coercion"; import { asNullableRecord as asLoopbackSchemaRecord } from "@openclaw/normalization-core/record-coerce"; import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce"; import { renderCatFacePngBase64 } from "../../test/helpers/live-image-probe.js"; import { AUTOMATIONS_TOOL_NAME } from "../agents/tools/automations-tool-name.js"; import { isTruthyEnvValue } from "../infra/env.js"; import { readResponseWithLimit } from "../infra/http-body.js"; -import { parseStrictPositiveInteger } from "../infra/parse-finite-number.js"; import { sleep } from "../utils/sleep.js"; import type { GatewayClient } from "./client.js"; import { diff --git a/src/gateway/hosted-plugin-surface-url.ts b/src/gateway/hosted-plugin-surface-url.ts index c7c3dd922fdb..2ce818aaff1e 100644 --- a/src/gateway/hosted-plugin-surface-url.ts +++ b/src/gateway/hosted-plugin-surface-url.ts @@ -1,5 +1,5 @@ // Hosted plugin surface URL resolver for gateway-advertised plugin node endpoints. -import { parseStrictPositiveInteger } from "../infra/parse-finite-number.js"; +import { parseStrictPositiveInteger } from "@openclaw/normalization-core/number-coercion"; import { isLoopbackHost } from "./net.js"; type HostSource = string | null | undefined; diff --git a/src/gateway/operator-approval-store.ts b/src/gateway/operator-approval-store.ts index 70be04df44dc..aba79750f669 100644 --- a/src/gateway/operator-approval-store.ts +++ b/src/gateway/operator-approval-store.ts @@ -1,7 +1,9 @@ // Persistent operator approval lifecycle and first-answer-wins transitions. import { createHash } from "node:crypto"; import type { DatabaseSync } from "node:sqlite"; +import { safeParseJson } from "@openclaw/normalization-core/json-coercion"; import { normalizeNullableString } from "@openclaw/normalization-core/string-coerce"; +import { normalizeUniqueTrimmedStringList } from "@openclaw/normalization-core/string-normalization"; import { sql, type Selectable } from "kysely"; import { type DecisionReceiptV1, @@ -261,27 +263,16 @@ function normalizeExecutionIdentityBinding(input: NewOperatorApproval) { } function parseApprovalPresentation(raw: string): ApprovalPresentation | null { - try { - const value: unknown = JSON.parse(raw); - return validateApprovalPresentation(value) ? value : null; - } catch { - return null; - } + const value = safeParseJson(raw); + return validateApprovalPresentation(value) ? value : null; } function parseStringArray(raw: string): string[] | null { - try { - const value: unknown = JSON.parse(raw); - if ( - !Array.isArray(value) || - value.some((entry) => typeof entry !== "string" || !entry.trim()) - ) { - return null; - } - return value as string[]; - } catch { + const value = safeParseJson(raw); + if (!Array.isArray(value) || value.some((entry) => typeof entry !== "string" || !entry.trim())) { return null; } + return value as string[]; } function requireString(value: string, label: string): string { @@ -331,17 +322,6 @@ function decodeOperatorApprovalHistoryCursor(raw: string): OperatorApprovalHisto } } -function normalizeStringArray(values: readonly string[] | undefined): string[] { - const result: string[] = []; - for (const value of values ?? []) { - const normalized = normalizeNullableString(value); - if (normalized && !result.includes(normalized)) { - result.push(normalized); - } - } - return result; -} - function stringifyPresentation(presentation: ApprovalPresentation): string { if (!validateApprovalPresentation(presentation)) { throw new Error("operator approval presentation must match the safe protocol schema"); @@ -1289,8 +1269,10 @@ export function insertOperatorApproval(params: { if (input.presentation.kind !== input.kind) { throw new Error("operator approval kind must match its safe presentation"); } - const reviewerDeviceIdsJson = JSON.stringify(normalizeStringArray(input.reviewerDeviceIds)); - const audienceSessionKeys = normalizeStringArray(input.audienceSessionKeys); + const reviewerDeviceIdsJson = JSON.stringify( + normalizeUniqueTrimmedStringList(input.reviewerDeviceIds), + ); + const audienceSessionKeys = normalizeUniqueTrimmedStringList(input.audienceSessionKeys); if (audienceSessionKeys.length > OPERATOR_APPROVAL_MAX_AUDIENCE_SESSION_KEYS) { throw new Error( `operator approval audience exceeds ${OPERATOR_APPROVAL_MAX_AUDIENCE_SESSION_KEYS} sessions`, diff --git a/src/gateway/question-manager.ts b/src/gateway/question-manager.ts index a2b4b7a982d3..662d6728af7a 100644 --- a/src/gateway/question-manager.ts +++ b/src/gateway/question-manager.ts @@ -1,7 +1,10 @@ // Gateway question manager. // Tracks transient operator questions and short-lived terminal records in memory. import { randomUUID } from "node:crypto"; -import { resolveExpiresAtMsFromDurationMs } from "@openclaw/normalization-core/number-coercion"; +import { + resolveExpiresAtMsFromDurationMs, + resolveTimerTimeoutMs, +} from "@openclaw/normalization-core/number-coercion"; import type { Question, QuestionAnswers, @@ -10,7 +13,6 @@ import type { QuestionResolveResult, QuestionWaitAnswerResult, } from "../../packages/gateway-protocol/src/index.js"; -import { resolveTimerTimeoutMs } from "../shared/number-coercion.js"; /** Grace period for late question.waitAnswer and question.get calls. */ const QUESTION_RESOLVED_ENTRY_GRACE_MS = 15_000; diff --git a/src/gateway/server-discovery-runtime.ts b/src/gateway/server-discovery-runtime.ts index 656819c0d9a5..c9192f3f7e5f 100644 --- a/src/gateway/server-discovery-runtime.ts +++ b/src/gateway/server-discovery-runtime.ts @@ -1,7 +1,10 @@ +import { + clampTimerTimeoutMs, + parseStrictPositiveInteger, +} from "@openclaw/normalization-core/number-coercion"; // Gateway discovery runtime. // Starts local mDNS plugin discovery and optional wide-area DNS-SD publishing. import { isTruthyEnvValue } from "../infra/env.js"; -import { clampTimerTimeoutMs, parseStrictPositiveInteger } from "../infra/parse-finite-number.js"; import { pickPrimaryTailnetIPv4, pickPrimaryTailnetIPv6 } from "../infra/tailnet.js"; import { parseTcpPort } from "../infra/tcp-port.js"; import { resolveWideAreaDiscoveryDomain, writeWideAreaGatewayZone } from "../infra/widearea-dns.js"; diff --git a/src/gateway/server-methods/doctor.ts b/src/gateway/server-methods/doctor.ts index 4a1355dec4d3..7e3e3a314e66 100644 --- a/src/gateway/server-methods/doctor.ts +++ b/src/gateway/server-methods/doctor.ts @@ -4,6 +4,7 @@ import path from "node:path"; import { expectDefined } from "@openclaw/normalization-core"; import { parseDateStringTimestampMs } from "@openclaw/normalization-core/number-coercion"; import { asOptionalRecord } from "@openclaw/normalization-core/record-coerce"; +import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce"; import { ErrorCodes, errorShape } from "../../../packages/gateway-protocol/src/index.js"; import { listAgentIds, @@ -31,7 +32,6 @@ import { repairDreamingArtifacts, writeBackfillDiaryEntries, } from "./doctor.memory-core-runtime.js"; -import { normalizeTrimmedString } from "./record-shared.js"; import type { GatewayRequestContext, GatewayRequestHandlers, RespondFn } from "./types.js"; const MANAGED_DEEP_SLEEP_CRON_NAME = "Memory Dreaming Promotion"; @@ -518,14 +518,14 @@ function isManagedDreamingJob( job: ManagedCronJobLike, params: { name: string; tag: string; payloadText: string }, ): boolean { - const description = normalizeTrimmedString(job.description); + const description = normalizeOptionalString(job.description); if (description?.includes(params.tag)) { return true; } // Older managed jobs may lack the tag, so fall back to the exact system-event signature. - const name = normalizeTrimmedString(job.name); - const payloadKind = normalizeTrimmedString(job.payload?.kind)?.toLowerCase(); - const payloadText = normalizeTrimmedString(job.payload?.text); + const name = normalizeOptionalString(job.name); + const payloadKind = normalizeOptionalString(job.payload?.kind)?.toLowerCase(); + const payloadText = normalizeOptionalString(job.payload?.text); return ( name === params.name && payloadKind === "systemevent" && payloadText === params.payloadText ); @@ -735,7 +735,9 @@ export const doctorHandlers: GatewayRequestHandlers = { } const nowMs = Date.now(); const dreamingConfig = resolveDreamingConfig(cfg); - const workspaceDir = normalizeTrimmedString((status as Record).workspaceDir); + const workspaceDir = normalizeOptionalString( + (status as Record).workspaceDir, + ); const configuredWorkspaces = requestedAgentId ? workspaceDir ? [workspaceDir] diff --git a/src/gateway/server-methods/models-auth-status.test.ts b/src/gateway/server-methods/models-auth-status.test.ts index c232abe979ea..fc800acdf832 100644 --- a/src/gateway/server-methods/models-auth-status.test.ts +++ b/src/gateway/server-methods/models-auth-status.test.ts @@ -2,13 +2,13 @@ // credential cleanup, secret refresh, and provider run abort side effects. import { expectDefined } from "@openclaw/normalization-core"; +import { MAX_DATE_TIMESTAMP_MS } from "@openclaw/normalization-core/number-coercion"; import { createRequireRecord } from "openclaw/plugin-sdk/test-fixtures"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import type { AuthHealthSummary } from "../../agents/auth-health.js"; import type { AuthProfileStore } from "../../agents/auth-profiles.js"; import { NON_ENV_SECRETREF_MARKER } from "../../agents/model-auth-markers.js"; import type { UsageSummary } from "../../infra/provider-usage.types.js"; -import { MAX_DATE_TIMESTAMP_MS } from "../../shared/number-coercion.js"; import { withEnvAsync } from "../../test-utils/env.js"; import { createChatRunState } from "../server-chat-state.js"; import type { GatewayRequestHandlerOptions } from "./types.js"; diff --git a/src/gateway/server-methods/models-auth-status.ts b/src/gateway/server-methods/models-auth-status.ts index c1932f6cdc20..f3711caf5a6c 100644 --- a/src/gateway/server-methods/models-auth-status.ts +++ b/src/gateway/server-methods/models-auth-status.ts @@ -4,6 +4,7 @@ import { findNormalizedProviderValue, normalizeProviderId, } from "@openclaw/model-catalog-core/provider-id"; +import { asDateTimestampMs } from "@openclaw/normalization-core/number-coercion"; import { ErrorCodes, errorShape } from "../../../packages/gateway-protocol/src/index.js"; import { type AuthHealthSummary, @@ -49,7 +50,6 @@ import { providerUsageLabel, resolveUsageProviderId } from "../../infra/provider import type { UsageProviderId } from "../../infra/provider-usage.types.js"; import { createSubsystemLogger } from "../../logging/subsystem.js"; import { refreshActiveProviderAuthRuntimeSnapshot } from "../../secrets/runtime.js"; -import { asDateTimestampMs } from "../../shared/number-coercion.js"; import { abortChatRunsForProvider, type ChatAbortOps } from "../chat-abort.js"; import { formatForLog } from "../ws-log.js"; import { diff --git a/src/gateway/server-methods/plugins.test.ts b/src/gateway/server-methods/plugins.test.ts index 8430dc300639..cf00b34ef201 100644 --- a/src/gateway/server-methods/plugins.test.ts +++ b/src/gateway/server-methods/plugins.test.ts @@ -1,6 +1,6 @@ // Plugin management Gateway handler tests cover DTO mapping, trust errors, and reload planning. -import { coerceErrorMessage, expectDefined } from "@openclaw/normalization-core"; +import { expectDefined } from "@openclaw/normalization-core"; import { beforeEach, describe, expect, it, vi } from "vitest"; const managementMocks = vi.hoisted(() => { @@ -38,7 +38,6 @@ const searchMock = vi.hoisted(() => vi.fn()); vi.mock("../../plugins/management-service.js", () => ({ ManagedPluginLifecycleError: managementMocks.ManagedPluginLifecycleError, - formatManagedPluginLifecycleError: coerceErrorMessage, installManagedPlugin: (...args: unknown[]) => managementMocks.install(...args), listManagedPlugins: (...args: unknown[]) => managementMocks.list(...args), setManagedPluginEnabled: (...args: unknown[]) => managementMocks.setEnabled(...args), diff --git a/src/gateway/server-methods/plugins.ts b/src/gateway/server-methods/plugins.ts index 6de804aa0824..77a8f7535233 100644 --- a/src/gateway/server-methods/plugins.ts +++ b/src/gateway/server-methods/plugins.ts @@ -12,9 +12,9 @@ import { validatePluginsUninstallParams, } from "../../../packages/gateway-protocol/src/index.js"; import type { OpenClawConfig } from "../../config/types.openclaw.js"; +import { formatErrorMessage } from "../../infra/errors.js"; import { searchInstallablePluginPackages } from "../../plugins/catalog-search.js"; import { - formatManagedPluginLifecycleError, installManagedPlugin, listManagedPlugins, ManagedPluginLifecycleError, @@ -51,11 +51,7 @@ export const pluginsHandlers: GatewayRequestHandlers = { try { respond(true, await listManagedPlugins({ config: context.getRuntimeConfig() }), undefined); } catch (error) { - respond( - false, - undefined, - errorShape(ErrorCodes.UNAVAILABLE, formatManagedPluginLifecycleError(error)), - ); + respond(false, undefined, errorShape(ErrorCodes.UNAVAILABLE, formatErrorMessage(error))); } }, "plugins.search": async ({ params, respond }) => { @@ -106,11 +102,7 @@ export const pluginsHandlers: GatewayRequestHandlers = { undefined, ); } catch (error) { - respond( - false, - undefined, - errorShape(ErrorCodes.UNAVAILABLE, formatManagedPluginLifecycleError(error)), - ); + respond(false, undefined, errorShape(ErrorCodes.UNAVAILABLE, formatErrorMessage(error))); } }, "plugins.install": async ({ params, respond }) => { @@ -149,7 +141,7 @@ export const pluginsHandlers: GatewayRequestHandlers = { lifecycleError?.kind === "invalid-request" ? ErrorCodes.INVALID_REQUEST : ErrorCodes.UNAVAILABLE, - formatManagedPluginLifecycleError(error), + formatErrorMessage(error), details ? { details } : undefined, ), ); @@ -181,7 +173,7 @@ export const pluginsHandlers: GatewayRequestHandlers = { lifecycleError?.kind === "invalid-request" ? ErrorCodes.INVALID_REQUEST : ErrorCodes.UNAVAILABLE, - formatManagedPluginLifecycleError(error), + formatErrorMessage(error), ), ); } @@ -219,7 +211,7 @@ export const pluginsHandlers: GatewayRequestHandlers = { lifecycleError?.kind === "invalid-request" ? ErrorCodes.INVALID_REQUEST : ErrorCodes.UNAVAILABLE, - formatManagedPluginLifecycleError(error), + formatErrorMessage(error), ), ); } diff --git a/src/gateway/server-methods/push.ts b/src/gateway/server-methods/push.ts index b02184c895fc..f4bcc55fbda1 100644 --- a/src/gateway/server-methods/push.ts +++ b/src/gateway/server-methods/push.ts @@ -1,6 +1,9 @@ // Push gateway methods send APNs/web-push test notifications and manage web // push subscriptions/VAPID public-key access for UI clients. -import { normalizeStringifiedOptionalString } from "@openclaw/normalization-core/string-coerce"; +import { + normalizeOptionalString, + normalizeStringifiedOptionalString, +} from "@openclaw/normalization-core/string-coerce"; import { ErrorCodes, errorShape, @@ -26,7 +29,6 @@ import { resolveVapidKeys, } from "../../infra/push-web.js"; import { respondInvalidParams, respondUnavailableOnThrow } from "./nodes.helpers.js"; -import { normalizeTrimmedString } from "./record-shared.js"; import type { GatewayRequestHandlers } from "./types.js"; export const pushHandlers: GatewayRequestHandlers = { @@ -46,8 +48,8 @@ export const pushHandlers: GatewayRequestHandlers = { return; } - const title = normalizeTrimmedString(params.title) ?? "OpenClaw"; - const body = normalizeTrimmedString(params.body) ?? `Push test for node ${nodeId}`; + const title = normalizeOptionalString(params.title) ?? "OpenClaw"; + const body = normalizeOptionalString(params.body) ?? `Push test for node ${nodeId}`; await respondUnavailableOnThrow(respond, async () => { const registration = await loadApnsRegistration(nodeId); @@ -187,8 +189,8 @@ export const pushHandlers: GatewayRequestHandlers = { return; } - const title = normalizeTrimmedString(params.title) ?? "OpenClaw"; - const body = normalizeTrimmedString(params.body) ?? "Web push test notification"; + const title = normalizeOptionalString(params.title) ?? "OpenClaw"; + const body = normalizeOptionalString(params.body) ?? "Web push test notification"; await respondUnavailableOnThrow(respond, async () => { const results = await broadcastWebPush({ title, body }); diff --git a/src/gateway/server-methods/record-shared.ts b/src/gateway/server-methods/record-shared.ts deleted file mode 100644 index 4b5768582ee4..000000000000 --- a/src/gateway/server-methods/record-shared.ts +++ /dev/null @@ -1,2 +0,0 @@ -/** Non-empty trimmed-string normalization shared by gateway request handlers. */ -export { normalizeOptionalString as normalizeTrimmedString } from "@openclaw/normalization-core/string-coerce"; diff --git a/src/gateway/session-utils.fs.test.ts b/src/gateway/session-utils.fs.test.ts index 287dc59491af..7a6945b556c7 100644 --- a/src/gateway/session-utils.fs.test.ts +++ b/src/gateway/session-utils.fs.test.ts @@ -1,6 +1,10 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; +import { + estimateStringChars, + estimateTokensFromChars, +} from "@openclaw/normalization-core/cjk-chars"; import { SessionManager } from "openclaw/plugin-sdk/agent-sessions"; // Session filesystem utility tests cover transcript reading, usage extraction, // preview rows, message counts, title fields, and archive candidate resolution. @@ -12,7 +16,6 @@ import { openFileBackedSessionManagerForTest, } from "../../test/helpers/session-manager-file-fixture.js"; import { withEnv, withEnvAsync } from "../test-utils/env.js"; -import { estimateStringChars, estimateTokensFromChars } from "../utils/cjk-chars.js"; import { projectChatDisplayMessages } from "./chat-display-projection.js"; import { createToolSummaryPreviewTranscriptLines } from "./session-preview.test-helpers.js"; import { diff --git a/src/gateway/session-utils.fs.ts b/src/gateway/session-utils.fs.ts index c039b38bde6e..de706fbdf1fd 100644 --- a/src/gateway/session-utils.fs.ts +++ b/src/gateway/session-utils.fs.ts @@ -3,6 +3,10 @@ import fs from "node:fs"; import readline from "node:readline"; import { expectDefined } from "@openclaw/normalization-core"; +import { + estimateStringChars, + estimateTokensFromChars, +} from "@openclaw/normalization-core/cjk-chars"; import { asNonNegativeFiniteNumber, asPositiveFiniteNumber as resolvePositiveUsageNumber, @@ -26,7 +30,6 @@ import { jsonUtf8Bytes } from "../infra/json-utf8-bytes.js"; import { pruneMapToMaxSize } from "../infra/map-size.js"; import { extractAssistantPhaseText } from "../shared/chat-message-content.js"; import { truncateUtf16Safe } from "../utils.js"; -import { estimateStringChars, estimateTokensFromChars } from "../utils/cjk-chars.js"; import { stripInlineDirectiveTagsForDisplay } from "../utils/directive-tags.js"; import { extractToolCallNames, hasToolCall } from "../utils/transcript-tools.js"; import { stripEnvelope } from "./chat-sanitize.js"; diff --git a/src/hooks/fire-and-forget.test.ts b/src/hooks/fire-and-forget.test.ts index de796c2e844d..2cb46c3f8630 100644 --- a/src/hooks/fire-and-forget.test.ts +++ b/src/hooks/fire-and-forget.test.ts @@ -1,6 +1,6 @@ +import { MAX_TIMER_TIMEOUT_MS } from "@openclaw/normalization-core/number-coercion"; // Fire-and-forget hook tests cover async hook execution without blocking callers. import { describe, expect, it, vi } from "vitest"; -import { MAX_TIMER_TIMEOUT_MS } from "../shared/number-coercion.js"; import { fireAndForgetBoundedHook, fireAndForgetHook } from "./fire-and-forget.js"; function requireFirstLog(logger: ReturnType): string { diff --git a/src/hooks/fire-and-forget.ts b/src/hooks/fire-and-forget.ts index 2aaaedcd381a..32d2245be822 100644 --- a/src/hooks/fire-and-forget.ts +++ b/src/hooks/fire-and-forget.ts @@ -1,9 +1,9 @@ +import { resolveTimerTimeoutMs } from "@openclaw/normalization-core/number-coercion"; // Fire-and-forget hook helpers schedule hook work without blocking hot paths. import { truncateUtf16Safe } from "@openclaw/normalization-core/utf16-slice"; import { logVerbose } from "../globals.js"; import { formatErrorMessage } from "../infra/errors.js"; import { resolveGlobalSingleton } from "../shared/global-singleton.js"; -import { resolveTimerTimeoutMs } from "../shared/number-coercion.js"; const DEFAULT_MAX_CONCURRENT_FIRE_AND_FORGET_HOOKS = 16; const DEFAULT_MAX_QUEUED_FIRE_AND_FORGET_HOOKS = 256; diff --git a/src/infra/backoff.test.ts b/src/infra/backoff.test.ts index e96eaf1f1c11..c4a723c0ab66 100644 --- a/src/infra/backoff.test.ts +++ b/src/infra/backoff.test.ts @@ -1,6 +1,6 @@ +import { MAX_TIMER_TIMEOUT_MS } from "@openclaw/normalization-core/number-coercion"; // Covers retry backoff calculation and abortable sleep behavior. import { describe, expect, it, vi } from "vitest"; -import { MAX_TIMER_TIMEOUT_MS } from "../shared/number-coercion.js"; import { computeBackoff, sleepWithAbort, type BackoffPolicy } from "./backoff.js"; async function expectAbortedSleep(promise: Promise): Promise { diff --git a/src/infra/bonjour-discovery.ts b/src/infra/bonjour-discovery.ts index 1e04b387a388..07c141bea3ca 100644 --- a/src/infra/bonjour-discovery.ts +++ b/src/infra/bonjour-discovery.ts @@ -1,5 +1,6 @@ // Discovers gateways over Bonjour and normalizes service records. import { expectDefined } from "@openclaw/normalization-core"; +import { parseStrictInteger } from "@openclaw/normalization-core/number-coercion"; import { normalizeOptionalLowercaseString } from "@openclaw/normalization-core/string-coerce"; import { normalizeStringEntries, @@ -7,7 +8,6 @@ import { } from "@openclaw/normalization-core/string-normalization"; import pLimit from "p-limit"; import { runCommandWithTimeout } from "../process/exec.js"; -import { parseStrictInteger } from "./parse-finite-number.js"; import { isTailnetIPv4 } from "./tailnet.js"; import { resolveWideAreaDiscoveryDomain } from "./widearea-dns.js"; diff --git a/src/infra/clawhub-artifacts.ts b/src/infra/clawhub-artifacts.ts index 5e9ce182b17a..0a9f56ce9260 100644 --- a/src/infra/clawhub-artifacts.ts +++ b/src/infra/clawhub-artifacts.ts @@ -1,6 +1,7 @@ // ClawHub package, skill, resolver URL, and GitHub archive downloads. import { createHash } from "node:crypto"; import fs from "node:fs/promises"; +import { parseStrictPositiveInteger } from "@openclaw/normalization-core/number-coercion"; import { normalizeLowercaseStringOrEmpty, normalizeOptionalString, @@ -13,7 +14,6 @@ import { type ClawHubFetch, } from "./clawhub-client.js"; import { sha256Base64, sha256Hex } from "./crypto-digest.js"; -import { parseStrictPositiveInteger } from "./parse-finite-number.js"; import { createTempDownloadTarget } from "./temp-download.js"; const DEFAULT_GITHUB_CODELOAD_URL = "https://codeload.github.com"; diff --git a/src/infra/clawhub-client.ts b/src/infra/clawhub-client.ts index 105cb03ddd34..adc0c5ef28cc 100644 --- a/src/infra/clawhub-client.ts +++ b/src/infra/clawhub-client.ts @@ -2,12 +2,14 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; -import { resolveTimerTimeoutMs } from "@openclaw/normalization-core/number-coercion"; +import { + parseStrictNonNegativeInteger, + resolveTimerTimeoutMs, +} from "@openclaw/normalization-core/number-coercion"; import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce"; import { retryClawHubRead } from "./clawhub-retry.js"; import { isTruthyEnvValue } from "./env.js"; import { readResponseTextSnippet, readResponseWithLimit } from "./http-body.js"; -import { parseStrictNonNegativeInteger } from "./parse-finite-number.js"; const DEFAULT_CLAWHUB_URL = "https://clawhub.ai"; const DEFAULT_FETCH_TIMEOUT_MS = 30_000; diff --git a/src/infra/command-analysis/risks.ts b/src/infra/command-analysis/risks.ts index 7fa8dd5e4830..f517a05c85d8 100644 --- a/src/infra/command-analysis/risks.ts +++ b/src/infra/command-analysis/risks.ts @@ -1,3 +1,4 @@ +import { parseStrictPositiveInteger } from "@openclaw/normalization-core/number-coercion"; // Command risk detection follows nested carriers, shell wrappers, and inline // interpreter eval paths used by approval policy and command explanations. import { uniqueStrings } from "@openclaw/normalization-core/string-normalization"; @@ -12,7 +13,6 @@ import { import { unwrapKnownDispatchWrapperInvocation } from "../dispatch-wrapper-resolution.js"; import type { ExecCommandSegment } from "../exec-approvals-analysis.js"; import { normalizeExecutableToken } from "../exec-wrapper-resolution.js"; -import { parseStrictPositiveInteger } from "../parse-finite-number.js"; import { POSIX_INLINE_COMMAND_FLAGS, resolveInlineCommandMatch } from "../shell-inline-command.js"; import { extractShellWrapperInlineCommand, diff --git a/src/infra/heartbeat-wake.ts b/src/infra/heartbeat-wake.ts index b388aa5ae120..231a90b16ad7 100644 --- a/src/infra/heartbeat-wake.ts +++ b/src/infra/heartbeat-wake.ts @@ -1,6 +1,6 @@ +import { resolveTimerTimeoutMs } from "@openclaw/normalization-core/number-coercion"; // Tracks heartbeat wake requests, busy skips, and retry timing. import { runWithGatewayIndependentRootWorkAdmission } from "../process/gateway-work-admission.js"; -import { resolveTimerTimeoutMs } from "../shared/number-coercion.js"; import { normalizeHeartbeatWakeReason } from "./heartbeat-reason.js"; import type { HeartbeatRunResult, diff --git a/src/infra/http-body.ts b/src/infra/http-body.ts index 6cc016b208f5..86d285adb4b7 100644 --- a/src/infra/http-body.ts +++ b/src/infra/http-body.ts @@ -2,11 +2,13 @@ import type { IncomingMessage, ServerResponse } from "node:http"; import { clearTimeout as clearNodeTimeout, setTimeout as setNodeTimeout } from "node:timers"; import { decodeTextPrefix } from "@openclaw/normalization-core"; -import { resolveTimerTimeoutMs } from "@openclaw/normalization-core/number-coercion"; +import { + parseStrictNonNegativeInteger, + resolveTimerTimeoutMs, +} from "@openclaw/normalization-core/number-coercion"; import { truncateUtf16Safe } from "@openclaw/normalization-core/utf16-slice"; import { formatErrorMessage } from "./errors.js"; import { readChunkWithIdleTimeout, withResponseBodyTimeout } from "./http-response-body-timeout.js"; -import { parseStrictNonNegativeInteger } from "./parse-finite-number.js"; export { readChunkWithIdleTimeout } from "./http-response-body-timeout.js"; diff --git a/src/infra/npm-managed-root.ts b/src/infra/npm-managed-root.ts index b8450aeed787..12cb1f37f25c 100644 --- a/src/infra/npm-managed-root.ts +++ b/src/infra/npm-managed-root.ts @@ -3,7 +3,7 @@ import type { Stats } from "node:fs"; import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; -import { isRecord } from "@openclaw/normalization-core/record-coerce"; +import { filterStringRecord, isRecord } from "@openclaw/normalization-core/record-coerce"; import { normalizeOptionalString as readOptionalString } from "@openclaw/normalization-core/string-coerce"; import { parse as parseYaml } from "yaml"; import { runCommandWithTimeout } from "../process/exec.js"; @@ -65,16 +65,7 @@ type ManagedNpmRootRunCommand = typeof runCommandWithTimeout; type ManagedNpmRootOpenClawHostState = "none" | "managed-active-host" | "linked-active-host"; function readDependencyRecord(value: unknown): Record { - if (!isRecord(value)) { - return {}; - } - const dependencies: Record = {}; - for (const [key, raw] of Object.entries(value)) { - if (typeof raw === "string") { - dependencies[key] = raw; - } - } - return dependencies; + return filterStringRecord(value) ?? {}; } function isSafePackageName(name: string): boolean { diff --git a/src/infra/outbound/message-gateway-options.test.ts b/src/infra/outbound/message-gateway-options.test.ts index 062e91530993..0d5576977538 100644 --- a/src/infra/outbound/message-gateway-options.test.ts +++ b/src/infra/outbound/message-gateway-options.test.ts @@ -1,7 +1,7 @@ +import { MAX_TIMER_TIMEOUT_MS } from "@openclaw/normalization-core/number-coercion"; // Covers outbound gateway option defaults, timeout clamping, and backend URL // suppression. import { describe, expect, it } from "vitest"; -import { MAX_TIMER_TIMEOUT_MS } from "../../shared/number-coercion.js"; import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../../utils/message-channel.js"; import { resolveOutboundMessageGatewayOptions } from "./message-gateway-options.js"; diff --git a/src/infra/outbound/message-gateway-options.ts b/src/infra/outbound/message-gateway-options.ts index d103197b0401..c890c52cbe43 100644 --- a/src/infra/outbound/message-gateway-options.ts +++ b/src/infra/outbound/message-gateway-options.ts @@ -1,6 +1,6 @@ // Gateway option normalization hides transport URL details for backend/managed // gateway clients and clamps timeout values. -import { resolveTimerTimeoutMs } from "../../shared/number-coercion.js"; +import { resolveTimerTimeoutMs } from "@openclaw/normalization-core/number-coercion"; import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES, diff --git a/src/infra/parse-finite-number.test.ts b/src/infra/parse-finite-number.test.ts deleted file mode 100644 index e1415f9e790f..000000000000 --- a/src/infra/parse-finite-number.test.ts +++ /dev/null @@ -1,89 +0,0 @@ -// Covers strict finite number parsing helpers. -import { describe, expect, it } from "vitest"; -import { - parseFiniteNumber, - parseStrictFiniteNumber, - parseStrictInteger, - parseStrictNonNegativeInteger, - parseStrictPositiveInteger, -} from "./parse-finite-number.js"; - -function expectParserCases( - parse: (value: unknown) => T | undefined, - cases: Array<{ value: unknown; expected: T | undefined }>, -) { - for (const { value, expected } of cases) { - expect(parse(value)).toBe(expected); - } -} - -describe("parseFiniteNumber", () => { - it("parses finite values and rejects invalid inputs", () => { - expectParserCases(parseFiniteNumber, [ - { value: 42, expected: 42 }, - { value: "3.14", expected: 3.14 }, - { value: " 3.14ms", expected: undefined }, - { value: "+7", expected: 7 }, - { value: "1e3", expected: 1000 }, - { value: Number.NaN, expected: undefined }, - { value: Number.POSITIVE_INFINITY, expected: undefined }, - { value: "not-a-number", expected: undefined }, - { value: " ", expected: undefined }, - { value: "", expected: undefined }, - { value: null, expected: undefined }, - ]); - }); -}); - -describe("parseStrictInteger", () => { - it("parses strict integers and rejects non-integers", () => { - expectParserCases(parseStrictInteger, [ - { value: "42", expected: 42 }, - { value: " -7 ", expected: -7 }, - { value: 12, expected: 12 }, - { value: "+9", expected: 9 }, - { value: "42ms", expected: undefined }, - { value: "0abc", expected: undefined }, - { value: "1.5", expected: undefined }, - { value: "1e3", expected: undefined }, - { value: " ", expected: undefined }, - { value: Number.MAX_SAFE_INTEGER + 1, expected: undefined }, - ]); - }); -}); - -describe("parseStrictFiniteNumber", () => { - it("parses full finite numbers and rejects partial tokens", () => { - expectParserCases(parseStrictFiniteNumber, [ - { value: "42", expected: 42 }, - { value: "3.14", expected: 3.14 }, - { value: ".5", expected: 0.5 }, - { value: "1e3", expected: 1000 }, - { value: "3.14ms", expected: undefined }, - { value: "0abc", expected: undefined }, - { value: "0x10", expected: undefined }, - { value: " ", expected: undefined }, - { value: Number.POSITIVE_INFINITY, expected: undefined }, - ]); - }); -}); - -describe("parseStrictPositiveInteger", () => { - it("enforces positive integers", () => { - expectParserCases(parseStrictPositiveInteger, [ - { value: "9", expected: 9 }, - { value: "0", expected: undefined }, - { value: "-1", expected: undefined }, - ]); - }); -}); - -describe("parseStrictNonNegativeInteger", () => { - it("allows zero and positive integers only", () => { - expectParserCases(parseStrictNonNegativeInteger, [ - { value: "0", expected: 0 }, - { value: "9", expected: 9 }, - { value: "-1", expected: undefined }, - ]); - }); -}); diff --git a/src/infra/parse-finite-number.ts b/src/infra/parse-finite-number.ts deleted file mode 100644 index 7cb3f4673493..000000000000 --- a/src/infra/parse-finite-number.ts +++ /dev/null @@ -1,18 +0,0 @@ -// Number parsing facade for legacy infra imports; implementation lives in -// normalization-core so config, timers, and CLI parsing share one contract. -export { - parseFiniteNumber, - parseStrictFiniteNumber, - parseStrictInteger, - parseStrictNonNegativeInteger, - parseStrictPositiveInteger, - clampTimerTimeoutMs, - finiteSecondsToTimerSafeMilliseconds, - MAX_TIMER_TIMEOUT_MS, - MAX_TIMER_TIMEOUT_SECONDS, - positiveSecondsToSafeMilliseconds, - nonNegativeSecondsToSafeMilliseconds, - resolveExpiresAtMsFromDurationSeconds, - resolveExpiresAtMsFromDurationOrEpoch, - resolveExpiresAtMsFromEpochSeconds, -} from "../../packages/normalization-core/src/number-coercion.js"; diff --git a/src/infra/ports-inspect.ts b/src/infra/ports-inspect.ts index 8a51e66e8647..cfb207c166ac 100644 --- a/src/infra/ports-inspect.ts +++ b/src/infra/ports-inspect.ts @@ -2,10 +2,10 @@ import net from "node:net"; import os from "node:os"; import { expectDefined } from "@openclaw/normalization-core"; +import { parseStrictPositiveInteger } from "@openclaw/normalization-core/number-coercion"; import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce"; import pMap from "p-map"; import { runCommandWithTimeout } from "../process/exec.js"; -import { parseStrictPositiveInteger } from "./parse-finite-number.js"; import { buildPortHints } from "./ports-format.js"; import { parseLsofListenerRecordsByPort, diff --git a/src/infra/ports-lsof-listeners.ts b/src/infra/ports-lsof-listeners.ts index 8d0a03dec295..54563ec95d77 100644 --- a/src/infra/ports-lsof-listeners.ts +++ b/src/infra/ports-lsof-listeners.ts @@ -1,4 +1,4 @@ -import { parseStrictPositiveInteger } from "./parse-finite-number.js"; +import { parseStrictPositiveInteger } from "@openclaw/normalization-core/number-coercion"; import { parseTcpEndpoint } from "./ports-netstat.js"; import type { PortListener } from "./ports-types.js"; diff --git a/src/infra/ports-netstat.ts b/src/infra/ports-netstat.ts index 9d94c128e2c7..bc7c0662f032 100644 --- a/src/infra/ports-netstat.ts +++ b/src/infra/ports-netstat.ts @@ -1,5 +1,5 @@ import { expectDefined } from "@openclaw/normalization-core"; -import { parseStrictPositiveInteger } from "./parse-finite-number.js"; +import { parseStrictPositiveInteger } from "@openclaw/normalization-core/number-coercion"; import type { PortListener } from "./ports-types.js"; type WindowsNetstatListener = PortListener & { pid: number; address: string }; diff --git a/src/infra/provider-usage.fetch.codex.ts b/src/infra/provider-usage.fetch.codex.ts index 0f3753905b8b..6fc0718a1a20 100644 --- a/src/infra/provider-usage.fetch.codex.ts +++ b/src/infra/provider-usage.fetch.codex.ts @@ -1,7 +1,7 @@ +import { parseStrictFiniteNumber } from "@openclaw/normalization-core/number-coercion"; // Fetches Codex provider usage windows. import { resolveProviderRequestHeaders } from "../agents/provider-request-config.js"; import { cancelUnreadResponseBody } from "./http-body.js"; -import { parseStrictFiniteNumber } from "./parse-finite-number.js"; import { buildUsageHttpErrorSnapshot, fetchJson, diff --git a/src/infra/provider-usage.fetch.shared.ts b/src/infra/provider-usage.fetch.shared.ts index 573edfc88eee..f1330a93b1ca 100644 --- a/src/infra/provider-usage.fetch.shared.ts +++ b/src/infra/provider-usage.fetch.shared.ts @@ -22,7 +22,7 @@ export async function fetchJson( return await fetchFn(url, { ...init, signal }); } -export { parseFiniteNumber } from "./parse-finite-number.js"; +export { parseFiniteNumber } from "@openclaw/normalization-core/number-coercion"; /** Parses a provider reset-time string without leaking an invalid Date timestamp. */ export function parseUsageResetAt(value: unknown): number | undefined { diff --git a/src/infra/provider-usage.shared.test.ts b/src/infra/provider-usage.shared.test.ts index 8ccd719b2e63..9389d9f404f6 100644 --- a/src/infra/provider-usage.shared.test.ts +++ b/src/infra/provider-usage.shared.test.ts @@ -1,6 +1,6 @@ +import { MAX_TIMER_TIMEOUT_MS } from "@openclaw/normalization-core/number-coercion"; // Covers shared provider usage helpers. import { afterEach, describe, expect, it, vi } from "vitest"; -import { MAX_TIMER_TIMEOUT_MS } from "../shared/number-coercion.js"; import { clampPercent, raceUsageTimeout, resolveUsageProviderId } from "./provider-usage.shared.js"; describe("provider-usage.shared", () => { diff --git a/src/infra/provider-usage.shared.ts b/src/infra/provider-usage.shared.ts index 250fd3f3baca..34544b8341b2 100644 --- a/src/infra/provider-usage.shared.ts +++ b/src/infra/provider-usage.shared.ts @@ -1,6 +1,6 @@ // Shared provider usage labels, ids, and timeout helpers. import { normalizeProviderId } from "@openclaw/model-catalog-core/provider-id"; -import { resolveTimerTimeoutMs } from "../shared/number-coercion.js"; +import { resolveTimerTimeoutMs } from "@openclaw/normalization-core/number-coercion"; import type { UsageProviderId } from "./provider-usage.types.js"; /** Default timeout for provider usage collection. */ diff --git a/src/infra/restart-stale-pids.ts b/src/infra/restart-stale-pids.ts index 5d67f7ad20a1..f40b83e3850e 100644 --- a/src/infra/restart-stale-pids.ts +++ b/src/infra/restart-stale-pids.ts @@ -2,6 +2,7 @@ import { spawnSync } from "node:child_process"; import { readFileSync } from "node:fs"; import path from "node:path"; +import { parseStrictPositiveInteger } from "@openclaw/normalization-core/number-coercion"; import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce"; import { uniqueValues } from "@openclaw/normalization-core/string-normalization"; import { resolveGatewayPort } from "../config/paths.js"; @@ -10,7 +11,6 @@ import { killProcessTree } from "../process/kill-tree.js"; import { sleep } from "../utils/sleep.js"; import { formatErrorMessage, hasErrnoCode } from "./errors.js"; import { isGatewayArgv, parseProcCmdline } from "./gateway-process-argv.js"; -import { parseStrictPositiveInteger } from "./parse-finite-number.js"; import { resolveLsofCommandSync } from "./ports-lsof.js"; import { spawnPsSync } from "./spawn-ps.js"; import { getWindowsInstallRoots } from "./windows-install-roots.js"; diff --git a/src/infra/restart.ts b/src/infra/restart.ts index 159b3f54e8b3..f5409c7c6154 100644 --- a/src/infra/restart.ts +++ b/src/infra/restart.ts @@ -2,6 +2,7 @@ import { spawnSync } from "node:child_process"; import os from "node:os"; import path from "node:path"; +import { resolveTimerTimeoutMs } from "@openclaw/normalization-core/number-coercion"; import { resolveGatewayLaunchAgentLabel, resolveGatewaySystemdServiceName, @@ -15,7 +16,6 @@ import { runWithGatewayIndependentRootWorkAdmission, type GatewayRestartSignalAdmissionLease, } from "../process/gateway-work-admission.js"; -import { resolveTimerTimeoutMs } from "../shared/number-coercion.js"; import { formatErrorMessage } from "./errors.js"; import { type GatewayRestartIntent, normalizeRestartIntentReason } from "./restart-intent.js"; import { cleanStaleGatewayProcessesSync } from "./restart-stale-pids.js"; diff --git a/src/infra/retry.test.ts b/src/infra/retry.test.ts index a12ea87e2a3a..9395a6b3aa1c 100644 --- a/src/infra/retry.test.ts +++ b/src/infra/retry.test.ts @@ -1,6 +1,6 @@ +import { MAX_TIMER_TIMEOUT_MS } from "@openclaw/normalization-core/number-coercion"; // Tests retry backoff timing and cancellation behavior. import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { MAX_TIMER_TIMEOUT_MS } from "../shared/number-coercion.js"; import { getRetryAttemptErrors } from "./retry-attempt-errors.js"; import { resolveRetryConfig, retryAsync } from "./retry.js"; diff --git a/src/infra/shell-env.ts b/src/infra/shell-env.ts index 7bb5ed7d4add..2f4221d6bab9 100644 --- a/src/infra/shell-env.ts +++ b/src/infra/shell-env.ts @@ -3,13 +3,15 @@ import { execFileSync } from "node:child_process"; import fs from "node:fs"; import os from "node:os"; import path from "node:path"; -import { resolveTimerTimeoutMs } from "@openclaw/normalization-core/number-coercion"; +import { + parseStrictNonNegativeInteger, + resolveTimerTimeoutMs, +} from "@openclaw/normalization-core/number-coercion"; import { isTruthyEnvValue } from "./env.js"; import { formatErrorMessage } from "./errors.js"; import { resolveExecutableFromPathEnv } from "./executable-path.js"; import { sanitizeHostExecEnv } from "./host-env-security.js"; import { pruneMapToMaxSize } from "./map-size.js"; -import { parseStrictNonNegativeInteger } from "./parse-finite-number.js"; const DEFAULT_TIMEOUT_MS = 15_000; const DEFAULT_MAX_BUFFER_BYTES = 2 * 1024 * 1024; diff --git a/src/infra/sqlite-wal.test.ts b/src/infra/sqlite-wal.test.ts index 36c4737dec00..4e55ab5de950 100644 --- a/src/infra/sqlite-wal.test.ts +++ b/src/infra/sqlite-wal.test.ts @@ -5,9 +5,9 @@ import os from "node:os"; import path from "node:path"; import type { DatabaseSync } from "node:sqlite"; import { expectDefined } from "@openclaw/normalization-core"; +import { MAX_TIMER_TIMEOUT_MS } from "@openclaw/normalization-core/number-coercion"; import { afterEach, describe, expect, it, vi } from "vitest"; import { useAutoCleanupTempDirTracker } from "../../test/helpers/temp-dir.js"; -import { MAX_TIMER_TIMEOUT_MS } from "../shared/number-coercion.js"; import { requireNodeSqlite } from "./node-sqlite.js"; import { configureSqliteConnectionPragmas, diff --git a/src/infra/sqlite-wal.ts b/src/infra/sqlite-wal.ts index 264a94d21dc6..6b278fd6cf3e 100644 --- a/src/infra/sqlite-wal.ts +++ b/src/infra/sqlite-wal.ts @@ -2,8 +2,8 @@ import fs from "node:fs"; import path from "node:path"; import type { DatabaseSync } from "node:sqlite"; +import { MAX_TIMER_TIMEOUT_MS } from "@openclaw/normalization-core/number-coercion"; import type { Result } from "@openclaw/normalization-core/result"; -import { MAX_TIMER_TIMEOUT_MS } from "../shared/number-coercion.js"; import { isSqliteLockError } from "./sqlite-transaction.js"; // WAL maintenance configures SQLite write-ahead logging and schedules bounded diff --git a/src/infra/ssh-config.ts b/src/infra/ssh-config.ts index db810b04ec08..ecfeba095c6a 100644 --- a/src/infra/ssh-config.ts +++ b/src/infra/ssh-config.ts @@ -1,6 +1,6 @@ +import { parseStrictPositiveInteger } from "@openclaw/normalization-core/number-coercion"; // Reads effective SSH target config from the local ssh client. import { runCommandWithTimeout } from "../process/exec.js"; -import { parseStrictPositiveInteger } from "./parse-finite-number.js"; import { resolveSshClient } from "./ssh-client.js"; import type { SshParsedTarget } from "./ssh-tunnel.js"; diff --git a/src/infra/ssh-tunnel.ts b/src/infra/ssh-tunnel.ts index 512e2d97b271..0a214afb6378 100644 --- a/src/infra/ssh-tunnel.ts +++ b/src/infra/ssh-tunnel.ts @@ -1,9 +1,9 @@ // Starts and monitors SSH tunnels for remote gateway access. import { spawn } from "node:child_process"; import net from "node:net"; +import { parseStrictPositiveInteger } from "@openclaw/normalization-core/number-coercion"; import { normalizeStringEntries } from "@openclaw/normalization-core/string-normalization"; import { formatErrorMessage, isErrno } from "./errors.js"; -import { parseStrictPositiveInteger } from "./parse-finite-number.js"; import { ensurePortAvailable, PortInUseError } from "./ports.js"; import { resolveSshClient } from "./ssh-client.js"; diff --git a/src/infra/tcp-port.ts b/src/infra/tcp-port.ts index 99472328d535..3e63eb12801c 100644 --- a/src/infra/tcp-port.ts +++ b/src/infra/tcp-port.ts @@ -1,5 +1,5 @@ // Parses strict TCP port inputs for config and CLI surfaces. -import { parseStrictPositiveInteger } from "./parse-finite-number.js"; +import { parseStrictPositiveInteger } from "@openclaw/normalization-core/number-coercion"; // TCP port parsing is strict because config and CLI inputs both use this helper. export const MAX_TCP_PORT = 65_535; diff --git a/src/infra/windows-port-pids.ts b/src/infra/windows-port-pids.ts index 0723a783c11d..7a339df5c836 100644 --- a/src/infra/windows-port-pids.ts +++ b/src/infra/windows-port-pids.ts @@ -1,9 +1,9 @@ // Resolves Windows process identity and listening-port ownership. import { spawnSync } from "node:child_process"; +import { parseStrictPositiveInteger } from "@openclaw/normalization-core/number-coercion"; import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce"; import { normalizeStringEntries } from "@openclaw/normalization-core/string-normalization"; import { parseCmdScriptCommandLine } from "../daemon/cmd-argv.js"; -import { parseStrictPositiveInteger } from "./parse-finite-number.js"; import { parseWindowsNetstatListeners } from "./ports-netstat.js"; import { getWindowsPowerShellExePath, diff --git a/src/llm/providers/stream-wrappers/proxy.ts b/src/llm/providers/stream-wrappers/proxy.ts index 2e037f66e615..414d8747b7f2 100644 --- a/src/llm/providers/stream-wrappers/proxy.ts +++ b/src/llm/providers/stream-wrappers/proxy.ts @@ -1,3 +1,4 @@ +import { parseStrictFiniteNumber } from "@openclaw/normalization-core/number-coercion"; // Proxy stream wrapper applies provider-specific wrappers around base stream functions. import { normalizeOptionalLowercaseString, @@ -7,7 +8,6 @@ import { resolveProviderRequestPolicy } from "../../../agents/provider-attributi import { resolveProviderRequestPolicyConfig } from "../../../agents/provider-request-config.js"; import type { StreamFn } from "../../../agents/runtime/index.js"; import type { ThinkLevel } from "../../../auto-reply/thinking.js"; -import { parseStrictFiniteNumber } from "../../../infra/parse-finite-number.js"; import { normalizeOpenAICompatibleReasoningPayload } from "../../../plugin-sdk/provider-stream-shared.js"; import { parseBooleanValue } from "../../../utils/boolean.js"; import { streamSimple } from "../../stream.js"; diff --git a/src/logging/diagnostic-payload.ts b/src/logging/diagnostic-payload.ts index 476808447328..2467fce8a965 100644 --- a/src/logging/diagnostic-payload.ts +++ b/src/logging/diagnostic-payload.ts @@ -1,6 +1,6 @@ +import { parseStrictNonNegativeInteger } from "@openclaw/normalization-core/number-coercion"; // Diagnostic payload helpers emit structured diagnostic events with normalized fields. import { emitInternalDiagnosticEvent as emitDiagnosticEvent } from "../infra/diagnostic-events.js"; -import { parseStrictNonNegativeInteger } from "../infra/parse-finite-number.js"; // Diagnostic helpers for oversized payload decisions across channels/providers. type LargePayloadBase = { diff --git a/src/logging/diagnostic-stability-bundle.ts b/src/logging/diagnostic-stability-bundle.ts index c43804ff3375..4e69f624c44d 100644 --- a/src/logging/diagnostic-stability-bundle.ts +++ b/src/logging/diagnostic-stability-bundle.ts @@ -4,6 +4,7 @@ import path from "node:path"; import process from "node:process"; import v8 from "node:v8"; import { expectDefined } from "@openclaw/normalization-core"; +import { parseStrictNonNegativeInteger } from "@openclaw/normalization-core/number-coercion"; import { truncateUtf16Safe } from "@openclaw/normalization-core/utf16-slice"; import { resolveStateDir } from "../config/paths.js"; import type { @@ -12,7 +13,6 @@ import type { } from "../infra/diagnostic-events.js"; import { isMissingPathError } from "../infra/errors.js"; import { registerFatalErrorHook } from "../infra/fatal-error-hooks.js"; -import { parseStrictNonNegativeInteger } from "../infra/parse-finite-number.js"; import { replaceFileAtomicSync } from "../infra/replace-file.js"; import { getDiagnosticStabilitySnapshot, diff --git a/src/media-understanding/config-provider-models.ts b/src/media-understanding/config-provider-models.ts index 90d4b65ae5c8..7ac9483eb383 100644 --- a/src/media-understanding/config-provider-models.ts +++ b/src/media-understanding/config-provider-models.ts @@ -1,7 +1,7 @@ // Config provider model helpers discover image-capable custom providers for // media-understanding auto-registration. +import { normalizeMediaProviderId } from "../../packages/media-understanding-common/src/provider-id.js"; import type { OpenClawConfig } from "../config/types.js"; -import { normalizeMediaProviderId } from "./provider-id.js"; type ConfigProvider = NonNullable< NonNullable["providers"]>[string] diff --git a/src/media-understanding/entry-capabilities.ts b/src/media-understanding/entry-capabilities.ts index f08f15761a31..3d808469c33b 100644 --- a/src/media-understanding/entry-capabilities.ts +++ b/src/media-understanding/entry-capabilities.ts @@ -1,7 +1,7 @@ // Entry capability helpers validate explicit media capability tags and infer // shared provider entries from registry metadata. +import { normalizeMediaProviderId } from "../../packages/media-understanding-common/src/provider-id.js"; import type { MediaUnderstandingModelConfig } from "../config/types.tools.js"; -import { normalizeMediaProviderId } from "./provider-id.js"; import type { MediaUnderstandingCapability, MediaUnderstandingCapabilityRegistry, diff --git a/src/media-understanding/image.ts b/src/media-understanding/image.ts index b5860fea24ef..229098bd749e 100644 --- a/src/media-understanding/image.ts +++ b/src/media-understanding/image.ts @@ -1,8 +1,9 @@ -// Model-backed image understanding runtime for providers without a native media -// provider hook. import { clampPositiveTimerTimeoutMs } from "@openclaw/normalization-core/number-coercion"; import { isPromiseLike } from "@openclaw/normalization-core/promise-like"; import { isRecord } from "@openclaw/normalization-core/record-coerce"; +// Model-backed image understanding runtime for providers without a native media +// provider hook. +import { normalizeMediaProviderId } from "../../packages/media-understanding-common/src/provider-id.js"; import { isMinimaxVlmModel, minimaxUnderstandImage } from "../agents/minimax-vlm.js"; import { requireApiKey, resolveApiKeyForProviderCore } from "../agents/model-auth.js"; import { resolveProviderRequestCapabilities } from "../agents/provider-attribution.js"; @@ -23,7 +24,6 @@ import { isSecretRef } from "../config/types.secrets.js"; import { complete } from "../llm/stream.js"; import type { AssistantMessage, Context, Model, ProviderStreamOptions } from "../llm/types.js"; import { getResolvedImageRuntimeContext, resolveImageRuntime } from "./image-model-runtime.js"; -import { normalizeMediaProviderId } from "./provider-id.js"; import type { ImageDescriptionRequest, ImageDescriptionResult, diff --git a/src/media-understanding/manifest-metadata.ts b/src/media-understanding/manifest-metadata.ts index 8bc3707d509c..19e740200418 100644 --- a/src/media-understanding/manifest-metadata.ts +++ b/src/media-understanding/manifest-metadata.ts @@ -1,8 +1,8 @@ // Manifest metadata registry builder for media-understanding providers without // loading plugin runtime code. +import { normalizeMediaProviderId } from "../../packages/media-understanding-common/src/provider-id.js"; import type { OpenClawConfig } from "../config/types.js"; import { loadManifestMetadataSnapshot } from "../plugins/manifest-contract-eligibility.js"; -import { normalizeMediaProviderId } from "./provider-id.js"; import type { MediaUnderstandingProvider } from "./types.js"; /** Builds a media provider registry from trusted manifest metadata without loading plugin code. */ diff --git a/src/media-understanding/provider-capability-registry.ts b/src/media-understanding/provider-capability-registry.ts index 3fc7a04e8a37..f9b3257bf691 100644 --- a/src/media-understanding/provider-capability-registry.ts +++ b/src/media-understanding/provider-capability-registry.ts @@ -1,9 +1,9 @@ // Capability registry used to decide which shared media model entries are // eligible for image/audio/video understanding. +import { normalizeMediaProviderId } from "../../packages/media-understanding-common/src/provider-id.js"; import type { OpenClawConfig } from "../config/types.js"; import { resolvePluginCapabilityProviders } from "../plugins/capability-provider-runtime.js"; import { resolveImageCapableConfigProviderIds } from "./config-provider-models.js"; -import { normalizeMediaProviderId } from "./provider-id.js"; import type { MediaUnderstandingCapabilityRegistry, MediaUnderstandingProvider } from "./types.js"; function mergeProviderCapabilities( diff --git a/src/media-understanding/provider-id.ts b/src/media-understanding/provider-id.ts deleted file mode 100644 index 1b47154b9133..000000000000 --- a/src/media-understanding/provider-id.ts +++ /dev/null @@ -1,2 +0,0 @@ -// Core facade for shared media provider id normalization. -export * from "../../packages/media-understanding-common/src/provider-id.js"; diff --git a/src/media-understanding/provider-registry.ts b/src/media-understanding/provider-registry.ts index ee39f0c4ef2d..4cfe2f75e3c4 100644 --- a/src/media-understanding/provider-registry.ts +++ b/src/media-understanding/provider-registry.ts @@ -1,8 +1,8 @@ +import { normalizeMediaProviderId } from "../../packages/media-understanding-common/src/provider-id.js"; import type { OpenClawConfig } from "../config/types.js"; import { resolvePluginCapabilityProviders } from "../plugins/capability-provider-runtime.js"; import { resolveImageCapableConfigProviderIds } from "./config-provider-models.js"; import { describeImageWithModel, describeImagesWithModel } from "./image-runtime.js"; -import { normalizeMediaProviderId } from "./provider-id.js"; import type { MediaUnderstandingProvider } from "./types.js"; function mergeProviderIntoRegistry( @@ -44,7 +44,10 @@ function hydrateModelBackedMediaProvider( }; } -export { normalizeMediaExecutionProviderId, normalizeMediaProviderId } from "./provider-id.js"; +export { + normalizeMediaExecutionProviderId, + normalizeMediaProviderId, +} from "../../packages/media-understanding-common/src/provider-id.js"; /** Builds the media-understanding provider registry from plugin capabilities and config providers. */ export function buildMediaUnderstandingRegistry( diff --git a/src/media-understanding/runner.entries.ts b/src/media-understanding/runner.entries.ts index 964253a7af20..0e0c61d5a653 100644 --- a/src/media-understanding/runner.entries.ts +++ b/src/media-understanding/runner.entries.ts @@ -11,6 +11,7 @@ import { normalizeStringEntries } from "@openclaw/normalization-core/string-norm import { truncateUtf16Safe } from "@openclaw/normalization-core/utf16-slice"; import { MediaUnderstandingSkipError } from "../../packages/media-understanding-common/src/errors.js"; import { extractGeminiResponse } from "../../packages/media-understanding-common/src/output-extract.js"; +import { normalizeMediaExecutionProviderId } from "../../packages/media-understanding-common/src/provider-id.js"; import { estimateBase64Size, resolveVideoMaxBase64Bytes, @@ -62,7 +63,6 @@ import { resolveRequestedLocalAudioBackend, } from "./local-audio.js"; import { resolveOpenAiAudioAuthModelApi } from "./openai-audio-api.js"; -import { normalizeMediaExecutionProviderId } from "./provider-id.js"; import { getMediaUnderstandingProvider, normalizeMediaProviderId } from "./provider-registry.js"; import { resolveMaxBytes, resolveMaxChars, resolvePrompt, resolveTimeoutMs } from "./resolve.js"; import type { diff --git a/src/media-understanding/runner.ts b/src/media-understanding/runner.ts index 8cedcae91074..83e7e7b0e441 100644 --- a/src/media-understanding/runner.ts +++ b/src/media-understanding/runner.ts @@ -11,6 +11,10 @@ import { import { uniqueStrings } from "@openclaw/normalization-core/string-normalization"; import type { ActiveMediaModel } from "../../packages/media-understanding-common/src/active-model.js"; import { isMediaUnderstandingSkipError } from "../../packages/media-understanding-common/src/errors.js"; +import { + normalizeMediaExecutionProviderId, + normalizeMediaProviderId, +} from "../../packages/media-understanding-common/src/provider-id.js"; import { providerSupportsCapability } from "../../packages/media-understanding-common/src/provider-supports.js"; import { isMinimaxVlmModel, isMinimaxVlmProvider } from "../agents/minimax-vlm.js"; import { @@ -43,7 +47,6 @@ import { inspectLocalAudioSelection, } from "./local-audio.js"; import { resolveOpenAiAudioAuthModelApi } from "./openai-audio-api.js"; -import { normalizeMediaExecutionProviderId, normalizeMediaProviderId } from "./provider-id.js"; import { buildMediaUnderstandingRegistry, getMediaUnderstandingProvider, diff --git a/src/media/fetch.test.ts b/src/media/fetch.test.ts index 6a05c95d1e42..84a4517a4d53 100644 --- a/src/media/fetch.test.ts +++ b/src/media/fetch.test.ts @@ -1,7 +1,7 @@ // Media fetch tests cover remote media download limits and validation. import fs from "node:fs/promises"; +import { MAX_TIMER_TIMEOUT_MS } from "@openclaw/normalization-core/number-coercion"; import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; -import { MAX_TIMER_TIMEOUT_MS } from "../shared/number-coercion.js"; import { createTempHomeEnv, type TempHomeEnv } from "../test-utils/temp-home.js"; const fetchWithSsrFGuardMock = vi.hoisted(() => vi.fn()); diff --git a/src/media/input-files.ts b/src/media/input-files.ts index 2a08924f24ba..f1e3c3012d44 100644 --- a/src/media/input-files.ts +++ b/src/media/input-files.ts @@ -6,6 +6,7 @@ import { import { canonicalizeBase64, estimateBase64DecodedBytes } from "@openclaw/media-core/base64"; import { parseMediaContentLength } from "@openclaw/media-core/content-length"; import { detectMime, normalizeMimeType } from "@openclaw/media-core/mime"; +import { resolveTimerTimeoutMs } from "@openclaw/normalization-core/number-coercion"; import { normalizeOptionalLowercaseString, normalizeOptionalString, @@ -16,7 +17,6 @@ import { readResponseWithLimit } from "../infra/http-body.js"; import { fetchWithSsrFGuard } from "../infra/net/fetch-guard.js"; import type { SsrFPolicy } from "../infra/net/ssrf.js"; import { logWarn } from "../logger.js"; -import { resolveTimerTimeoutMs } from "../shared/number-coercion.js"; import { convertHeicToJpeg } from "./media-services.js"; import { extractPdfContent, type PdfExtractedImage } from "./pdf-extract.js"; diff --git a/src/meeting-bot/transcripts-bridge.runtime.ts b/src/meeting-bot/transcripts-bridge.runtime.ts index c0ff07536c31..aafa8502e3cf 100644 --- a/src/meeting-bot/transcripts-bridge.runtime.ts +++ b/src/meeting-bot/transcripts-bridge.runtime.ts @@ -1,4 +1,5 @@ import path from "node:path"; +import { coerceErrorMessage } from "@openclaw/normalization-core/error-coercion"; import { resolveStateDir } from "../config/paths.js"; import { KeyedAsyncQueue } from "../plugin-sdk/keyed-async-queue.js"; import { resolveTranscriptsConfig } from "../transcripts/config.js"; @@ -105,9 +106,7 @@ export function createMeetingDurableTranscriptBridge< const reportCaptureError = (sessionId: string, error: unknown) => { params.logger.debug?.( - `[meeting-transcripts] capture ignored session=${sessionId}: ${ - error instanceof Error ? error.message : String(error) - }`, + `[meeting-transcripts] capture ignored session=${sessionId}: ${coerceErrorMessage(error)}`, ); }; @@ -118,16 +117,12 @@ export function createMeetingDurableTranscriptBridge< try { void Promise.resolve(subscriber.onStatus(status)).catch((error: unknown) => { params.logger.warn( - `[meeting-transcripts] subscriber status failed session=${status.sessionId ?? "unknown"}: ${ - error instanceof Error ? error.message : String(error) - }`, + `[meeting-transcripts] subscriber status failed session=${status.sessionId ?? "unknown"}: ${coerceErrorMessage(error)}`, ); }); } catch (error) { params.logger.warn( - `[meeting-transcripts] subscriber status failed session=${status.sessionId ?? "unknown"}: ${ - error instanceof Error ? error.message : String(error) - }`, + `[meeting-transcripts] subscriber status failed session=${status.sessionId ?? "unknown"}: ${coerceErrorMessage(error)}`, ); } }; @@ -173,9 +168,7 @@ export function createMeetingDurableTranscriptBridge< } catch (error) { if (!active.initializationWarned) { params.logger.warn( - `[meeting-transcripts] durable capture initialization pending session=${session.id}: ${ - error instanceof Error ? error.message : String(error) - }`, + `[meeting-transcripts] durable capture initialization pending session=${session.id}: ${coerceErrorMessage(error)}`, ); active.initializationWarned = true; } @@ -242,9 +235,7 @@ export function createMeetingDurableTranscriptBridge< } catch (error) { subscribers.delete(subscriberSessionId); params.logger.warn( - `[meeting-transcripts] detached failing subscriber session=${subscriberSessionId}: ${ - error instanceof Error ? error.message : String(error) - }`, + `[meeting-transcripts] detached failing subscriber session=${subscriberSessionId}: ${coerceErrorMessage(error)}`, ); notifySubscriberStatus(subscriber, { sessionId: subscriberSessionId, @@ -295,7 +286,7 @@ export function createMeetingDurableTranscriptBridge< } catch (error) { if (!(error instanceof MeetingTranscriptDeliveryError)) { reportCaptureError(session.id, error); - active.finalCaptureError = error instanceof Error ? error.message : String(error); + active.finalCaptureError = coerceErrorMessage(error); active.finalCaptureFailedAt ??= new Date().toISOString(); deliveryError = undefined; break; @@ -350,9 +341,7 @@ export function createMeetingDurableTranscriptBridge< }); } catch (error) { params.logger.warn( - `[meeting-transcripts] could not finalize durable capture session=${session.id}: ${ - error instanceof Error ? error.message : String(error) - }`, + `[meeting-transcripts] could not finalize durable capture session=${session.id}: ${coerceErrorMessage(error)}`, ); throw error; } diff --git a/src/plugin-sdk/AGENTS.md b/src/plugin-sdk/AGENTS.md index a867b8733e95..9ad75bf0b907 100644 --- a/src/plugin-sdk/AGENTS.md +++ b/src/plugin-sdk/AGENTS.md @@ -14,7 +14,7 @@ can affect bundled plugins and third-party plugins. - Definition files: - `package.json` - `scripts/lib/plugin-sdk-entrypoints.json` - - `src/plugin-sdk/entrypoints.ts` + - `scripts/lib/plugin-sdk-entries.mts` - `src/plugin-sdk/api-baseline.ts` - `src/plugin-sdk/plugin-entry.ts` - `src/plugin-sdk/core.ts` @@ -89,7 +89,7 @@ can affect bundled plugins and third-party plugins. - When adding or changing a public subpath, keep these aligned: - docs in `docs/plugins/*` - `scripts/lib/plugin-sdk-entrypoints.json` - - `src/plugin-sdk/entrypoints.ts` + - `scripts/lib/plugin-sdk-entries.mts` - `package.json` exports - API baseline and export checks - If a bundled channel/helper need crosses package boundaries, first ask diff --git a/src/plugin-sdk/api-baseline.ts b/src/plugin-sdk/api-baseline.ts index dad81f290f3d..28f271f389f4 100644 --- a/src/plugin-sdk/api-baseline.ts +++ b/src/plugin-sdk/api-baseline.ts @@ -9,13 +9,13 @@ import { type PluginSdkDocCategory, type PluginSdkDocEntrypoint, } from "../../scripts/lib/plugin-sdk-doc-metadata.ts"; +import { publicPluginSdkEntrypoints } from "../../scripts/lib/plugin-sdk-entries.mts"; import { createDeclarationClosureRenderer, formatPluginSdkDiagnostics, } from "./api-baseline-declaration-closure.js"; import { printPluginSdkExportDeclaration } from "./api-baseline-declaration-print.js"; import { normalizePluginSdkApiSourcePath as relativePath } from "./api-baseline-normalization.js"; -import { publicPluginSdkEntrypoints } from "./entrypoints.ts"; export { normalizePluginSdkApiDeclarationText, diff --git a/src/plugin-sdk/core.ts b/src/plugin-sdk/core.ts index 126bcfad939b..035c407dfa8b 100644 --- a/src/plugin-sdk/core.ts +++ b/src/plugin-sdk/core.ts @@ -282,7 +282,7 @@ export { readStringArrayParam, readToolStringParam as readStringParam, } from "../agents/tools/common.js"; -export { parseStrictPositiveInteger } from "../infra/parse-finite-number.js"; +export { parseStrictPositiveInteger } from "@openclaw/normalization-core/number-coercion"; export { isTrustedProxyAddress, resolveClientIp } from "../gateway/net.js"; export { formatZonedTimestamp } from "../infra/format-time/format-datetime.js"; export { resolveConfiguredAcpBindingRecord } from "../acp/persistent-bindings.resolve.js"; diff --git a/src/plugin-sdk/entrypoints.ts b/src/plugin-sdk/entrypoints.ts deleted file mode 100644 index 4f52e0ca175b..000000000000 --- a/src/plugin-sdk/entrypoints.ts +++ /dev/null @@ -1,85 +0,0 @@ -// SDK entrypoint metadata lists supported public subpaths and deprecated barrel exports. -import deprecatedBarrelPluginSdkSubpathList from "../../scripts/lib/plugin-sdk-deprecated-barrel-subpaths.json" with { type: "json" }; -import deprecatedPublicPluginSdkSubpathList from "../../scripts/lib/plugin-sdk-deprecated-public-subpaths.json" with { type: "json" }; -import pluginSdkEntryList from "../../scripts/lib/plugin-sdk-entrypoints.json" with { type: "json" }; -import privateLocalOnlyPluginSdkSubpathList from "../../scripts/lib/plugin-sdk-private-local-only-subpaths.json" with { type: "json" }; - -/** All declared SDK subpath entrypoints, including public and local-only surfaces. */ -export const pluginSdkEntrypoints = [...pluginSdkEntryList]; - -/** All SDK subpaths; the removed package root is intentionally absent. */ -export const pluginSdkSubpaths = pluginSdkEntrypoints; - -const privateLocalOnlyPluginSdkSubpathSet = new Set( - privateLocalOnlyPluginSdkSubpathList.filter( - (entry): entry is string => typeof entry === "string" && !entry.includes("/"), - ), -); - -/** Entrypoints excluded from the typed, documented public SDK surface. */ -export const privateLocalOnlyPluginSdkEntrypoints = pluginSdkSubpaths.filter((entry) => - privateLocalOnlyPluginSdkSubpathSet.has(entry), -); - -/** Entrypoints exported by the published package for third-party plugin imports. */ -export const publicPluginSdkEntrypoints = pluginSdkEntrypoints.filter( - (entry) => !privateLocalOnlyPluginSdkSubpathSet.has(entry), -); - -/** Published SDK subpaths. */ -export const publicPluginSdkSubpaths = publicPluginSdkEntrypoints; - -/** Public SDK subpaths that remain importable but are marked deprecated in docs/contracts. */ -export const deprecatedPublicPluginSdkEntrypoints = publicPluginSdkSubpaths.filter((entry) => - deprecatedPublicPluginSdkSubpathList.includes(entry), -); - -/** Deprecated subpaths still re-exported by the root SDK barrel for compatibility. */ -export const deprecatedBarrelPluginSdkEntrypoints = pluginSdkSubpaths.filter((entry) => - deprecatedBarrelPluginSdkSubpathList.includes(entry), -); - -/** - * Transitional compatibility/helper surfaces owned by their matching bundled plugin. - * - * Cross-owner extension imports are blocked by package contract guardrails. - */ -export const reservedBundledPluginSdkEntrypoints = [] as const; - -/** - * Supported SDK facades backed by bundled plugins until generic contracts replace them. - */ -export const supportedBundledFacadeSdkEntrypoints = [ - "discord", - "matrix", - "telegram-account", -] as const; - -/** Plugin-owned surfaces intentionally public and documented for third-party plugins. */ -export const publicPluginOwnedSdkEntrypoints = ["memory-core-host-engine-foundation"] as const; - -/** Map every SDK entrypoint name to its source file path inside the repo. */ -export function buildPluginSdkEntrySources(entries: readonly string[] = pluginSdkEntrypoints) { - return Object.fromEntries(entries.map((entry) => [entry, `src/plugin-sdk/${entry}.ts`])); -} - -/** Build the package.json exports map for public plugin SDK subpaths. */ -export function buildPluginSdkPackageExports() { - return Object.fromEntries( - publicPluginSdkEntrypoints.map((entry) => [ - `./plugin-sdk/${entry}`, - { - types: `./dist/plugin-sdk/${entry}.d.ts`, - default: `./dist/plugin-sdk/${entry}.js`, - }, - ]), - ); -} - -/** List the dist artifacts expected for every generated plugin SDK entrypoint. */ -export function listPluginSdkDistArtifacts() { - return publicPluginSdkEntrypoints.flatMap((entry) => [ - `dist/plugin-sdk/${entry}.js`, - `dist/plugin-sdk/${entry}.d.ts`, - ]); -} diff --git a/src/plugin-sdk/infra-runtime.ts b/src/plugin-sdk/infra-runtime.ts index e9b28997c816..ff03f07ce438 100644 --- a/src/plugin-sdk/infra-runtime.ts +++ b/src/plugin-sdk/infra-runtime.ts @@ -250,7 +250,22 @@ export * from "../infra/net/undici-global-dispatcher.js"; export * from "../infra/net/ssrf.js"; export * from "../infra/outbound/identity.js"; export * from "../infra/outbound/sanitize-text.js"; -export * from "../infra/parse-finite-number.js"; +export { + clampTimerTimeoutMs, + finiteSecondsToTimerSafeMilliseconds, + MAX_TIMER_TIMEOUT_MS, + MAX_TIMER_TIMEOUT_SECONDS, + nonNegativeSecondsToSafeMilliseconds, + parseFiniteNumber, + parseStrictFiniteNumber, + parseStrictInteger, + parseStrictNonNegativeInteger, + parseStrictPositiveInteger, + positiveSecondsToSafeMilliseconds, + resolveExpiresAtMsFromDurationOrEpoch, + resolveExpiresAtMsFromDurationSeconds, + resolveExpiresAtMsFromEpochSeconds, +} from "@openclaw/normalization-core/number-coercion"; export * from "../infra/outbound/send-deps.js"; export * from "../infra/retry.js"; export * from "../infra/retry-policy.js"; diff --git a/src/plugin-sdk/plugin-test-contracts.ts b/src/plugin-sdk/plugin-test-contracts.ts index 0f32d5e79bc6..124922b3b0f4 100644 --- a/src/plugin-sdk/plugin-test-contracts.ts +++ b/src/plugin-sdk/plugin-test-contracts.ts @@ -8,7 +8,6 @@ export { registerTestPlugin, registerVirtualTestPlugin, requireProvider, - uniqueSortedStrings, } from "./test-helpers/contracts-testkit.js"; export { runDirectImportSmoke } from "./test-helpers/direct-smoke.js"; export { describePackageManifestContract } from "./test-helpers/package-manifest-contract.js"; diff --git a/src/plugin-sdk/provider-auth-runtime.test.ts b/src/plugin-sdk/provider-auth-runtime.test.ts index a4b7e56eddd6..e56aaf7b58b9 100644 --- a/src/plugin-sdk/provider-auth-runtime.test.ts +++ b/src/plugin-sdk/provider-auth-runtime.test.ts @@ -2,9 +2,9 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; +import { MAX_TIMER_TIMEOUT_MS } from "@openclaw/normalization-core/number-coercion"; import { describe, expect, it, vi } from "vitest"; import { saveAuthProfileStore } from "../agents/auth-profiles/store.js"; -import { MAX_TIMER_TIMEOUT_MS } from "../shared/number-coercion.js"; import { getFreePort } from "../test-utils/ports.js"; import * as providerAuthRuntime from "./provider-auth-runtime.js"; diff --git a/src/plugin-sdk/provider-auth-runtime.ts b/src/plugin-sdk/provider-auth-runtime.ts index 3e9560b09b88..a6dac75d1106 100644 --- a/src/plugin-sdk/provider-auth-runtime.ts +++ b/src/plugin-sdk/provider-auth-runtime.ts @@ -4,13 +4,13 @@ import fs from "node:fs"; import { createServer } from "node:http"; import path from "node:path"; import { fileURLToPath, pathToFileURL } from "node:url"; +import { resolveTimerTimeoutMs } from "@openclaw/normalization-core/number-coercion"; import { ensureAuthProfileStore } from "../agents/auth-profiles/store.js"; import { resolveApiKeyForProviderCore as resolveModelApiKeyForProvider } from "../agents/model-auth.js"; import { normalizeProviderId } from "../agents/model-selection.js"; import type { OpenClawConfig } from "../config/config.js"; import { startOAuthLoopbackCallbackServer } from "../infra/oauth-loopback-callback.js"; import { escapeHtml } from "../shared/html-escape.js"; -import { resolveTimerTimeoutMs } from "../shared/number-coercion.js"; export { resolveEnvApiKey } from "../agents/model-auth-env.js"; export { diff --git a/src/plugin-sdk/provider-catalog-live-normalize.internal.ts b/src/plugin-sdk/provider-catalog-live-normalize.internal.ts index ac896466bfaf..79c7f5eac95f 100644 --- a/src/plugin-sdk/provider-catalog-live-normalize.internal.ts +++ b/src/plugin-sdk/provider-catalog-live-normalize.internal.ts @@ -5,11 +5,12 @@ export function readLiveModelCatalogRecord(body: unknown): Record | undefined, - keys: readonly string[], +export function readLiveModelCatalogStringField( + row: unknown, + keys: string | readonly string[], ): string | undefined { - for (const key of keys) { + const record = readLiveModelCatalogRecord(row); + for (const key of typeof keys === "string" ? [keys] : keys) { const value = record?.[key]; if (typeof value === "string" && value.trim()) { return value.trim(); @@ -18,11 +19,12 @@ function readLiveModelString( return undefined; } -function readLiveModelBoolean( - record: Record | undefined, - keys: readonly string[], +export function readLiveModelCatalogBooleanField( + row: unknown, + keys: string | readonly string[], ): boolean | undefined { - for (const key of keys) { + const record = readLiveModelCatalogRecord(row); + for (const key of typeof keys === "string" ? [keys] : keys) { const value = record?.[key]; if (typeof value === "boolean") { return value; @@ -31,16 +33,28 @@ function readLiveModelBoolean( return undefined; } -function readLiveModelPositiveInteger( +export function readLiveModelCatalogPositiveSafeIntegerField( + row: unknown, + keys: string | readonly string[], +): number | undefined { + const record = readLiveModelCatalogRecord(row); + for (const key of typeof keys === "string" ? [keys] : keys) { + const value = record?.[key]; + if (typeof value === "number" && Number.isSafeInteger(value) && value > 0) { + return value; + } + } + return undefined; +} + +function readLiveModelPositiveIntegerFromRecords( records: readonly (Record | undefined)[], keys: readonly string[], ): number | undefined { for (const record of records) { - for (const key of keys) { - const value = record?.[key]; - if (typeof value === "number" && Number.isSafeInteger(value) && value > 0) { - return value; - } + const value = readLiveModelCatalogPositiveSafeIntegerField(record, keys); + if (value !== undefined) { + return value; } } return undefined; @@ -94,7 +108,7 @@ function rowAdvertisesNonTextModel( if (outputModalities.length > 0 && !outputModalities.includes("text")) { return true; } - const kind = readLiveModelString(record, [ + const kind = readLiveModelCatalogStringField(record, [ "type", "task", "model_type", @@ -108,7 +122,7 @@ function rowAdvertisesChatModel( record: Record, nestedRecords: readonly (Record | undefined)[], ): boolean | undefined { - const explicitChatCapability = readLiveModelBoolean(nestedRecords[0], [ + const explicitChatCapability = readLiveModelCatalogBooleanField(nestedRecords[0], [ "completion_chat", "chat_completion", "chatCompletion", @@ -174,14 +188,14 @@ function buildOpenAICompatibleLiveModel( acceptUnknownModel?: (params: { id: string; record: Record }) => boolean, ): ModelDefinitionConfig | undefined { const record = readLiveModelCatalogRecord(row); - const id = readLiveModelString(record, ["id", "model", "model_name", "modelName"]); + const id = readLiveModelCatalogStringField(record, ["id", "model", "model_name", "modelName"]); if (!record || !id || !isSafeLiveModelId(id)) { return undefined; } - if (readLiveModelBoolean(record, ["active", "enabled", "available"]) === false) { + if (readLiveModelCatalogBooleanField(record, ["active", "enabled", "available"]) === false) { return undefined; } - if (readLiveModelBoolean(record, ["archived", "deprecated"]) === true) { + if (readLiveModelCatalogBooleanField(record, ["archived", "deprecated"]) === true) { return undefined; } const capabilities = readLiveModelCatalogRecord(record.capabilities); @@ -215,7 +229,7 @@ function buildOpenAICompatibleLiveModel( ["input_modalities", "inputModalities", "input"], ); const contextWindow = - readLiveModelPositiveInteger( + readLiveModelPositiveIntegerFromRecords( [record, topProvider, capabilities, modelInfo], [ "context_window", @@ -237,7 +251,7 @@ function buildOpenAICompatibleLiveModel( template?.contextWindow ?? 128_000; const maxTokens = - readLiveModelPositiveInteger( + readLiveModelPositiveIntegerFromRecords( [record, topProvider, capabilities, modelInfo], [ "max_completion_tokens", @@ -256,7 +270,7 @@ function buildOpenAICompatibleLiveModel( fallback.maxTokens ?? template?.maxTokens ?? Math.min(contextWindow, 8192); - const explicitReasoning = readLiveModelBoolean(record, [ + const explicitReasoning = readLiveModelCatalogBooleanField(record, [ "reasoning", "supports_reasoning", "supportsReasoning", @@ -277,7 +291,7 @@ function buildOpenAICompatibleLiveModel( return { id, - name: readLiveModelString(record, ["display_name", "displayName", "name"]) ?? id, + name: readLiveModelCatalogStringField(record, ["display_name", "displayName", "name"]) ?? id, ...(template?.api ? { api: template.api } : {}), reasoning, input, diff --git a/src/plugin-sdk/provider-catalog-live-normalize.primitives.test.ts b/src/plugin-sdk/provider-catalog-live-normalize.primitives.test.ts new file mode 100644 index 000000000000..e04270593664 --- /dev/null +++ b/src/plugin-sdk/provider-catalog-live-normalize.primitives.test.ts @@ -0,0 +1,34 @@ +import { describe, expect, it } from "vitest"; +import { + readLiveModelCatalogBooleanField, + readLiveModelCatalogPositiveSafeIntegerField, + readLiveModelCatalogStringField, +} from "./provider-catalog-live-normalize.internal.js"; + +describe("live model catalog primitive fields", () => { + it.each<[unknown, string | readonly string[], string | undefined]>([ + [{ primary: " ", fallback: " model-id " }, ["primary", "fallback"], "model-id"], + [{ value: 42 }, "value", undefined], + [[{ value: "model-id" }], "value", undefined], + ])("reads the first nonblank trimmed string from %j", (row, keys, expected) => { + expect(readLiveModelCatalogStringField(row, keys)).toBe(expected); + }); + + it.each<[unknown, string | readonly string[], number | undefined]>([ + [{ primary: 0, fallback: 8 }, ["primary", "fallback"], 8], + [{ value: -1 }, "value", undefined], + [{ value: 1.5 }, "value", undefined], + [{ value: Number.MAX_SAFE_INTEGER + 1 }, "value", undefined], + [{ value: "8" }, "value", undefined], + ])("accepts only positive safe integer fields from %j", (row, keys, expected) => { + expect(readLiveModelCatalogPositiveSafeIntegerField(row, keys)).toBe(expected); + }); + + it.each<[unknown, string | readonly string[], boolean | undefined]>([ + [{ primary: "false", fallback: false }, ["primary", "fallback"], false], + [{ value: true }, "value", true], + [{ value: 1 }, "value", undefined], + ])("accepts only strict Boolean fields from %j", (row, keys, expected) => { + expect(readLiveModelCatalogBooleanField(row, keys)).toBe(expected); + }); +}); diff --git a/src/plugin-sdk/provider-catalog-live-runtime.ts b/src/plugin-sdk/provider-catalog-live-runtime.ts index cf5f35493b6f..90ac628c0259 100644 --- a/src/plugin-sdk/provider-catalog-live-runtime.ts +++ b/src/plugin-sdk/provider-catalog-live-runtime.ts @@ -9,7 +9,10 @@ import type { } from "../plugins/types.js"; import { buildOpenAICompatibleLiveModels, + readLiveModelCatalogBooleanField, + readLiveModelCatalogPositiveSafeIntegerField, readLiveModelCatalogRecord, + readLiveModelCatalogStringField, } from "./provider-catalog-live-normalize.internal.js"; import { buildSingleProviderApiKeyCatalog, @@ -33,6 +36,11 @@ export type LiveModelCatalogHeaderContext = { }; export { clearLiveCatalogCacheForTests }; +export { + readLiveModelCatalogBooleanField, + readLiveModelCatalogPositiveSafeIntegerField, + readLiveModelCatalogStringField, +}; export type FetchLiveProviderModelIdsParams = { providerId: string; diff --git a/src/plugin-sdk/test-helpers/contracts-testkit.ts b/src/plugin-sdk/test-helpers/contracts-testkit.ts index d476e74e7282..242c9216d958 100644 --- a/src/plugin-sdk/test-helpers/contracts-testkit.ts +++ b/src/plugin-sdk/test-helpers/contracts-testkit.ts @@ -12,9 +12,8 @@ import { } from "../../test-utils/plugin-registration.js"; import type { OpenClawPluginApi } from "../plugin-entry.js"; export { assertNoImportTimeSideEffects } from "./import-side-effects.js"; -import { uniqueSortedStrings } from "./string-utils.js"; -export { registerProviders, requireProvider, uniqueSortedStrings }; +export { registerProviders, requireProvider }; /** Creates a minimal plugin registry fixture with quiet logger defaults. */ export function createPluginRegistryFixture( diff --git a/src/plugin-sdk/test-helpers/string-utils.ts b/src/plugin-sdk/test-helpers/string-utils.ts deleted file mode 100644 index 6c8bd6b3b313..000000000000 --- a/src/plugin-sdk/test-helpers/string-utils.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Shared string helpers for plugin SDK contract tests. - */ -import { sortUniqueStrings } from "../../../packages/normalization-core/src/string-normalization.js"; - -/** Sorts and deduplicates string values for stable contract assertions. */ -export function uniqueSortedStrings(values: readonly string[]) { - return sortUniqueStrings(values); -} diff --git a/src/plugin-sdk/text-utility-runtime.ts b/src/plugin-sdk/text-utility-runtime.ts index e6f19fd8d313..cde04a6a4fba 100644 --- a/src/plugin-sdk/text-utility-runtime.ts +++ b/src/plugin-sdk/text-utility-runtime.ts @@ -2,6 +2,8 @@ import type { BaseProbeResult } from "../channels/plugins/types.public.js"; import { withTimeout } from "../utils/with-timeout.js"; +export { estimateStringChars } from "@openclaw/normalization-core/cjk-chars"; + export { estimateToolResultTextChars, sliceToolResultTextToBudget, @@ -11,7 +13,6 @@ export { resolveLiveToolResultMaxChars, } from "../agents/tool-result-limits.js"; export { escapeHtml } from "../shared/html-escape.js"; -export { estimateStringChars } from "../utils/cjk-chars.js"; type ChannelProbeResult = BaseProbeResult & { elapsedMs?: number }; diff --git a/src/plugins/contracts/plugin-sdk-index.bundle.test.ts b/src/plugins/contracts/plugin-sdk-index.bundle.test.ts index 097a1c2ae466..2d7872b0f3a7 100644 --- a/src/plugins/contracts/plugin-sdk-index.bundle.test.ts +++ b/src/plugins/contracts/plugin-sdk-index.bundle.test.ts @@ -4,7 +4,10 @@ import { createRequire } from "node:module"; import path from "node:path"; import { pathToFileURL } from "node:url"; import { afterAll, describe, expect, it } from "vitest"; -import { buildPluginSdkEntrySources, pluginSdkEntrypoints } from "../../plugin-sdk/entrypoints.js"; +import { + buildPluginSdkEntrySources, + pluginSdkEntrypoints, +} from "../../../scripts/lib/plugin-sdk-entries.mts"; import { createSyncSuiteTempRootTracker } from "../test-helpers/fs-fixtures.js"; import { resolveBundledPluginFile } from "./test-helpers/bundled-plugin-roots.js"; diff --git a/src/plugins/contracts/plugin-sdk-package-contract-guardrails.test.ts b/src/plugins/contracts/plugin-sdk-package-contract-guardrails.test.ts index e58138f4d655..ffedf804dfc7 100644 --- a/src/plugins/contracts/plugin-sdk-package-contract-guardrails.test.ts +++ b/src/plugins/contracts/plugin-sdk-package-contract-guardrails.test.ts @@ -6,13 +6,13 @@ import { beforeAll, describe, expect, it } from "vitest"; import { deprecatedBarrelPluginSdkEntrypoints, deprecatedPublicPluginSdkEntrypoints, + packagedPrivatePluginSdkRuntimeEntrypoints, privateLocalOnlyPluginSdkEntrypoints, pluginSdkEntrypoints, publicPluginOwnedSdkEntrypoints, publicPluginSdkEntrypoints, - reservedBundledPluginSdkEntrypoints, supportedBundledFacadeSdkEntrypoints, -} from "../../plugin-sdk/entrypoints.js"; +} from "../../../scripts/lib/plugin-sdk-entries.mts"; import { expectNoReaddirSyncDuring } from "../../test-utils/fs-scan-assertions.js"; import { listGitTrackedFiles, @@ -227,12 +227,6 @@ function collectPluginOwnedSdkEntrypoints(): string[] { .toSorted(); } -function resolvePluginOwnerFromEntrypoint(entrypoint: string): string | undefined { - return collectBundledPluginIds().find( - (pluginId) => entrypoint === pluginId || entrypoint.startsWith(`${pluginId}-`), - ); -} - function collectClassificationOverlaps(classifications: Record) { const seen = new Map(); for (const [classification, entrypoints] of Object.entries(classifications)) { @@ -549,65 +543,6 @@ function collectWorkspaceCodeFiles(): string[] { return files; } -function collectCrossOwnerReservedSdkImports(): Array<{ - file: string; - specifier: string; - owner?: string; -}> { - const leaks: Array<{ file: string; specifier: string; owner?: string }> = []; - const reserved = new Set(reservedBundledPluginSdkEntrypoints); - const importPattern = - /\b(?:import|export)\b[\s\S]*?\bfrom\s*["']openclaw\/plugin-sdk\/([a-z0-9][a-z0-9-]*)["']/g; - - for (const file of collectExtensionFiles(resolve(REPO_ROOT, "extensions"))) { - const repoRelativePath = toRepoRelativePath(file); - const pluginId = repoRelativePath.split("/")[1]; - const source = fs.readFileSync(file, "utf8"); - for (const match of source.matchAll(importPattern)) { - const subpath = match[1]; - if (!subpath || !reserved.has(subpath)) { - continue; - } - const owner = resolvePluginOwnerFromEntrypoint(subpath); - if (owner === pluginId) { - continue; - } - leaks.push({ - file: repoRelativePath, - specifier: `openclaw/plugin-sdk/${subpath}`, - owner, - }); - } - } - return leaks; -} - -function collectReservedSdkSubpathImports(): string[] { - const imports = new Set(); - const reserved = new Set(reservedBundledPluginSdkEntrypoints); - const importPatterns = [ - /\b(?:import|export)\b[\s\S]*?\bfrom\s*["']openclaw\/plugin-sdk\/([a-z0-9][a-z0-9-]*)["']/g, - /\bimport\s*\(\s*["']openclaw\/plugin-sdk\/([a-z0-9][a-z0-9-]*)["']\s*\)/g, - /\bvi\.(?:mock|doMock)\s*\(\s*["']openclaw\/plugin-sdk\/([a-z0-9][a-z0-9-]*)["']/g, - ]; - - for (const root of ["src", "test", "extensions", "packages", "scripts"]) { - for (const file of collectCodeFiles(resolve(REPO_ROOT, root))) { - const source = fs.readFileSync(file, "utf8"); - for (const importPattern of importPatterns) { - for (const match of source.matchAll(importPattern)) { - const subpath = match[1]; - if (subpath && reserved.has(subpath)) { - imports.add(subpath); - } - } - } - } - } - - return [...imports].toSorted(); -} - function hasWildcardReexport(entrypoint: string): boolean { const source = fs.readFileSync(resolve(REPO_ROOT, "src/plugin-sdk", `${entrypoint}.ts`), "utf8"); return /^\s*export\s+(?:type\s+)?\*\s+from\s+["'][^"']+["']/mu.test(source); @@ -642,14 +577,9 @@ function collectExtensionProductionSdkSubpathImports(subpaths: ReadonlySet { let deprecatedTestAliasImports: string[] = []; - let unusedReservedSdkSubpaths: string[] = []; beforeAll(() => { deprecatedTestAliasImports = collectDeprecatedTestAliasImports(); - const usedReserved = new Set(collectReservedSdkSubpathImports()); - unusedReservedSdkSubpaths = reservedBundledPluginSdkEntrypoints.filter( - (entrypoint) => !usedReserved.has(entrypoint), - ); }); it("lists package guardrail scan inputs from git without walking roots", () => { @@ -678,15 +608,13 @@ describe("plugin-sdk package contract guardrails", () => { }); it("keeps package.json exports aligned with built plugin-sdk entrypoints", () => { - const localOnly = new Set(privateLocalOnlyPluginSdkEntrypoints); const packageExports = collectPluginSdkPackageExports(); + const typedPackageExports = collectTypedPluginSdkPackageExports(); - expect(packageExports.filter((entrypoint) => !localOnly.has(entrypoint))).toEqual( - [...publicPluginSdkEntrypoints].toSorted(), + expect(packageExports).toEqual( + [...publicPluginSdkEntrypoints, ...packagedPrivatePluginSdkRuntimeEntrypoints].toSorted(), ); - expect( - publicPluginSdkEntrypoints.filter((entrypoint) => !packageExports.includes(entrypoint)), - ).toEqual([]); + expect([...typedPackageExports].toSorted()).toEqual([...publicPluginSdkEntrypoints].toSorted()); }); it("keeps Vitest-backed SDK test helpers local-only", () => { @@ -715,50 +643,43 @@ describe("plugin-sdk package contract guardrails", () => { it("keeps bundled plugin SDK compatibility subpaths explicitly classified", () => { const entrypoints = new Set(pluginSdkEntrypoints); - const reserved = new Set(reservedBundledPluginSdkEntrypoints); const supported = new Set(supportedBundledFacadeSdkEntrypoints); const localOnly = new Set(privateLocalOnlyPluginSdkEntrypoints); - const unknownReserved = [...reserved].filter((entrypoint) => !entrypoints.has(entrypoint)); const unknownSupported = [...supported].filter((entrypoint) => !entrypoints.has(entrypoint)); const unknownLocalOnly = [...localOnly].filter((entrypoint) => !entrypoints.has(entrypoint)); const unclassifiedBundledFacades = collectBundledFacadeSdkEntrypoints().filter( - (entrypoint) => - !reserved.has(entrypoint) && !supported.has(entrypoint) && !localOnly.has(entrypoint), + (entrypoint) => !supported.has(entrypoint) && !localOnly.has(entrypoint), ); - const unreservedPrivateSurfaces = collectPrivateBundledSdkSurfaceEntrypoints().filter( - (entrypoint) => !reserved.has(entrypoint) && !localOnly.has(entrypoint), + const unclassifiedPrivateSurfaces = collectPrivateBundledSdkSurfaceEntrypoints().filter( + (entrypoint) => !localOnly.has(entrypoint), ); expect({ - unknownReserved, unknownSupported, unknownLocalOnly, unclassifiedBundledFacades, - unreservedPrivateSurfaces, + unclassifiedPrivateSurfaces, }).toEqual({ - unknownReserved: [], unknownSupported: [], unknownLocalOnly: [], unclassifiedBundledFacades: [], - unreservedPrivateSurfaces: [], + unclassifiedPrivateSurfaces: [], }); }); it("keeps plugin-owned SDK subpaths explicitly classified and documented", () => { const entrypoints = new Set(pluginSdkEntrypoints); - const reserved = new Set(reservedBundledPluginSdkEntrypoints); const supported = new Set(supportedBundledFacadeSdkEntrypoints); const publicOwned = new Set(publicPluginOwnedSdkEntrypoints); const localOnly = new Set(privateLocalOnlyPluginSdkEntrypoints); const documented = collectDocumentedSdkSubpaths(); const pluginOwnedEntrypoints = collectPluginOwnedSdkEntrypoints(); - const classified = new Set([...reserved, ...supported, ...publicOwned, ...localOnly]); + const classified = new Set([...supported, ...publicOwned, ...localOnly]); const unknownPublicOwned = [...publicOwned].filter( (entrypoint) => !entrypoints.has(entrypoint), ); const classificationOverlaps = collectClassificationOverlaps({ - reserved: reservedBundledPluginSdkEntrypoints, supported: supportedBundledFacadeSdkEntrypoints, publicOwned: publicPluginOwnedSdkEntrypoints, localOnly: privateLocalOnlyPluginSdkEntrypoints, @@ -898,14 +819,6 @@ describe("plugin-sdk package contract guardrails", () => { expect(deprecatedTestAliasImports).toStrictEqual([]); }); - it("keeps reserved SDK compatibility subpaths inside their owning bundled plugins", () => { - expect(collectCrossOwnerReservedSdkImports()).toStrictEqual([]); - }); - - it("keeps reserved SDK compatibility subpaths actively used", () => { - expect(unusedReservedSdkSubpaths).toStrictEqual([]); - }); - it("keeps generic core poll helpers free of plugin owner names", () => { expect(collectGenericCoreOwnerNameLeaks()).toStrictEqual([]); }); diff --git a/src/plugins/contracts/plugin-sdk-subpaths.test.ts b/src/plugins/contracts/plugin-sdk-subpaths.test.ts index 921637cd6c06..5082fed1c025 100644 --- a/src/plugins/contracts/plugin-sdk-subpaths.test.ts +++ b/src/plugins/contracts/plugin-sdk-subpaths.test.ts @@ -28,6 +28,14 @@ import type { } from "openclaw/plugin-sdk/reply-runtime"; import ts from "typescript"; import { beforeAll, describe, expect, expectTypeOf, it } from "vitest"; +import { + buildPluginSdkPackageExports, + deprecatedPublicPluginSdkEntrypoints, + packagedPrivatePluginSdkRuntimeEntrypoints, + pluginSdkEntrypoints, + privateLocalOnlyPluginSdkEntrypoints, + publicPluginSdkSubpaths as pluginSdkSubpaths, +} from "../../../scripts/lib/plugin-sdk-entries.mts"; import type { ChannelMessageActionContext } from "../../channels/plugins/types.public.js"; import type { BaseProbeResult, @@ -51,13 +59,6 @@ import type { } from "../../plugin-sdk/channel-plugin-common.js"; import * as channelReplyPipelineDirectSdk from "../../plugin-sdk/channel-reply-pipeline.js"; import * as coreDirectSdk from "../../plugin-sdk/core.js"; -import { - buildPluginSdkPackageExports, - deprecatedPublicPluginSdkEntrypoints, - pluginSdkEntrypoints, - privateLocalOnlyPluginSdkEntrypoints, - publicPluginSdkSubpaths as pluginSdkSubpaths, -} from "../../plugin-sdk/entrypoints.js"; import { expectNoReaddirSyncDuring } from "../../test-utils/fs-scan-assertions.js"; import { listGitTrackedFiles, toRepoRelativePath } from "../../test-utils/repo-files.js"; import type { PluginRuntime } from "../runtime/types.js"; @@ -519,10 +520,19 @@ describe("plugin-sdk subpath exports", () => { expect(docs).toContain("private-local entries explicitly"); for (const subpath of pluginSdkSubpaths) { - expect(packageExports).toHaveProperty(`./plugin-sdk/${subpath}`); + expect(packageExports).toHaveProperty(`./plugin-sdk/${subpath}`, { + types: `./dist/plugin-sdk/${subpath}.d.ts`, + default: `./dist/plugin-sdk/${subpath}.js`, + }); } + const packagedPrivateSubpaths = new Set(packagedPrivatePluginSdkRuntimeEntrypoints); for (const subpath of privateLocalOnlyPluginSdkEntrypoints) { - expect(packageExports).not.toHaveProperty(`./plugin-sdk/${subpath}`); + const packageExport = packageExports[`./plugin-sdk/${subpath}`]; + if (packagedPrivateSubpaths.has(subpath)) { + expect(packageExport).toEqual({ default: `./dist/plugin-sdk/${subpath}.js` }); + } else { + expect(packageExport).toBeUndefined(); + } } for (const subpath of deprecatedPublicPluginSdkEntrypoints) { expect(pluginSdkSubpaths).toContain(subpath); diff --git a/src/plugins/contracts/registry.contract.test.ts b/src/plugins/contracts/registry.contract.test.ts index 2e9cec213d4c..d4d118d3c892 100644 --- a/src/plugins/contracts/registry.contract.test.ts +++ b/src/plugins/contracts/registry.contract.test.ts @@ -1,6 +1,6 @@ // Registry contract tests cover plugin contract registry contents and lookup behavior. +import { sortUniqueStrings } from "@openclaw/normalization-core/string-normalization"; import { describe, expect, it } from "vitest"; -import { uniqueSortedStrings } from "../../plugin-sdk/test-helpers/string-utils.js"; import { loadPluginManifestRegistryCore, type PluginManifestRecord } from "../manifest-registry.js"; import { resolveManifestContractPluginIds } from "../plugin-registry.js"; import { BUNDLED_PLUGIN_CONTRACT_SNAPSHOTS } from "./inventory/bundled-capability-metadata.js"; @@ -20,7 +20,7 @@ describe("plugin contract registry", () => { actualPluginIds: readonly string[]; predicate: (plugin: PluginManifestRecord) => boolean; }) { - expect(uniqueSortedStrings(params.actualPluginIds)).toEqual( + expect(sortUniqueStrings(params.actualPluginIds)).toEqual( resolveBundledManifestPluginIds(params.predicate), ); } @@ -257,7 +257,7 @@ describe("plugin contract registry", () => { }); expect( - uniqueSortedStrings( + sortUniqueStrings( pluginRegistrationContractRegistry .filter((entry) => entry.webFetchProviderIds.length > 0) .map((entry) => entry.pluginId), @@ -277,11 +277,11 @@ describe("plugin contract registry", () => { snapshotPluginIds.has(pluginId) && !ACTIVATION_SCOPED_WEB_SEARCH_PLUGIN_ID_SET.has(pluginId), ); - const expectedPluginIds = uniqueSortedStrings([ + const expectedPluginIds = sortUniqueStrings([ ...bundledWebSearchPluginIds, ...ACTIVATION_SCOPED_WEB_SEARCH_PLUGIN_IDS, ]); - const actualPluginIds = uniqueSortedStrings( + const actualPluginIds = sortUniqueStrings( pluginRegistrationContractRegistry .filter((entry) => entry.webSearchProviderIds.length > 0) .map((entry) => entry.pluginId), diff --git a/src/plugins/install-security-scan.runtime.ts b/src/plugins/install-security-scan.runtime.ts index a99c59b346e6..270b6809514e 100644 --- a/src/plugins/install-security-scan.runtime.ts +++ b/src/plugins/install-security-scan.runtime.ts @@ -1,11 +1,11 @@ // Runtime bridge for plugin install security scanning. import fs from "node:fs/promises"; import path from "node:path"; +import { parseStrictPositiveInteger } from "@openclaw/normalization-core/number-coercion"; import type { OpenClawConfig } from "../config/types.openclaw.js"; import { formatErrorMessage } from "../infra/errors.js"; import { tryReadJson } from "../infra/json-files.js"; import { resolveOpenClawPackageRootSync } from "../infra/openclaw-root.js"; -import { parseStrictPositiveInteger } from "../infra/parse-finite-number.js"; import { runInstallPolicy, type InstallPolicyFinding, diff --git a/src/plugins/management-service.ts b/src/plugins/management-service.ts index 4a36c6030622..6d401b98a510 100644 --- a/src/plugins/management-service.ts +++ b/src/plugins/management-service.ts @@ -1715,8 +1715,4 @@ export async function uninstallManagedPlugin(params: { }); } -/** Normalize unexpected lifecycle failures for Gateway response adapters. */ -export function formatManagedPluginLifecycleError(error: unknown): string { - return formatErrorMessage(error); -} /* oxlint-disable max-lines -- TODO: split this grandfathered oversized file. */ diff --git a/src/plugins/memory-state.ts b/src/plugins/memory-state.ts index 53f069420e67..a3442d32f4b8 100644 --- a/src/plugins/memory-state.ts +++ b/src/plugins/memory-state.ts @@ -1,5 +1,6 @@ /** Registry state for plugin memory runtimes, prompt supplements, and flush planning. */ import { AsyncLocalStorage } from "node:async_hooks"; +import { filterStringEntries } from "@openclaw/normalization-core/string-normalization"; import type { OpenClawConfig } from "../config/types.openclaw.js"; import { createSubsystemLogger } from "../logging/subsystem.js"; import type { @@ -126,7 +127,7 @@ function buildSynchronousMemoryPromptSection(params: MemoryPromptSectionParams): supplements: Array<{ pluginId: string; lines: string[] }>; } { const registry = requireActivePluginRegistry(); - const primary = normalizeMemoryPromptLines( + const primary = filterStringEntries( resolveMemoryCapabilityRegistration(registry.memoryCapabilities)?.capability.promptBuilder?.( params, ) ?? [], @@ -136,7 +137,7 @@ function buildSynchronousMemoryPromptSection(params: MemoryPromptSectionParams): .toSorted((left, right) => left.pluginId.localeCompare(right.pluginId)) .map((registration) => ({ pluginId: registration.pluginId, - lines: normalizeMemoryPromptLines(registration.builder(params)), + lines: filterStringEntries(registration.builder(params)), })); return { primary, supplements }; } @@ -193,7 +194,7 @@ export async function prepareMemoryPromptSection( const preparedSupplements = await Promise.all( preparationRegistrations.map(async (registration) => ({ pluginId: registration.pluginId, - lines: normalizeMemoryPromptLines( + lines: filterStringEntries( await registration.prepare(cloneMemoryPromptSectionParams(runParams)), ), })), @@ -243,13 +244,6 @@ export function buildMemoryPromptSection( return [...synchronous.primary, ...synchronous.supplements.flatMap((entry) => entry.lines)]; } -function normalizeMemoryPromptLines(value: unknown): string[] { - if (!Array.isArray(value)) { - return []; - } - return value.filter((line): line is string => typeof line === "string"); -} - export function listMemoryPromptSupplements(): MemoryPromptSupplementRegistration[] { return [...requireActivePluginRegistry().memoryPromptSupplements]; } diff --git a/src/process/command-queue.ts b/src/process/command-queue.ts index fbbe3183a090..be0474dd510f 100644 --- a/src/process/command-queue.ts +++ b/src/process/command-queue.ts @@ -1,11 +1,11 @@ // Command queue serializes and limits process execution for shared command lanes. import { AsyncLocalStorage } from "node:async_hooks"; +import { clampPositiveTimerTimeoutMs } from "@openclaw/normalization-core/number-coercion"; import { diagnosticLogger as diag, logLaneDequeue, logLaneEnqueue, } from "../logging/diagnostic-runtime.js"; -import { clampPositiveTimerTimeoutMs } from "../shared/number-coercion.js"; import type { CommandQueueEnqueueOptions } from "./command-queue.types.js"; import { GatewayDrainingError, diff --git a/src/process/exec-runner.ts b/src/process/exec-runner.ts index 53068c51c59e..746f7a19806e 100644 --- a/src/process/exec-runner.ts +++ b/src/process/exec-runner.ts @@ -1,11 +1,11 @@ import process from "node:process"; import { expectDefined } from "@openclaw/normalization-core"; import { toErrorObject } from "@openclaw/normalization-core/error-coercion"; +import { resolveTimerTimeoutMs } from "@openclaw/normalization-core/number-coercion"; import { decodeWindowsOutputBuffer, resolveWindowsConsoleEncoding, } from "../infra/windows-encoding.js"; -import { resolveTimerTimeoutMs } from "../shared/number-coercion.js"; import { releaseChildProcessOutputAfterExit } from "./child-process.js"; import { appendCapturedOutput, diff --git a/src/process/exec.ts b/src/process/exec.ts index 869baf080a4c..1c55f7d24de5 100644 --- a/src/process/exec.ts +++ b/src/process/exec.ts @@ -1,3 +1,4 @@ +import { resolveTimerTimeoutMs } from "@openclaw/normalization-core/number-coercion"; // Exec helpers run subprocesses with normalized output, timeout, and abort handling. import { danger, shouldLogVerbose } from "../globals.js"; import { @@ -5,7 +6,6 @@ import { resolveWindowsConsoleEncoding, } from "../infra/windows-encoding.js"; import { logDebug, logError } from "../logger.js"; -import { resolveTimerTimeoutMs } from "../shared/number-coercion.js"; import { releaseChildProcessOutputAfterExit } from "./child-process.js"; import { resolveMaxOutputBytes, type CommandOutputStream } from "./exec-output.js"; import { runCommandWithTimeout } from "./exec-runner.js"; diff --git a/src/secrets/configure.ts b/src/secrets/configure.ts index cd1b9e0664ad..1cda511215ab 100644 --- a/src/secrets/configure.ts +++ b/src/secrets/configure.ts @@ -2,6 +2,7 @@ import path from "node:path"; import { isDeepStrictEqual } from "node:util"; import { confirm, select, text } from "@clack/prompts"; +import { parseStrictPositiveInteger } from "@openclaw/normalization-core/number-coercion"; import { normalizeOptionalLowercaseString, normalizeOptionalString, @@ -20,7 +21,6 @@ import { type SecretRefSource, } from "../config/types.secrets.js"; import { isSafeExecutableValue } from "../infra/exec-safety.js"; -import { parseStrictPositiveInteger } from "../infra/parse-finite-number.js"; import { loadPluginManifestRegistryCore } from "../plugins/manifest-registry.js"; import { normalizeAgentId } from "../routing/session-key.js"; import { runSecretsApply, type SecretsApplyResult } from "./apply.js"; diff --git a/src/secrets/resolve.test.ts b/src/secrets/resolve.test.ts index 7ea5c6d3bf68..7db4ceae605a 100644 --- a/src/secrets/resolve.test.ts +++ b/src/secrets/resolve.test.ts @@ -2,9 +2,9 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; +import { MAX_TIMER_TIMEOUT_MS } from "@openclaw/normalization-core/number-coercion"; import { afterAll, beforeAll, describe, expect, it, vi } from "vitest"; import type { OpenClawConfig } from "../config/config.js"; -import { MAX_TIMER_TIMEOUT_MS } from "../shared/number-coercion.js"; import { killPidIfAlive, readPidFile, diff --git a/src/secrets/shared.ts b/src/secrets/shared.ts index 8596c2925721..9d49779367f8 100644 --- a/src/secrets/shared.ts +++ b/src/secrets/shared.ts @@ -1,8 +1,8 @@ /** Shared parsing and file helpers for secrets migration/runtime code. */ import path from "node:path"; +import { resolvePositiveTimerTimeoutMs } from "@openclaw/normalization-core/number-coercion"; import { privateFileStoreSync } from "../infra/private-file-store.js"; import { replaceFileAtomicSync } from "../infra/replace-file.js"; -import { resolvePositiveTimerTimeoutMs } from "../shared/number-coercion.js"; export { isRecord } from "../utils.js"; /** diff --git a/src/security/audit-gateway-config.ts b/src/security/audit-gateway-config.ts index 491472048c20..173c81b2827f 100644 --- a/src/security/audit-gateway-config.ts +++ b/src/security/audit-gateway-config.ts @@ -1,5 +1,6 @@ // Audits gateway config for bind, auth, and exposure risks. import { isIP } from "node:net"; +import { parseStrictNonNegativeInteger } from "@openclaw/normalization-core/number-coercion"; import { hasNonEmptyString, normalizeLowercaseStringOrEmpty, @@ -11,7 +12,6 @@ import type { OpenClawConfig } from "../config/types.openclaw.js"; import { hasConfiguredSecretInput } from "../config/types.secrets.js"; import { resolveGatewayAuth } from "../gateway/auth-resolve.js"; import { resolveGatewayAuthTokenSourceConflict } from "../gateway/auth-token-source-conflict.js"; -import { parseStrictNonNegativeInteger } from "../infra/parse-finite-number.js"; import type { SecurityAuditFinding } from "./audit.types.js"; import { collectCoreInsecureOrDangerousFlags } from "./core-dangerous-config-flags.js"; import { DEFAULT_GATEWAY_HTTP_TOOL_DENY } from "./dangerous-tools.js"; diff --git a/src/sessions/session-state-events.ts b/src/sessions/session-state-events.ts index 0783fb6166ef..8f3d29722de6 100644 --- a/src/sessions/session-state-events.ts +++ b/src/sessions/session-state-events.ts @@ -1,5 +1,6 @@ /** Best-effort durable signal log for session state changes. */ import type { DatabaseSync } from "node:sqlite"; +import { safeParseJsonRecord } from "@openclaw/normalization-core/json-coercion"; import type { Insertable, Selectable } from "kysely"; import { loadSessionEntryReadOnly } from "../config/sessions/session-accessor.js"; import type { SessionEntry } from "../config/sessions/types.js"; @@ -88,22 +89,8 @@ function normalizeOptionalSqliteNumber( return value === undefined ? undefined : normalizeSqliteNumber(value); } -function parsePayload(value: string | null): Record | undefined { - if (!value) { - return undefined; - } - try { - const parsed: unknown = JSON.parse(value); - return parsed && typeof parsed === "object" && !Array.isArray(parsed) - ? (parsed as Record) - : undefined; - } catch { - return undefined; - } -} - function rowToSessionStateEvent(row: SessionStateEventRow): SessionStateEventRecord { - const payload = parsePayload(row.payload_json); + const payload = row.payload_json ? safeParseJsonRecord(row.payload_json) : undefined; return { sequence: normalizeSqliteNumber(row.sequence) ?? 0, sessionKey: row.session_key, diff --git a/src/shared/number-coercion.ts b/src/shared/number-coercion.ts deleted file mode 100644 index 4c93865de6ca..000000000000 --- a/src/shared/number-coercion.ts +++ /dev/null @@ -1,2 +0,0 @@ -/** Shared numeric coercion facade for legacy imports inside core. */ -export * from "@openclaw/normalization-core/number-coercion"; diff --git a/src/state/openclaw-agent-db-session-migrations.ts b/src/state/openclaw-agent-db-session-migrations.ts index 6f8fca44f535..59401dbab9bc 100644 --- a/src/state/openclaw-agent-db-session-migrations.ts +++ b/src/state/openclaw-agent-db-session-migrations.ts @@ -1,4 +1,5 @@ import type { DatabaseSync } from "node:sqlite"; +import { safeParseJsonRecord } from "@openclaw/normalization-core/json-coercion"; import { asOptionalRecord } from "@openclaw/normalization-core/record-coerce"; import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce"; import { normalizeChatType, type ChatType } from "../channels/chat-type.js"; @@ -10,14 +11,7 @@ import { deriveSessionChatTypeFromKey } from "../sessions/session-chat-type-shar type MigratedConversationEntry = Record; function parseConversationEntry(value: unknown): MigratedConversationEntry | undefined { - if (typeof value !== "string") { - return undefined; - } - try { - return asOptionalRecord(JSON.parse(value)); - } catch { - return undefined; - } + return typeof value === "string" ? safeParseJsonRecord(value) : undefined; } function inferMigratedChatType(params: { diff --git a/src/state/openclaw-state-db.paths.ts b/src/state/openclaw-state-db.paths.ts index e7df2f10946b..482aa7b68c3d 100644 --- a/src/state/openclaw-state-db.paths.ts +++ b/src/state/openclaw-state-db.paths.ts @@ -2,8 +2,8 @@ import os from "node:os"; import path from "node:path"; import { isMainThread, threadId } from "node:worker_threads"; +import { parseStrictNonNegativeInteger } from "@openclaw/normalization-core/number-coercion"; import { resolveStateDir } from "../config/paths.js"; -import { parseStrictNonNegativeInteger } from "../infra/parse-finite-number.js"; /** * Path helpers for the shared OpenClaw SQLite state database. diff --git a/src/state/openclaw-state-lease.ts b/src/state/openclaw-state-lease.ts index 07a88f3899be..27b9bc25c517 100644 --- a/src/state/openclaw-state-lease.ts +++ b/src/state/openclaw-state-lease.ts @@ -1,6 +1,7 @@ // Host-owned SQLite leases serialize trusted work across processes. import { randomUUID } from "node:crypto"; import type { DatabaseSync } from "node:sqlite"; +import { MAX_TIMER_TIMEOUT_MS } from "@openclaw/normalization-core/number-coercion"; import { computeBackoff, sleepWithAbort } from "../infra/backoff.js"; import { executeSqliteQuerySync, @@ -9,7 +10,6 @@ import { } from "../infra/kysely-sync.js"; import { isSqliteLockError } from "../infra/sqlite-transaction.js"; import { loggingState } from "../logging/state.js"; -import { MAX_TIMER_TIMEOUT_MS } from "../shared/number-coercion.js"; import type { DB as OpenClawStateKyselyDatabase } from "./openclaw-state-db.generated.js"; import { openOpenClawStateDatabase, diff --git a/src/talk/forced-consult-coordinator.test.ts b/src/talk/forced-consult-coordinator.test.ts index cd56e9387674..44fb90847559 100644 --- a/src/talk/forced-consult-coordinator.test.ts +++ b/src/talk/forced-consult-coordinator.test.ts @@ -1,6 +1,6 @@ +import { MAX_TIMER_TIMEOUT_MS } from "@openclaw/normalization-core/number-coercion"; // Forced consult coordinator tests cover forced handoff to agent consultation. import { describe, expect, it, vi } from "vitest"; -import { MAX_TIMER_TIMEOUT_MS } from "../shared/number-coercion.js"; import { createRealtimeVoiceForcedConsultCoordinator } from "./forced-consult-coordinator.js"; describe("realtime voice forced consult coordinator", () => { diff --git a/src/talk/forced-consult-coordinator.ts b/src/talk/forced-consult-coordinator.ts index 956502aea1c9..051a5cc6de56 100644 --- a/src/talk/forced-consult-coordinator.ts +++ b/src/talk/forced-consult-coordinator.ts @@ -5,7 +5,7 @@ * native provider tool call can still arrive later. This coordinator prevents * duplicate consults and keeps late native calls correlated to forced handles. */ -import { resolveTimerTimeoutMs } from "../shared/number-coercion.js"; +import { resolveTimerTimeoutMs } from "@openclaw/normalization-core/number-coercion"; import { matchRealtimeVoiceConsultQuestions, readRealtimeVoiceConsultQuestion, diff --git a/src/tts/tts-core.test.ts b/src/tts/tts-core.test.ts index 391cdcd328d4..9c82dad1c3ed 100644 --- a/src/tts/tts-core.test.ts +++ b/src/tts/tts-core.test.ts @@ -1,8 +1,8 @@ // TTS core tests cover provider selection, synthesis, and error handling. import { readFileSync } from "node:fs"; +import { MAX_TIMER_TIMEOUT_MS } from "@openclaw/normalization-core/number-coercion"; import { describe, expect, it, vi } from "vitest"; import type { AssistantMessage, Model, Usage } from "../llm/types.js"; -import { MAX_TIMER_TIMEOUT_MS } from "../shared/number-coercion.js"; import type { SpeechModelOverridePolicy } from "./provider-types.js"; import { resolveSpeechProviderApiKey, summarizeText } from "./tts-core.js"; import type { ResolvedTtsConfig } from "./tts-types.js"; diff --git a/src/tts/tts-core.ts b/src/tts/tts-core.ts index 77e0e6ef6e04..bb4790e5e942 100644 --- a/src/tts/tts-core.ts +++ b/src/tts/tts-core.ts @@ -1,3 +1,4 @@ +import { resolveTimerTimeoutMs } from "@openclaw/normalization-core/number-coercion"; // TTS core coordinates text preparation, provider selection, and speech output. import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce"; import { @@ -8,7 +9,6 @@ import { } from "../agents/model-selection.js"; import type { OpenClawConfig } from "../config/types.js"; import type { TextContent } from "../llm/types.js"; -import { resolveTimerTimeoutMs } from "../shared/number-coercion.js"; import type { ResolvedTtsConfig } from "./tts-types.js"; export { normalizeApplyTextNormalization, diff --git a/src/tui/local-run-shutdown.ts b/src/tui/local-run-shutdown.ts index 5bec3ad3ed2a..9e373fe23d01 100644 --- a/src/tui/local-run-shutdown.ts +++ b/src/tui/local-run-shutdown.ts @@ -2,7 +2,7 @@ import { MAX_TIMER_TIMEOUT_MS, parseStrictNonNegativeInteger, -} from "../infra/parse-finite-number.js"; +} from "@openclaw/normalization-core/number-coercion"; // Local TUI runs get extra shutdown time because embedded agents/providers may still be closing. const LOCAL_RUN_SHUTDOWN_GRACE_MS = 120_000; diff --git a/src/tui/tui.test.ts b/src/tui/tui.test.ts index c9fe012a25d2..3791d6c0d567 100644 --- a/src/tui/tui.test.ts +++ b/src/tui/tui.test.ts @@ -1,9 +1,9 @@ // Covers core TUI state transitions and backend event rendering. import { EventEmitter } from "node:events"; import path from "node:path"; +import { MAX_TIMER_TIMEOUT_MS } from "@openclaw/normalization-core/number-coercion"; import { afterEach, describe, expect, it, vi } from "vitest"; import type { OpenClawConfig } from "../config/config.js"; -import { MAX_TIMER_TIMEOUT_MS } from "../infra/parse-finite-number.js"; import { MALFORMED_STREAMING_FRAGMENT_ERROR_MESSAGE } from "../shared/assistant-error-format.js"; import { withEnv } from "../test-utils/env.js"; import { getSlashCommands, parseCommand } from "./commands.js"; diff --git a/src/utils.test.ts b/src/utils.test.ts index df92dfa52473..81aa95aabeac 100644 --- a/src/utils.test.ts +++ b/src/utils.test.ts @@ -1,9 +1,9 @@ // Tests shared utility helpers used by CLI and runtime modules. import fs from "node:fs"; import path from "node:path"; +import { MAX_TIMER_TIMEOUT_MS } from "@openclaw/normalization-core/number-coercion"; import { describe, expect, it, vi } from "vitest"; import { isAbortError } from "./infra/abort-signal.js"; -import { MAX_TIMER_TIMEOUT_MS } from "./shared/number-coercion.js"; import { withTestDir } from "./test-helpers/temp-dir.js"; import { withEnv } from "./test-utils/env.js"; import { diff --git a/src/utils/cjk-chars.test.ts b/src/utils/cjk-chars.test.ts index 874cb5cc4979..48d38bb6fa28 100644 --- a/src/utils/cjk-chars.test.ts +++ b/src/utils/cjk-chars.test.ts @@ -1,10 +1,10 @@ // CJK character tests cover detection and width handling for CJK text. -import { describe, expect, it } from "vitest"; import { CHARS_PER_TOKEN_ESTIMATE, estimateStringChars, estimateTokensFromChars, -} from "./cjk-chars.js"; +} from "@openclaw/normalization-core/cjk-chars"; +import { describe, expect, it } from "vitest"; describe("estimateStringChars", () => { it("returns 0 for empty string", () => { diff --git a/src/utils/cjk-chars.ts b/src/utils/cjk-chars.ts deleted file mode 100644 index 1bdb27fc29fc..000000000000 --- a/src/utils/cjk-chars.ts +++ /dev/null @@ -1,5 +0,0 @@ -export { - CHARS_PER_TOKEN_ESTIMATE, - estimateStringChars, - estimateTokensFromChars, -} from "@openclaw/normalization-core/cjk-chars"; diff --git a/src/utils/sleep.ts b/src/utils/sleep.ts index 5db5da634af1..df21eb1e2cae 100644 --- a/src/utils/sleep.ts +++ b/src/utils/sleep.ts @@ -1,6 +1,6 @@ +import { resolveTimerTimeoutMs } from "@openclaw/normalization-core/number-coercion"; import { sleepWithAbort } from "@openclaw/retry"; import { createAbortError } from "../infra/abort-signal.js"; -import { resolveTimerTimeoutMs } from "../shared/number-coercion.js"; /** Promise-based sleep that clamps timer inputs through the shared timeout resolver. */ export function sleep(ms: number, signal?: AbortSignal): Promise { diff --git a/test/extension-import-boundaries.test.ts b/test/extension-import-boundaries.test.ts index fd1eef0caf3e..ea56a7b9955a 100644 --- a/test/extension-import-boundaries.test.ts +++ b/test/extension-import-boundaries.test.ts @@ -8,7 +8,6 @@ import { } from "../scripts/check-extension-plugin-sdk-boundary.mts"; import { main as sdkPackageMain } from "../scripts/check-sdk-package-extension-import-boundary.mts"; import { main as srcExtensionMain } from "../scripts/check-src-extension-import-boundary.mts"; -import { collectModuleReferencesFromSource } from "../scripts/lib/guard-inventory-utils.mjs"; import { createCapturedIo } from "./helpers/captured-io.js"; import { useAutoCleanupTempDirTracker } from "./helpers/temp-dir.js"; @@ -33,10 +32,6 @@ const boundaryInventoryCases: Array<{ name: "extension src outside plugin-sdk boundary", output: getJsonOutput(extensionPluginSdkMain, ["--mode=src-outside-plugin-sdk", "--json"]), }, - { - name: "extension plugin-sdk-internal boundary", - output: getJsonOutput(extensionPluginSdkMain, ["--mode=plugin-sdk-internal", "--json"]), - }, { name: "extension relative-outside-package boundary", output: getJsonOutput(extensionPluginSdkMain, ["--mode=relative-outside-package", "--json"]), @@ -47,26 +42,6 @@ const boundaryInventoryCases: Array<{ }, ]; -describe("fast module reference scanner", () => { - it("collects code references without matching comments or strings", () => { - expect( - collectModuleReferencesFromSource(` -// import "./commented"; -const text = 'import("./string")'; -import "./side-effect"; -import type { Example } from "./types"; -export { Example } from "./public"; -await import("./runtime"); -`), - ).toEqual([ - { kind: "import", line: 4, specifier: "./side-effect" }, - { kind: "import", line: 5, specifier: "./types" }, - { kind: "export", line: 6, specifier: "./public" }, - { kind: "dynamic-import", line: 7, specifier: "./runtime" }, - ]); - }); -}); - describe("extension import boundary inventories", () => { it.each(boundaryInventoryCases)("$name JSON output stays empty", async ({ output }) => { const jsonOutput = await output; @@ -167,7 +142,7 @@ describe("production plugin normalization ownership boundary", () => { facade: "openclaw/plugin-sdk/error-runtime", }, ])( - "rejects $name with a specific public facade when known", + "rejects $name with a specific SDK facade when known", async ({ source, file, kind, specifier, resolvedPath, facade }) => { const checker = createBoundaryFixture({ source, file }); const captured = createCapturedIo(); @@ -187,7 +162,10 @@ describe("production plugin normalization ownership boundary", () => { specifier, resolvedPath, }); - expect(entries[0]?.reason).toContain(facade ?? "matching public openclaw/plugin-sdk facade"); + expect(entries[0]?.reason).toContain(facade ?? "matching openclaw/plugin-sdk facade"); + if (facade === "openclaw/plugin-sdk/number-runtime") { + expect(entries[0]?.reason).toContain("bundled/private-local"); + } }, ); @@ -223,22 +201,6 @@ describe("production plugin normalization ownership boundary", () => { }, source: 'import "@openclaw/normalization-core/record-coerce";', }, - { - name: "comments and string literals", - source: [ - '// import "@openclaw/normalization-core/record-coerce";', - 'const text = "import(\\\"@openclaw/normalization-core/error-coercion\\\")";', - ].join("\n"), - }, - { - name: "serialized local guard", - source: [ - "function isRecord(value: unknown) {", - ' return value !== null && typeof value === "object" && !Array.isArray(value);', - "}", - "export const serialized = isRecord.toString();", - ].join("\n"), - }, ])("allows $name", async ({ source, file, packageJson }) => { const checker = createBoundaryFixture({ source, file, packageJson }); const captured = createCapturedIo(); @@ -264,7 +226,18 @@ describe("production plugin normalization ownership boundary", () => { expect(captured.readStdout()).toContain("re-exports"); expect(captured.readStdout()).toContain("@openclaw/normalization-core/error-coercion"); expect(captured.readStdout()).toContain("openclaw/plugin-sdk/error-runtime"); - expect(captured.readStderr()).toContain("this strict mode has no baseline"); + expect(captured.readStderr()).toContain("violations found (1)"); + }); + + it("rejects plugin-sdk-internal through the strict src-outside-plugin-sdk rule", async () => { + const checker = createBoundaryFixture({ + source: 'import "../../../src/plugin-sdk-internal/private.js";', + }); + const captured = createCapturedIo(); + + expect(await checker.main(["--mode=src-outside-plugin-sdk"], captured.io)).toBe(1); + expect(captured.readStdout()).toContain("src/plugin-sdk-internal/private.js"); + expect(captured.readStderr()).toContain("violations found (1)"); }); }); diff --git a/test/package-scripts.test.ts b/test/package-scripts.test.ts index 13d7a51161a2..496532e15a6a 100644 --- a/test/package-scripts.test.ts +++ b/test/package-scripts.test.ts @@ -152,12 +152,6 @@ describe("package scripts", () => { ); }); - it("exposes the strict production plugin normalization boundary command", () => { - expect(readPackageJson().scripts["lint:extensions:no-normalization-core-bypass"]).toBe( - "node --import tsx scripts/check-extension-plugin-sdk-boundary.mts --mode=normalization-core-bypass", - ); - }); - it("runs runtime postbuild before plugin SDK strict export checks", () => { expect(readPackageJson().scripts["build:plugin-sdk:strict-smoke"]).toBe( "node --import tsx scripts/tsdown-build.mts && node scripts/runtime-postbuild.mjs && node --import tsx scripts/run-with-env.mts OPENCLAW_PLUGIN_SDK_CANONICAL_DTS=1 -- node --import tsx scripts/write-plugin-sdk-entry-dts.ts && node --import tsx scripts/check-plugin-sdk-exports.mts", diff --git a/test/scripts/arg-utils.test.ts b/test/scripts/arg-utils.test.ts index 61f6b708eec5..c036cbd8d383 100644 --- a/test/scripts/arg-utils.test.ts +++ b/test/scripts/arg-utils.test.ts @@ -4,6 +4,8 @@ import { booleanFlag, classifyBoundedUnsignedDecimal, intFlag, + isOpenEndedTruthyValue, + isStrictAffirmativeValue, parseFlagArgs, parsePermissiveBooleanToken, parseStrictBooleanArg, @@ -80,6 +82,35 @@ describe("scripts/lib/arg-utils permissive Boolean tokens", () => { }); }); +describe("scripts/lib/arg-utils environment Boolean policies", () => { + it.each([ + { input: undefined, expected: false }, + { input: "", expected: false }, + { input: " ", expected: false }, + { input: "0", expected: false }, + { input: " FALSE ", expected: false }, + { input: "no", expected: false }, + { input: "off", expected: true }, + { input: "enabled", expected: true }, + { input: "1", expected: true }, + ])("applies open-ended truthiness to $input", ({ input, expected }) => { + expect(isOpenEndedTruthyValue(input)).toBe(expected); + }); + + it.each([ + { input: undefined, expected: false }, + { input: "", expected: false }, + { input: "0", expected: false }, + { input: "on", expected: false }, + { input: "enabled", expected: false }, + { input: "1", expected: true }, + { input: " TRUE ", expected: true }, + { input: "Yes", expected: true }, + ])("applies strict affirmative truthiness to $input", ({ input, expected }) => { + expect(isStrictAffirmativeValue(input)).toBe(expected); + }); +}); + describe("scripts/lib/arg-utils parseFlagArgs", () => { it("uses the last value when a flag is repeated", () => { expect(readFlagValue(["-p", "first.json", "-p", "second.json"], "-p")).toBe("second.json"); @@ -161,24 +192,6 @@ describe("scripts/lib/arg-utils parseFlagArgs", () => { ).toThrow("--json was provided more than once"); }); - it("requires custom specs to declare consumed flags", () => { - expect(() => - parseFlagArgs(["--custom"], {}, [ - { - consume(argv, index) { - if (argv[index] !== "--custom") { - return null; - } - return { - nextIndex: index, - apply() {}, - }; - }, - }, - ]), - ).toThrow("parseFlagArgs specs must declare a flag for consumed options"); - }); - it("rejects missing string flag values before consuming the next option", () => { expect(() => parseFlagArgs(["--base", "--head", "HEAD"], { base: "origin/main", head: "HEAD" }, [ diff --git a/test/scripts/build-identity.test.ts b/test/scripts/build-identity.test.ts new file mode 100644 index 000000000000..ffa1eacbd5a3 --- /dev/null +++ b/test/scripts/build-identity.test.ts @@ -0,0 +1,60 @@ +import { describe, expect, it, vi } from "vitest"; +import { resolveBuildIdentityEnvironment } from "../../scripts/lib/build-identity.mts"; + +describe("resolveBuildIdentityEnvironment", () => { + it.each([ + { + env: { GIT_COMMIT: "A".repeat(40), GIT_SHA: "b".repeat(40) }, + expected: "a".repeat(40), + readsCheckout: false, + }, + { + env: { GIT_SHA: "B".repeat(40), GITHUB_SHA: "c".repeat(40) }, + expected: "b".repeat(40), + readsCheckout: false, + }, + { + env: { GITHUB_SHA: "c".repeat(40) }, + expected: "d".repeat(40), + readsCheckout: true, + }, + ])("preserves build source precedence %#", ({ env, expected, readsCheckout }) => { + const readGitCommit = vi.fn(() => "D".repeat(40)); + const resolved = resolveBuildIdentityEnvironment({ + commitLabel: "build commit", + env, + now: () => new Date("2026-07-10T12:34:56.000Z"), + readGitCommit, + }); + + expect(resolved.GIT_COMMIT).toBe(expected); + expect(readGitCommit).toHaveBeenCalledTimes(readsCheckout ? 1 : 0); + }); + + it("uses workflow identity only when the checkout cannot be read", () => { + expect( + resolveBuildIdentityEnvironment({ + commitLabel: "runtime pack commit", + env: { + GITHUB_SHA: "e".repeat(40), + OPENCLAW_BUILD_TIMESTAMP: " 2026-07-10T01:02:03.000Z ", + }, + now: () => new Date("2026-07-11T12:34:56.000Z"), + readGitCommit: () => null, + }), + ).toMatchObject({ + GIT_COMMIT: "e".repeat(40), + OPENCLAW_BUILD_TIMESTAMP: "2026-07-10T01:02:03.000Z", + }); + }); + + it("uses the owner label in malformed commit diagnostics", () => { + expect(() => + resolveBuildIdentityEnvironment({ + commitLabel: "runtime pack commit", + env: { GIT_COMMIT: "deadbeef" }, + readGitCommit: () => null, + }), + ).toThrow("runtime pack commit must be a full 40-character hexadecimal SHA"); + }); +}); diff --git a/test/scripts/check-coercion-helper-declarations.test.ts b/test/scripts/check-coercion-helper-declarations.test.ts index d473de576d91..e45db8a8deb5 100644 --- a/test/scripts/check-coercion-helper-declarations.test.ts +++ b/test/scripts/check-coercion-helper-declarations.test.ts @@ -20,91 +20,32 @@ describe("coercion helper declaration AST guard", () => { it("finds functions, callable variables, methods, fields, and object properties", () => { const source = [ "export async function readString() {}", - "if (true) {", - " function asRecord() {}", - "}", "const isRecord = (value: unknown) => Boolean(value);", - "let toError = function (value: unknown) { return value; };", - "var optionalString = async (value: unknown) => value;", - "const readString = (((value: unknown) => String(value)) satisfies ((value: unknown) => string));", - "function readNumber(record: Record, key: string) { return record[key]; }", - "const timestampMs = (value: unknown) => Number(value);", - "function readBoolean() {}", - "function readOptionalString() {}", - "function normalizeString() {}", - "const asString = (value: unknown) => String(value);", - "function asObject() {}", "const readOptionalString = normalizeOptionalString;", "const optionalString = helpers.readStringValue;", - "const asObject = helpers.asOptionalRecord;", + "const timestampMs = (((value: unknown) => Number(value)) satisfies ((value: unknown) => number));", "class Example {", - " readString() {}", - " toError = () => new Error();", + " readNumber() {}", " asRecord = function () { return {}; };", "}", "const object = {", - " optionalString() {},", " readBoolean: () => true,", - " readNumber: function () { return 1; },", "};", - "function normalizeOptionalString() {}", - "const parseDateFirstTimestampMs = () => 0;", - "function safeParseJsonRecord() {}", - "function resolveIntegerOption() {}", + "function normalizeAgentId() {}", + "const isValidAgentId = () => true;", ].join("\n"); expect(findBannedCoercionHelperDeclarations(source, "src/example.ts")).toEqual([ { file: "src/example.ts", kind: "function", line: 1, name: "readString" }, - { file: "src/example.ts", kind: "function", line: 3, name: "asRecord" }, - { file: "src/example.ts", kind: "variable", line: 5, name: "isRecord" }, - { file: "src/example.ts", kind: "variable", line: 6, name: "toError" }, - { file: "src/example.ts", kind: "variable", line: 7, name: "optionalString" }, - { file: "src/example.ts", kind: "variable", line: 8, name: "readString" }, - { file: "src/example.ts", kind: "function", line: 9, name: "readNumber" }, - { file: "src/example.ts", kind: "variable", line: 10, name: "timestampMs" }, - { file: "src/example.ts", kind: "function", line: 11, name: "readBoolean" }, - { file: "src/example.ts", kind: "function", line: 12, name: "readOptionalString" }, - { file: "src/example.ts", kind: "function", line: 13, name: "normalizeString" }, - { file: "src/example.ts", kind: "variable", line: 14, name: "asString" }, - { file: "src/example.ts", kind: "function", line: 15, name: "asObject" }, - { - file: "src/example.ts", - kind: "variable", - line: 16, - name: "readOptionalString", - }, - { file: "src/example.ts", kind: "variable", line: 17, name: "optionalString" }, - { file: "src/example.ts", kind: "variable", line: 18, name: "asObject" }, - { file: "src/example.ts", kind: "method", line: 20, name: "readString" }, - { file: "src/example.ts", kind: "field", line: 21, name: "toError" }, - { file: "src/example.ts", kind: "field", line: 22, name: "asRecord" }, - { file: "src/example.ts", kind: "method", line: 25, name: "optionalString" }, - { file: "src/example.ts", kind: "property", line: 26, name: "readBoolean" }, - { file: "src/example.ts", kind: "property", line: 27, name: "readNumber" }, - { - file: "src/example.ts", - kind: "function", - line: 29, - name: "normalizeOptionalString", - }, - { - file: "src/example.ts", - kind: "variable", - line: 30, - name: "parseDateFirstTimestampMs", - }, - { - file: "src/example.ts", - kind: "function", - line: 31, - name: "safeParseJsonRecord", - }, - { - file: "src/example.ts", - kind: "function", - line: 32, - name: "resolveIntegerOption", - }, + { file: "src/example.ts", kind: "variable", line: 2, name: "isRecord" }, + { file: "src/example.ts", kind: "variable", line: 3, name: "readOptionalString" }, + { file: "src/example.ts", kind: "variable", line: 4, name: "optionalString" }, + { file: "src/example.ts", kind: "variable", line: 5, name: "timestampMs" }, + { file: "src/example.ts", kind: "method", line: 7, name: "readNumber" }, + { file: "src/example.ts", kind: "field", line: 8, name: "asRecord" }, + { file: "src/example.ts", kind: "property", line: 11, name: "readBoolean" }, + { file: "src/example.ts", kind: "function", line: 13, name: "normalizeAgentId" }, + { file: "src/example.ts", kind: "variable", line: 14, name: "isValidAgentId" }, ]); }); @@ -126,9 +67,10 @@ describe("coercion helper declaration AST guard", () => { expect(findBannedCoercionHelperDeclarations(source, "src/example.ts")).toEqual([]); }); - it("allows an exact function-kind declaration and reports ordinary excess/stale counts", () => { + it("allows one exact declaration and reports duplicate, unowned, and stale entries", () => { const declarations: CoercionHelperDeclaration[] = [ { file: "src/allowed.ts", kind: "function", line: 2, name: "isRecord" }, + { file: "src/allowed.ts", kind: "function", line: 3, name: "isRecord" }, { file: "src/new.ts", kind: "function", line: 4, name: "readString" }, ]; const carveOuts: CoercionHelperCarveOut[] = [ @@ -136,29 +78,28 @@ describe("coercion helper declaration AST guard", () => { file: "src/allowed.ts", name: "isRecord", kind: "function", - count: 1, reason: "Dependency-free protocol boundary.", }, { file: "src/removed.ts", name: "toError", kind: "function", - count: 1, reason: "Hostile object trap semantics.", }, ]; expect(auditCoercionHelperDeclarations(declarations, carveOuts)).toEqual({ - excessDeclarations: [{ file: "src/new.ts", kind: "function", line: 4, name: "readString" }], + excessDeclarations: [ + { file: "src/allowed.ts", kind: "function", line: 3, name: "isRecord" }, + { file: "src/new.ts", kind: "function", line: 4, name: "readString" }, + ], invalidCarveOuts: [], staleCarveOuts: [ { file: "src/removed.ts", name: "toError", kind: "function", - count: 1, reason: "Hostile object trap semantics.", - actualCount: 0, }, ], }); @@ -177,14 +118,13 @@ describe("coercion helper declaration AST guard", () => { file: "src/owner.ts", name: "isRecord", kind: "function", - count: 1, reason: "Exact function owner.", }; expect(auditCoercionHelperDeclarations([declaration], [carveOut])).toEqual({ excessDeclarations: [declaration], invalidCarveOuts: [], - staleCarveOuts: [{ ...carveOut, actualCount: 0 }], + staleCarveOuts: [carveOut], }); }, ); @@ -194,7 +134,6 @@ describe("coercion helper declaration AST guard", () => { file: "src/owner.ts", name: "isRecord", kind: "function", - count: 1, reason: "Exact function owner.", }; const invalid = [ @@ -205,7 +144,7 @@ describe("coercion helper declaration AST guard", () => { file: "src/not-banned.ts", name: "domainParser", } as unknown as CoercionHelperCarveOut, - { ...valid, file: "src/blank.ts", count: 0, reason: "" }, + { ...valid, file: "src/blank.ts", reason: "" }, { ...valid, file: "src/kind.ts", @@ -216,7 +155,6 @@ describe("coercion helper declaration AST guard", () => { expect(auditCoercionHelperDeclarations([], invalid).invalidCarveOuts).toEqual([ "src/owner.ts [isRecord] is listed more than once", "src/not-banned.ts [domainParser] is not a banned helper name", - "src/blank.ts [isRecord] must have a positive count", "src/blank.ts [isRecord] needs a non-empty reason", "src/kind.ts [isRecord] has invalid kind getter", ]); @@ -253,32 +191,14 @@ describe("coercion helper declaration AST guard", () => { expect(findExportedCallableNames(source, "src/owner.ts")).toEqual(["alias", "canonical"]); }); - it.each([ - ["classified", ["kept"], [{ file: "src/owner.ts", name: "kept", status: "enforced" }]], - [ - "deferred", - ["format"], - [ - { - file: "src/owner.ts", - name: "format", - status: "deferred", - reason: "Meaningful public collision.", - }, - ], - ], - ] as const)("accepts a %s canonical export", (_label, names, classifications) => { - expect( - auditCanonicalCoercionExports(new Map([["src/owner.ts", names]]), classifications), - ).toEqual({ - invalidClassifications: [], - staleClassifications: [], - unclassifiedExports: [], - }); - }); - it("reports unclassified exports and stale, duplicate, or blank deferred entries", () => { const kept = { file: "src/owner.ts", name: "kept", status: "enforced" } as const; + const deferredKept = { + file: "src/owner.ts", + name: "format", + status: "deferred", + reason: "Meaningful public collision.", + } as const; const removed = { file: "src/owner.ts", name: "removed", @@ -292,8 +212,8 @@ describe("coercion helper declaration AST guard", () => { reason: "", } as const; const audit = auditCanonicalCoercionExports( - new Map([["src/owner.ts", ["kept", "newHelper"]]]), - [kept, kept, removed, blank], + new Map([["src/owner.ts", ["kept", "format", "newHelper"]]]), + [kept, deferredKept, kept, removed, blank], ); expect(audit.invalidClassifications).toEqual([ @@ -327,7 +247,6 @@ describe("coercion helper declaration AST guard", () => { file: "src/z.ts", name: "readString", kind: "function", - count: 1, reason: "Exact function owner.", }, ], @@ -348,10 +267,15 @@ describe("coercion helper declaration AST guard", () => { expect(output).toContain("Banned local coercion-helper declarations:"); expect(output).toContain("readString (method declaration)"); expect(output).toContain("Stale coercion-helper carve-outs:"); - expect(output).toContain("expected 1 function declaration(s), found 0"); - expect(output).toContain("Core/package/UI/workspace-script code"); - expect(output).toContain("Plugin production code"); - expect(output).toContain("number-runtime"); + expect(output).toContain( + "src/z.ts [readString] has no function declaration; remove the carve-out", + ); + expect(output).toContain( + "Core/package/UI/workspace-script code: use the matching @openclaw/normalization-core export or module.", + ); + expect(output).toContain( + "Bundled plugin production code: use the matching openclaw/plugin-sdk runtime; number-runtime is bundled/private-local, not a third-party typed contract.", + ); expect(output).toContain("Dependency-free, copied, generated, or serialized code"); }); diff --git a/test/scripts/ci-changed-node-test-plan.test.ts b/test/scripts/ci-changed-node-test-plan.test.ts index b99e9e6a966d..4d58571576bd 100644 --- a/test/scripts/ci-changed-node-test-plan.test.ts +++ b/test/scripts/ci-changed-node-test-plan.test.ts @@ -160,7 +160,9 @@ describe("CI changed Node test plan", () => { ]), ).toBe(true); expect( - hasSqliteSessionLifecycleAffectingChange(["src/media-understanding/provider-id.ts"]), + hasSqliteSessionLifecycleAffectingChange([ + "packages/media-understanding-common/src/provider-id.ts", + ]), ).toBe(false); expect(hasSqliteSessionLifecycleAffectingChange(["src/agents/model-auth.ts"])).toBe(false); expect(hasSqliteSessionLifecycleAffectingChange(["extensions/discord/src/index.ts"])).toBe( diff --git a/test/scripts/ci-node-test-plan.test.ts b/test/scripts/ci-node-test-plan.test.ts index 480e2ec4de10..1870d3c0fa18 100644 --- a/test/scripts/ci-node-test-plan.test.ts +++ b/test/scripts/ci-node-test-plan.test.ts @@ -434,8 +434,6 @@ describe("scripts/lib/ci-node-test-plan.mts", () => { expect( toolingGroups.every((group) => group.configs[0] === "test/vitest/vitest.tooling.config.ts"), ).toBe(true); - const toolingGroupSizes = toolingGroups.map((group) => group.includePatterns?.length ?? 0); - expect(Math.max(...toolingGroupSizes) - Math.min(...toolingGroupSizes)).toBeLessThanOrEqual(1); expect(new Set(toolingFiles).size).toBe(toolingFiles.length); expect(toolingFiles.toSorted((a, b) => a.localeCompare(b))).toEqual(listAllToolingTestFiles()); }); diff --git a/test/scripts/dependency-guard-script.test.ts b/test/scripts/dependency-guard-script.test.ts index 2dbbd8d624f4..0368bb7f0020 100644 --- a/test/scripts/dependency-guard-script.test.ts +++ b/test/scripts/dependency-guard-script.test.ts @@ -380,6 +380,7 @@ describe("dependency guard script", () => { const trustedAuthors = dependencyGuardCommentAuthors( "github-actions[bot], openclaw-autoscrub[bot]", ); + expect(dependencyGuardCommentAuthors(undefined)).toEqual(new Set(["github-actions[bot]"])); expect( isDependencyGuardMarkerComment( diff --git a/test/scripts/numeric-options.test.ts b/test/scripts/numeric-options.test.ts index cba1c8251d66..4847da4d56c0 100644 --- a/test/scripts/numeric-options.test.ts +++ b/test/scripts/numeric-options.test.ts @@ -1,5 +1,33 @@ import { describe, expect, it } from "vitest"; -import { readPositiveEnvInt } from "../../scripts/lib/numeric-options.mjs"; +import { + parseStrictNonNegativeDecimal, + readPositiveEnvInt, +} from "../../scripts/lib/numeric-options.mjs"; + +describe("parseStrictNonNegativeDecimal", () => { + it.each([ + ["0", 0], + [" 42 ", 42], + [42, 42], + ])("parses canonical decimal value %j", (raw, expected) => { + expect(parseStrictNonNegativeDecimal(raw, "limit")).toBe(expected); + }); + + it.each(["", "00", "01", "+1", "-1", "1.5", "1e3", "0x10"])( + "rejects non-canonical value %j", + (raw) => { + expect(() => parseStrictNonNegativeDecimal(raw, "limit")).toThrow( + "limit must be a non-negative integer", + ); + }, + ); + + it("distinguishes unsafe canonical integers", () => { + expect(() => parseStrictNonNegativeDecimal("9007199254740992", "limit")).toThrow( + "limit must be a safe integer", + ); + }); +}); describe("readPositiveEnvInt", () => { it("uses the fallback for missing or blank values", () => { diff --git a/test/scripts/plugin-boundary-report.test.ts b/test/scripts/plugin-boundary-report.test.ts index 8422a4544fc0..c2d8c19d9b73 100644 --- a/test/scripts/plugin-boundary-report.test.ts +++ b/test/scripts/plugin-boundary-report.test.ts @@ -6,28 +6,11 @@ import { type PluginBoundaryReportResult, } from "../../scripts/plugin-boundary-report.js"; -function requirePluginSdkSummary(summary: { - pluginSdk?: { - crossOwnerReservedImportCount?: unknown; - unusedReservedCount?: unknown; - }; -}) { - if (!summary.pluginSdk) { - throw new Error("Expected plugin SDK summary"); - } - return summary.pluginSdk; -} - describe("plugin-boundary-report", () => { let summaryResult: PluginBoundaryReportResult; beforeAll(() => { - summaryResult = createPluginBoundaryReport([ - "--summary", - "--json", - "--fail-on-cross-owner", - "--fail-on-unclassified-unused-reserved", - ]); + summaryResult = createPluginBoundaryReport(["--summary", "--json"]); }); it("emits compact CI-safe summary JSON", () => { @@ -44,10 +27,6 @@ describe("plugin-boundary-report", () => { dueForReview?: unknown; }>; }; - pluginSdk?: { - crossOwnerReservedImportCount?: unknown; - unusedReservedCount?: unknown; - }; memoryHostSdk?: { implementation?: unknown; }; @@ -70,9 +49,6 @@ describe("plugin-boundary-report", () => { expect((record.readerSample as unknown[]).length).toBeLessThanOrEqual(5); expect(record.dueForReview).toEqual(expect.any(Boolean)); } - const pluginSdk = requirePluginSdkSummary(summary); - expect(pluginSdk.crossOwnerReservedImportCount).toBe(0); - expect(pluginSdk.unusedReservedCount).toBe(0); expect(["private-core-bridge", "private-package-core-integrated"]).toContain( summary.memoryHostSdk?.implementation, ); diff --git a/test/scripts/security-sensitive-guard-script.test.ts b/test/scripts/security-sensitive-guard-script.test.ts index 95984d4eef94..ab6b2af5e979 100644 --- a/test/scripts/security-sensitive-guard-script.test.ts +++ b/test/scripts/security-sensitive-guard-script.test.ts @@ -183,6 +183,9 @@ describe("security-sensitive guard script", () => { const trustedAuthors = securitySensitiveGuardCommentAuthors( "github-actions[bot], openclaw-security-guard[bot]", ); + expect(securitySensitiveGuardCommentAuthors(undefined)).toEqual( + new Set(["github-actions[bot]"]), + ); expect( isSecuritySensitiveGuardMarkerComment( diff --git a/test/vitest/vitest.shared.config.ts b/test/vitest/vitest.shared.config.ts index 38f2b1fee5e9..b3e056b1a1b7 100644 --- a/test/vitest/vitest.shared.config.ts +++ b/test/vitest/vitest.shared.config.ts @@ -3,6 +3,7 @@ import fs from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; import acpCorePackageJson from "../../packages/acp-core/package.json" with { type: "json" }; +import normalizationCorePackageJson from "../../packages/normalization-core/package.json" with { type: "json" }; import { pluginSdkSubpaths } from "../../scripts/lib/plugin-sdk-entries.mts"; import privateLocalOnlyPluginSdkSubpaths from "../../scripts/lib/plugin-sdk-private-local-only-subpaths.json" with { type: "json" }; import { createStateSchemaInlinePlugin } from "../../scripts/lib/state-schema-inline-plugin.mts"; @@ -430,120 +431,10 @@ export const sharedVitestConfig = { find: "@openclaw/net-policy", replacement: path.join(repoRoot, "packages", "net-policy", "src", "index.ts"), }, - { - find: "@openclaw/normalization-core/agent-id", - replacement: path.join(repoRoot, "packages", "normalization-core", "src", "agent-id.ts"), - }, - { - find: "@openclaw/normalization-core/boolean-coercion", - replacement: path.join( - repoRoot, - "packages", - "normalization-core", - "src", - "boolean-coercion.ts", - ), - }, - { - find: "@openclaw/normalization-core/cjk-chars", - replacement: path.join(repoRoot, "packages", "normalization-core", "src", "cjk-chars.ts"), - }, - { - find: "@openclaw/normalization-core/error-coercion", - replacement: path.join( - repoRoot, - "packages", - "normalization-core", - "src", - "error-coercion.ts", - ), - }, - { - find: "@openclaw/normalization-core/json-schema", - replacement: path.join(repoRoot, "packages", "normalization-core", "src", "json-schema.ts"), - }, - { - find: "@openclaw/normalization-core/number-coercion", - replacement: path.join( - repoRoot, - "packages", - "normalization-core", - "src", - "number-coercion.ts", - ), - }, - { - find: "@openclaw/normalization-core/phone-presentation", - replacement: path.join( - repoRoot, - "packages", - "normalization-core", - "src", - "phone-presentation.ts", - ), - }, - { - find: "@openclaw/normalization-core/promise-like", - replacement: path.join( - repoRoot, - "packages", - "normalization-core", - "src", - "promise-like.ts", - ), - }, - { - find: "@openclaw/normalization-core/record-coerce", - replacement: path.join( - repoRoot, - "packages", - "normalization-core", - "src", - "record-coerce.ts", - ), - }, - { - find: "@openclaw/normalization-core/result", - replacement: path.join(repoRoot, "packages", "normalization-core", "src", "result.ts"), - }, - { - find: "@openclaw/normalization-core/stable-node-path", - replacement: path.join( - repoRoot, - "packages", - "normalization-core", - "src", - "stable-node-path.ts", - ), - }, - { - find: "@openclaw/normalization-core/string-coerce", - replacement: path.join( - repoRoot, - "packages", - "normalization-core", - "src", - "string-coerce.ts", - ), - }, - { - find: "@openclaw/normalization-core/string-normalization", - replacement: path.join( - repoRoot, - "packages", - "normalization-core", - "src", - "string-normalization.ts", - ), - }, - { - find: "@openclaw/normalization-core/utf16-slice", - replacement: path.join(repoRoot, "packages", "normalization-core", "src", "utf16-slice.ts"), - }, - { - find: /^@openclaw\/normalization-core$/u, - replacement: path.join(repoRoot, "packages", "normalization-core", "src", "index.ts"), - }, + ...sourcePackageAliasesFromExports( + "normalization-core", + normalizationCorePackageJson.exports, + ), sourcePackageAlias("markdown-core", "code-spans"), sourcePackageAlias("markdown-core", "fences"), sourcePackageAlias("media-core", "attachment-classify"), diff --git a/tsconfig.json b/tsconfig.json index 52d70a5291fd..106165524271 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -158,6 +158,9 @@ "./packages/normalization-core/src/error-coercion.ts" ], "@openclaw/normalization-core/expect": ["./packages/normalization-core/src/expect.ts"], + "@openclaw/normalization-core/json-coercion": [ + "./packages/normalization-core/src/json-coercion.ts" + ], "@openclaw/normalization-core/json-schema": [ "./packages/normalization-core/src/json-schema.ts" ], diff --git a/ui/src/app-navigation.ts b/ui/src/app-navigation.ts index d831ab4bf6b9..421c4dcc499b 100644 --- a/ui/src/app-navigation.ts +++ b/ui/src/app-navigation.ts @@ -1,9 +1,9 @@ +import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce"; import { isValidWorkboardBoardId } from "@openclaw/workboard-contract"; // Control UI app navigation defines sidebar and settings presentation metadata. import type { RouteId } from "./app-route-paths.ts"; import type { IconName } from "./components/icons.ts"; import { i18n, t } from "./i18n/index.ts"; -import { normalizeLowercaseStringOrEmpty } from "./lib/string-coerce.ts"; export type NavigationRouteId = RouteId; diff --git a/ui/src/app/assistant-identity.ts b/ui/src/app/assistant-identity.ts index 5509a9db1a28..08afaceacfe0 100644 --- a/ui/src/app/assistant-identity.ts +++ b/ui/src/app/assistant-identity.ts @@ -1,6 +1,6 @@ +import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce"; import type { GatewayBrowserClient } from "../api/gateway.ts"; import { normalizeAssistantIdentity, type AssistantIdentity } from "../lib/assistant-identity.ts"; -import { normalizeOptionalString } from "../lib/string-coerce.ts"; import { getSafeLocalStorage } from "../local-storage.ts"; const LOCAL_ASSISTANT_IDENTITY_KEY = "openclaw.control.assistant.v1"; diff --git a/ui/src/app/control-ui-auth.ts b/ui/src/app/control-ui-auth.ts index deb4cc213f3b..a613818bb466 100644 --- a/ui/src/app/control-ui-auth.ts +++ b/ui/src/app/control-ui-auth.ts @@ -1,5 +1,6 @@ // Control UI module implements control ui auth behavior. -import { normalizeOptionalString, uniqueStrings } from "../lib/string-coerce.ts"; +import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce"; +import { uniqueStrings } from "@openclaw/normalization-core/string-normalization"; type ControlUiAuthSource = { hello?: { auth?: { deviceToken?: string | null } | null } | null; diff --git a/ui/src/app/custom-theme.ts b/ui/src/app/custom-theme.ts index c24bf63e6d1d..04378ae15f73 100644 --- a/ui/src/app/custom-theme.ts +++ b/ui/src/app/custom-theme.ts @@ -1,7 +1,7 @@ import { asNullableRecord as readThemeRecord } from "@openclaw/normalization-core/record-coerce"; -import { truncateUtf16Safe } from "@openclaw/normalization-core/utf16-slice"; // Control UI module implements custom theme behavior. -import { normalizeOptionalString } from "../lib/string-coerce.ts"; +import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce"; +import { truncateUtf16Safe } from "@openclaw/normalization-core/utf16-slice"; const TWEAKCN_HOSTS = new Set(["tweakcn.com", "www.tweakcn.com"]); const THEME_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/; diff --git a/ui/src/app/exec-approval.ts b/ui/src/app/exec-approval.ts index e45925f3e8a8..b0ffc605c8b5 100644 --- a/ui/src/app/exec-approval.ts +++ b/ui/src/app/exec-approval.ts @@ -1,6 +1,6 @@ // Application-owned approval parsing and queue state. import { isRecord } from "@openclaw/normalization-core/record-coerce"; -import { normalizeOptionalString } from "../lib/string-coerce.ts"; +import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce"; export type ExecApprovalRequestPayload = { command: string; diff --git a/ui/src/app/settings.ts b/ui/src/app/settings.ts index de79f681bc05..2ebd30b99e87 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 { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce"; import { DEFAULT_SIDEBAR_ENTRIES, normalizeSidebarEntries, @@ -8,7 +9,6 @@ import { } from "../app-navigation.ts"; import { isSupportedLocale } from "../i18n/index.ts"; import { normalizeBoardSessionViews, type BoardSessionViews } from "../lib/board/settings.ts"; -import { normalizeOptionalString } from "../lib/string-coerce.ts"; import { getSafeLocalStorage, getSafeSessionStorage } from "../local-storage.ts"; import { normalizeSidebarSessionActivePanels, diff --git a/ui/src/app/startup-settings.ts b/ui/src/app/startup-settings.ts index 805d936d412e..73e62c5e4966 100644 --- a/ui/src/app/startup-settings.ts +++ b/ui/src/app/startup-settings.ts @@ -1,3 +1,4 @@ +import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce"; // Control UI startup settings resolve native auth handoff and URL parameters. import { CONTROL_UI_BOOTSTRAP_PROFILE_FRAGMENT_PARAM, @@ -5,7 +6,6 @@ import { type ControlUiBootstrapProfileHint, } from "../../../src/gateway/control-ui-contract.js"; import { inferBasePathFromPathname, sessionRouteNamespaceFromPath } from "../app-route-paths.ts"; -import { normalizeOptionalString } from "../lib/string-coerce.ts"; import type { UiSettings } from "./settings.ts"; type ApplicationStartupLocation = { diff --git a/ui/src/app/user-identity.ts b/ui/src/app/user-identity.ts index aaa144d144b4..a16007f21acf 100644 --- a/ui/src/app/user-identity.ts +++ b/ui/src/app/user-identity.ts @@ -1,7 +1,7 @@ +import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce"; // Control UI module implements user identity behavior. import { truncateUtf16Safe } from "@openclaw/normalization-core/utf16-slice"; import { isRenderableControlUiAvatarUrl, resolveChatAvatarRenderUrl } from "../lib/avatar.ts"; -import { normalizeOptionalString } from "../lib/string-coerce.ts"; const MAX_LOCAL_USER_NAME = 50; const MAX_LOCAL_USER_TEXT_AVATAR = 16; diff --git a/ui/src/components/app-sidebar-session-navigation-logic.ts b/ui/src/components/app-sidebar-session-navigation-logic.ts index a3f64af0f40a..f601de89c4ba 100644 --- a/ui/src/components/app-sidebar-session-navigation-logic.ts +++ b/ui/src/components/app-sidebar-session-navigation-logic.ts @@ -1,3 +1,4 @@ +import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce"; import type { GatewaySessionRow, SessionsListResult } from "../api/types.ts"; import { SIDEBAR_NAV_ROUTES } from "../app-navigation.ts"; import type { NavigationRouteId } from "../app-navigation.ts"; @@ -38,7 +39,6 @@ import { resolveUiSessionNavigationParentKey, } from "../lib/sessions/session-key.ts"; import { reconcileSidebarZone } from "../lib/sidebar-zone.ts"; -import { normalizeOptionalString } from "../lib/string-coerce.ts"; import { limitSidebarSessionRows, SIDEBAR_SESSION_NO_ATTENTION, diff --git a/ui/src/components/app-sidebar-session-navigation.ts b/ui/src/components/app-sidebar-session-navigation.ts index 61b17f0069c7..521cb002a828 100644 --- a/ui/src/components/app-sidebar-session-navigation.ts +++ b/ui/src/components/app-sidebar-session-navigation.ts @@ -1,3 +1,4 @@ +import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce"; import type { PropertyValues } from "lit"; import { state } from "lit/decorators.js"; import type { GatewaySessionRow, SessionsListResult } from "../api/types.ts"; @@ -20,7 +21,6 @@ import { parseAgentSessionKey, resolveUiDefaultAgentId, } from "../lib/sessions/session-key.ts"; -import { normalizeOptionalString } from "../lib/string-coerce.ts"; import { AppSidebarBase } from "./app-sidebar-base.ts"; import { adoptedCatalogSessionKeys } from "./app-sidebar-session-catalogs.ts"; import { diff --git a/ui/src/components/command-palette.ts b/ui/src/components/command-palette.ts index 715079dbb4bc..d40049c93b52 100644 --- a/ui/src/components/command-palette.ts +++ b/ui/src/components/command-palette.ts @@ -1,5 +1,9 @@ // Control UI component renders the command palette. import { consume } from "@lit/context"; +import { + normalizeLowercaseStringOrEmpty, + normalizeOptionalString, +} from "@openclaw/normalization-core/string-coerce"; import { html, nothing } from "lit"; import { property, state } from "lit/decorators.js"; import { ref } from "lit/directives/ref.js"; @@ -9,7 +13,6 @@ import { t } from "../i18n/index.ts"; import { formatRelativeTimestamp } from "../lib/format.ts"; import { resolveSessionDisplayName } from "../lib/session-display.ts"; import { getVisibleSessionRows } from "../lib/sessions/index.ts"; -import { normalizeLowercaseStringOrEmpty, normalizeOptionalString } from "../lib/string-coerce.ts"; import { OpenClawLightDomContentsElement } from "../lit/openclaw-element.ts"; import { SubscriptionsController } from "../lit/subscriptions-controller.ts"; import { isCommandPaletteShortcut } from "./command-palette-contract.ts"; diff --git a/ui/src/components/config-form.search.ts b/ui/src/components/config-form.search.ts index aada6039666a..3e36be14d68a 100644 --- a/ui/src/components/config-form.search.ts +++ b/ui/src/components/config-form.search.ts @@ -1,5 +1,5 @@ +import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce"; import type { ConfigUiHints } from "../api/types.ts"; -import { normalizeLowercaseStringOrEmpty } from "../lib/string-coerce.ts"; import { arrayItemSchema, arrayItemSchemaIndexes } from "./config-form.array-items.ts"; import { hintForPath, humanize, schemaType, type JsonSchema } from "./config-form.shared.ts"; diff --git a/ui/src/components/github-link-hovercard.ts b/ui/src/components/github-link-hovercard.ts index c375e6277835..f34cc0732b1d 100644 --- a/ui/src/components/github-link-hovercard.ts +++ b/ui/src/components/github-link-hovercard.ts @@ -1,6 +1,7 @@ import { initialState, Task } from "@lit/task"; import { asFiniteNumber } from "@openclaw/normalization-core/number-coercion"; import { isRecord } from "@openclaw/normalization-core/record-coerce"; +import { readNonBlankString } from "@openclaw/normalization-core/string-coerce"; import { ReactiveElement } from "lit"; import type { ControlUiGitHubPreview } from "../../../src/gateway/control-ui-contract.js"; import type { GatewayBrowserClient } from "../api/gateway.ts"; @@ -35,21 +36,13 @@ type CacheEntry = { let nextHovercardId = 0; function requiredString(record: Record, key: string): string { - const value = record[key]; - if (typeof value !== "string" || !value.trim()) { + const value = readNonBlankString(record[key]); + if (value === undefined) { throw new Error(`GitHub response omitted ${key}`); } return value; } -function readOptionalGitHubString( - record: Record, - key: string, -): string | undefined { - const value = record[key]; - return typeof value === "string" && value.trim() ? value : undefined; -} - function parseGitHubIssueOrPullRequestLink(href: string): GitHubLinkTarget | null { let url: URL; try { @@ -96,19 +89,19 @@ function parsePreviewResponse(target: GitHubLinkTarget, value: unknown): GitHubP additions: asFiniteNumber(value.additions), avatarDataUrl: safeAvatarDataUrl(value.avatarDataUrl), changedFiles: asFiniteNumber(value.changedFiles), - closedAt: readOptionalGitHubString(value, "closedAt"), + closedAt: readNonBlankString(value.closedAt), comments: asFiniteNumber(value.comments), createdAt: requiredString(value, "createdAt"), deletions: asFiniteNumber(value.deletions), draft: typeof value.draft === "boolean" ? value.draft : undefined, kind: target.kind, - login: readOptionalGitHubString(value, "login") ?? "ghost", - mergedAt: readOptionalGitHubString(value, "mergedAt"), + login: readNonBlankString(value.login) ?? "ghost", + mergedAt: readNonBlankString(value.mergedAt), number: target.number, owner: target.owner, repo: target.repo, state: requiredString(value, "state"), - stateReason: readOptionalGitHubString(value, "stateReason"), + stateReason: readNonBlankString(value.stateReason), title: requiredString(value, "title"), updatedAt: requiredString(value, "updatedAt"), }; diff --git a/ui/src/components/login-gate.ts b/ui/src/components/login-gate.ts index 42f88dbd527f..cf0a9d6231f4 100644 --- a/ui/src/components/login-gate.ts +++ b/ui/src/components/login-gate.ts @@ -1,3 +1,4 @@ +import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce"; // Control UI component renders the login gate. import { html, nothing } from "lit"; import { property } from "lit/decorators.js"; @@ -11,7 +12,6 @@ import { shouldShowInsecureContextHint, } from "../lib/connection-hints.ts"; import { buildExternalLinkRel, EXTERNAL_LINK_TARGET } from "../lib/external-link.ts"; -import { normalizeLowercaseStringOrEmpty } from "../lib/string-coerce.ts"; import { OpenClawLightDomContentsElement } from "../lit/openclaw-element.ts"; import { renderConnectCommand } from "./connect-command.ts"; import { icons } from "./icons.ts"; diff --git a/ui/src/components/markdown.ts b/ui/src/components/markdown.ts index 102395a31097..2a57f61f86f0 100644 --- a/ui/src/components/markdown.ts +++ b/ui/src/components/markdown.ts @@ -1,3 +1,4 @@ +import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce"; // Control UI module implements markdown behavior. import DOMPurify from "dompurify"; import { stripUnsupportedCitationControlMarkers } from "../../../src/shared/text/citation-control-markers.js"; @@ -5,7 +6,6 @@ import { routeIdFromPath } from "../app-route-paths.ts"; import { resolveControlUiBasePath } from "../app/browser.ts"; import { i18n, t } from "../i18n/index.ts"; import { truncateText } from "../lib/format.ts"; -import { normalizeLowercaseStringOrEmpty } from "../lib/string-coerce.ts"; import { renderAssistantTranscriptPlainTextFallback } from "./markdown-assistant-transcript.ts"; import { renderMarkdownCodeBlock } from "./markdown-code-blocks.ts"; import { isHostLocalMarkdownFileHref } from "./markdown-file-links.ts"; diff --git a/ui/src/components/session-organizer-operations.runtime.ts b/ui/src/components/session-organizer-operations.runtime.ts index e340105c64c9..9ebbdaf18f04 100644 --- a/ui/src/components/session-organizer-operations.runtime.ts +++ b/ui/src/components/session-organizer-operations.runtime.ts @@ -1,3 +1,4 @@ +import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce"; import { loadSettings, patchSettings } from "../app/settings.ts"; import { t } from "../i18n/index.ts"; import { readSessionMethodAccess } from "../lib/session-method-access.ts"; @@ -6,7 +7,6 @@ import { parseAgentSessionKey, resolveUiConfiguredMainKey, } from "../lib/sessions/session-key.ts"; -import { normalizeOptionalString } from "../lib/string-coerce.ts"; import { showToast } from "../lib/toast.ts"; import type { SidebarRecentSession, diff --git a/ui/src/components/settings-sidebar.ts b/ui/src/components/settings-sidebar.ts index 515913a44768..3137909e95ea 100644 --- a/ui/src/components/settings-sidebar.ts +++ b/ui/src/components/settings-sidebar.ts @@ -1,3 +1,4 @@ +import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce"; // Dedicated sidebar for the full-page settings takeover (see app-host.ts). import { html, nothing } from "lit"; import type { UpdateAvailable, UpdateScheduleState } from "../api/types.ts"; @@ -20,7 +21,6 @@ import type { UpdateProgress } from "../app/update-confirmation.ts"; import type { ApplicationStatusBanner } from "../app/update-overlay-helpers.ts"; import { t } from "../i18n/index.ts"; import { shouldHandleNavigationClick } from "../lib/navigation-click.ts"; -import { normalizeLowercaseStringOrEmpty } from "../lib/string-coerce.ts"; import { icons } from "./icons.ts"; import { redactLoginFailureError } from "./login-gate.ts"; import { renderOfflineSidebarStatus } from "./session-row-badges.ts"; diff --git a/ui/src/lib/agents/display.ts b/ui/src/lib/agents/display.ts index 2ba4e793cd4a..524eb9fa288d 100644 --- a/ui/src/lib/agents/display.ts +++ b/ui/src/lib/agents/display.ts @@ -1,5 +1,9 @@ // Control UI view renders agents utils screen content. import { formatByteSize } from "@openclaw/normalization-core"; +import { + normalizeLowercaseStringOrEmpty, + normalizeOptionalString, +} from "@openclaw/normalization-core/string-coerce"; import { html, nothing } from "lit"; import { expandToolGroups, @@ -18,7 +22,6 @@ import { t } from "../../i18n/index.ts"; import { resolveAgentAvatarUrl, resolveAssistantTextAvatar } from "../avatar.ts"; import { buildCatalogDisplayLookup, buildChatModelOptionFromLookup } from "../chat/model-ref.ts"; import { resolveAgentConfigEntryTarget } from "../config/config-state-model.ts"; -import { normalizeLowercaseStringOrEmpty, normalizeOptionalString } from "../string-coerce.ts"; type AgentRosterEntry = { id: string; diff --git a/ui/src/lib/agents/index.ts b/ui/src/lib/agents/index.ts index 4a5c90825bed..ff52cc5e9083 100644 --- a/ui/src/lib/agents/index.ts +++ b/ui/src/lib/agents/index.ts @@ -228,7 +228,7 @@ function emptyAgentFilesStatus(): AgentFilesStatus { return { list: null, loading: false, error: null }; } -function normalizeAgentId(agentId: string | null | undefined): string | null { +function readOptionalAgentId(agentId: string | null | undefined): string | null { const normalized = agentId?.trim(); return normalized ? normalized : null; } @@ -317,7 +317,7 @@ export function createAgentCapability(gateway: AgentGateway): AgentCapability { rawAgentId: string, force: boolean, ): Promise => { - const agentId = normalizeAgentId(rawAgentId); + const agentId = readOptionalAgentId(rawAgentId); const scope = lifecycle.capture(); if (!agentId || !scope) { return agentId ? (files.get(agentId)?.list ?? null) : null; @@ -402,7 +402,7 @@ export function createAgentCapability(gateway: AgentGateway): AgentCapability { ensureList: () => loadList(false), refreshList: () => loadList(true), files(agentId) { - const normalized = normalizeAgentId(agentId); + const normalized = readOptionalAgentId(agentId); return normalized ? (files.get(normalized) ?? emptyAgentFilesStatus()) : emptyAgentFilesStatus(); @@ -410,7 +410,7 @@ export function createAgentCapability(gateway: AgentGateway): AgentCapability { invalidateFiles(agentIds) { let changed = false; const normalizedIds = new Set( - agentIds.map(normalizeAgentId).filter((agentId): agentId is string => agentId !== null), + agentIds.map(readOptionalAgentId).filter((agentId): agentId is string => agentId !== null), ); for (const agentId of normalizedIds) { changed = files.delete(agentId) || changed; diff --git a/ui/src/lib/assistant-identity.ts b/ui/src/lib/assistant-identity.ts index 96fee5c81759..f3658f9ca10a 100644 --- a/ui/src/lib/assistant-identity.ts +++ b/ui/src/lib/assistant-identity.ts @@ -1,7 +1,7 @@ +import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce"; // Control UI module implements assistant identity behavior. import { truncateUtf16Safe } from "@openclaw/normalization-core/utf16-slice"; import { isRenderableAvatarImageDataUrl } from "../../../src/shared/avatar-limits.js"; -import { normalizeOptionalString } from "./string-coerce.ts"; // Short text/emoji avatars (e.g. "A", "PS", "🦞"). Anything longer that is not // a renderable image URL is dropped during normalization. diff --git a/ui/src/lib/avatar.ts b/ui/src/lib/avatar.ts index 68c675a05626..33e420bff3f5 100644 --- a/ui/src/lib/avatar.ts +++ b/ui/src/lib/avatar.ts @@ -1,9 +1,9 @@ +import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce"; import { isRenderableAvatarImageDataUrl } from "../../../src/shared/avatar-limits.js"; import type { AgentIdentityResult } from "../api/types.ts"; import { controlUiPublicAssetPath } from "../app/public-assets.ts"; import { DEFAULT_ASSISTANT_AVATAR } from "./assistant-identity.ts"; import { takeGraphemes } from "./graphemes.ts"; -import { normalizeOptionalString } from "./string-coerce.ts"; const CONTROL_UI_SAME_ORIGIN_AVATAR_URL_RE = /^\/(?!\/)/; const UNSAFE_ASSISTANT_TEXT_AVATAR_CHARS = /[\u200B-\u200F\u202A-\u202E\u2060-\u206F\uFEFF]/u; diff --git a/ui/src/lib/chat/commands.ts b/ui/src/lib/chat/commands.ts index 021e7eca99f0..3b24814ca95b 100644 --- a/ui/src/lib/chat/commands.ts +++ b/ui/src/lib/chat/commands.ts @@ -1,11 +1,11 @@ // Control UI chat domain owns pure slash command rules. import { asNullableRecord as asRecord } from "@openclaw/normalization-core/record-coerce"; +import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce"; import { truncateUtf16Safe } from "@openclaw/normalization-core/utf16-slice"; import type { CommandEntry } from "../../../../packages/gateway-protocol/src/index.js"; import { buildBuiltinChatCommands } from "../../../../src/auto-reply/commands-registry.shared.js"; import { t } from "../../i18n/index.ts"; -import { normalizeLowercaseStringOrEmpty } from "../string-coerce.ts"; export type SlashCommandCategory = "session" | "model" | "agents" | "tools"; diff --git a/ui/src/lib/chat/heartbeat-display.ts b/ui/src/lib/chat/heartbeat-display.ts index 217f21d069f1..f1f66b857982 100644 --- a/ui/src/lib/chat/heartbeat-display.ts +++ b/ui/src/lib/chat/heartbeat-display.ts @@ -1,6 +1,6 @@ +import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce"; // Control UI chat module implements heartbeat display behavior. import { escapeRegExp } from "../../../../src/shared/regexp.js"; -import { normalizeLowercaseStringOrEmpty } from "../string-coerce.ts"; const HEARTBEAT_TOKEN = "HEARTBEAT_OK"; const DEFAULT_HEARTBEAT_ACK_MAX_CHARS = 300; diff --git a/ui/src/lib/chat/message-extract.ts b/ui/src/lib/chat/message-extract.ts index 7a8ae3938c00..fd9f940f498e 100644 --- a/ui/src/lib/chat/message-extract.ts +++ b/ui/src/lib/chat/message-extract.ts @@ -1,10 +1,10 @@ +import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce"; // Control UI chat module implements message extract behavior. import { stripInternalRuntimeContext } from "../../../../src/agents/internal-runtime-context.js"; import { stripInboundMetadata } from "../../../../src/auto-reply/reply/strip-inbound-meta.js"; import { readPersistedMediaFacts } from "../../../../src/media/media-facts.js"; import { stripEnvelope } from "../../../../src/shared/chat-envelope.js"; import { extractAssistantPhaseText } from "../../../../src/shared/chat-message-content.js"; -import { normalizeLowercaseStringOrEmpty } from "../string-coerce.ts"; import { stripThinkingTags } from "../strip-thinking-tags.ts"; const textCache = new WeakMap(); diff --git a/ui/src/lib/chat/thinking.ts b/ui/src/lib/chat/thinking.ts index 964b1e79f77f..74c04d14ceee 100644 --- a/ui/src/lib/chat/thinking.ts +++ b/ui/src/lib/chat/thinking.ts @@ -1,3 +1,4 @@ +import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce"; import { BASE_THINKING_LEVELS, normalizeThinkLevel, @@ -12,7 +13,6 @@ import type { } from "../../api/types.ts"; import { pushUniqueTrimmedSelectOption } from "../select-options.ts"; import { sessionModelMatchesDefaults } from "../session-model-defaults.ts"; -import { normalizeLowercaseStringOrEmpty } from "../string-coerce.ts"; type ThinkingSessionDefaults = SessionsListResult["defaults"] | undefined; diff --git a/ui/src/lib/chat/tool-display.ts b/ui/src/lib/chat/tool-display.ts index 829112d099c2..24cd16340509 100644 --- a/ui/src/lib/chat/tool-display.ts +++ b/ui/src/lib/chat/tool-display.ts @@ -1,3 +1,4 @@ +import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce"; // Control UI module implements tool display behavior. import SHARED_TOOL_DISPLAY_JSON from "../../../../apps/shared/OpenClawKit/Sources/OpenClawKit/Resources/tool-display.json" with { type: "json" }; import { @@ -9,7 +10,6 @@ import { } from "../../../../src/agents/tool-display-common.js"; import type { ToolDetailMode } from "../../../../src/agents/tool-display-exec.js"; import type { ControlUiEmbedSandboxMode } from "../../../../src/gateway/control-ui-contract.js"; -import { normalizeLowercaseStringOrEmpty } from "../string-coerce.ts"; const A2UI_PATH = "/__openclaw__/a2ui"; const CANVAS_HOST_PATH = "/__openclaw__/canvas"; diff --git a/ui/src/lib/connection-hints.ts b/ui/src/lib/connection-hints.ts index 19e3749318a3..640c992f933d 100644 --- a/ui/src/lib/connection-hints.ts +++ b/ui/src/lib/connection-hints.ts @@ -1,9 +1,9 @@ +import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce"; // Connection-failure hint classification shared by the login gate. import { ConnectErrorDetailCodes, readConnectPairingRequiredMessage, } from "../../../packages/gateway-protocol/src/connect-error-details.js"; -import { normalizeLowercaseStringOrEmpty } from "../lib/string-coerce.ts"; const AUTH_REQUIRED_CODES = new Set([ ConnectErrorDetailCodes.AUTH_REQUIRED, diff --git a/ui/src/lib/cron/index.ts b/ui/src/lib/cron/index.ts index 35d4ad292659..657bec5402e8 100644 --- a/ui/src/lib/cron/index.ts +++ b/ui/src/lib/cron/index.ts @@ -1,4 +1,6 @@ import { isRecord } from "@openclaw/normalization-core/record-coerce"; +import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce"; +import { sortUniqueStrings } from "@openclaw/normalization-core/string-normalization"; import type { GatewayBrowserClient } from "../../api/gateway.ts"; import type { CronJob, @@ -24,7 +26,6 @@ import { formatMissingOperatorReadScopeMessage, isMissingOperatorReadScopeError, } from "../gateway-errors.ts"; -import { normalizeLowercaseStringOrEmpty, sortUniqueStrings } from "../string-coerce.ts"; import { parseCronEveryMs } from "./decimal.ts"; import { loadCronFailingCount } from "./scope.ts"; diff --git a/ui/src/lib/external-link.ts b/ui/src/lib/external-link.ts index e155b964f6b7..21c4a054ba3a 100644 --- a/ui/src/lib/external-link.ts +++ b/ui/src/lib/external-link.ts @@ -1,5 +1,5 @@ // Control UI module implements external link behavior. -import { normalizeOptionalLowercaseString } from "./string-coerce.ts"; +import { normalizeOptionalLowercaseString } from "@openclaw/normalization-core/string-coerce"; const REQUIRED_EXTERNAL_REL_TOKENS = ["noopener", "noreferrer"] as const; diff --git a/ui/src/lib/nodes/inventory.ts b/ui/src/lib/nodes/inventory.ts index 2a25d03a8f0d..48529094b7dd 100644 --- a/ui/src/lib/nodes/inventory.ts +++ b/ui/src/lib/nodes/inventory.ts @@ -1,11 +1,11 @@ import { asFiniteNumber as optionalNumber } from "@openclaw/normalization-core/number-coercion"; -import type { PresenceEntry } from "../../api/types.ts"; // Builds the unified node/device inventory shown on the Devices page. // The gateway exposes two overlapping views of the same machines: paired device // records (roles + tokens) and the node catalog (caps + live links). This module // joins them by id and groups duplicate pairings of the same client so the page // renders one row per machine instead of one row per historical keypair. -import { normalizeOptionalString } from "../string-coerce.ts"; +import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce"; +import type { PresenceEntry } from "../../api/types.ts"; import type { PairedDevice } from "./index.ts"; type NodeApprovalState = "approved" | "pending-approval" | "pending-reapproval" | "unapproved"; diff --git a/ui/src/lib/open-external-url.ts b/ui/src/lib/open-external-url.ts index 63dca8e9ef9f..5dfd22515300 100644 --- a/ui/src/lib/open-external-url.ts +++ b/ui/src/lib/open-external-url.ts @@ -1,5 +1,5 @@ // Control UI module implements open external url behavior. -import { normalizeLowercaseStringOrEmpty } from "./string-coerce.ts"; +import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce"; const DATA_URL_PREFIX = "data:"; const ALLOWED_EXTERNAL_PROTOCOLS = new Set(["http:", "https:", "blob:"]); diff --git a/ui/src/lib/select-options.ts b/ui/src/lib/select-options.ts index 3a0c216e5f0f..ed49d55f1acb 100644 --- a/ui/src/lib/select-options.ts +++ b/ui/src/lib/select-options.ts @@ -1,5 +1,5 @@ // Control UI module implements select options behavior. -import { normalizeLowercaseStringOrEmpty } from "./string-coerce.ts"; +import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce"; type SelectOption = { value: string; diff --git a/ui/src/lib/session-display.ts b/ui/src/lib/session-display.ts index f2fc5e1f4a8e..e344944016f6 100644 --- a/ui/src/lib/session-display.ts +++ b/ui/src/lib/session-display.ts @@ -1,7 +1,10 @@ +import { + normalizeLowercaseStringOrEmpty, + normalizeOptionalString, +} from "@openclaw/normalization-core/string-coerce"; // Control UI module implements session display behavior. import { sliceUtf16Safe } from "@openclaw/normalization-core/utf16-slice"; import { t } from "../i18n/index.ts"; -import { normalizeLowercaseStringOrEmpty, normalizeOptionalString } from "./string-coerce.ts"; const CHANNEL_LABELS: Record = { imessage: "iMessage", diff --git a/ui/src/lib/sessions/navigation.ts b/ui/src/lib/sessions/navigation.ts index 73f9c22e8bc3..2681ffc91de6 100644 --- a/ui/src/lib/sessions/navigation.ts +++ b/ui/src/lib/sessions/navigation.ts @@ -1,11 +1,11 @@ -import type { GatewayHelloOk } from "../../api/gateway.ts"; -import type { GatewaySessionRow, SessionsListResult } from "../../api/types.ts"; -import { isCronSessionKey } from "../session-display.ts"; import { normalizeLowercaseStringOrEmpty, normalizeOptionalLowercaseString, normalizeOptionalString, -} from "../string-coerce.ts"; +} from "@openclaw/normalization-core/string-coerce"; +import type { GatewayHelloOk } from "../../api/gateway.ts"; +import type { GatewaySessionRow, SessionsListResult } from "../../api/types.ts"; +import { isCronSessionKey } from "../session-display.ts"; import { parseCatalogSessionKey } from "./catalog-key.ts"; import { areUiSessionKeysEquivalent, diff --git a/ui/src/lib/sessions/route-navigation.ts b/ui/src/lib/sessions/route-navigation.ts index ad117cbd42c2..670f3d3604bb 100644 --- a/ui/src/lib/sessions/route-navigation.ts +++ b/ui/src/lib/sessions/route-navigation.ts @@ -1,9 +1,9 @@ +import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce"; import type { GatewaySessionRow } from "../../api/types.ts"; import { pathForRoute } from "../../app-route-paths.ts"; import { pathForSession } from "../../app-session-path-builder.ts"; import type { ApplicationNavigationOptions, ApplicationContext } from "../../app/context.ts"; import type { BoardFace } from "../board/settings.ts"; -import { normalizeOptionalString } from "../string-coerce.ts"; import { catalogSessionSearch, parseCatalogSessionKey } from "./catalog-key.ts"; import { areUiSessionKeysEquivalent, diff --git a/ui/src/lib/sessions/session-key.ts b/ui/src/lib/sessions/session-key.ts index 917ae116c008..130696e24e02 100644 --- a/ui/src/lib/sessions/session-key.ts +++ b/ui/src/lib/sessions/session-key.ts @@ -1,9 +1,10 @@ // Control UI module implements session key behavior. +import { normalizeAgentId } from "@openclaw/normalization-core/agent-id"; import { normalizeLowercaseStringOrEmpty, normalizeOptionalLowercaseString, normalizeOptionalString, -} from "../string-coerce.ts"; +} from "@openclaw/normalization-core/string-coerce"; type ParsedAgentSessionKey = { agentId: string; @@ -25,10 +26,7 @@ type UiSessionDefaults = { mainSessionKey?: string | null; }; -const VALID_ID_RE = /^[a-z0-9][a-z0-9_-]{0,63}$/i; -const INVALID_CHARS_RE = /[^a-z0-9_-]+/g; -const LEADING_DASH_RE = /^-+/; -const TRAILING_DASH_RE = /-+$/; +export { normalizeAgentId }; export function parseAgentSessionKey( sessionKey: string | undefined | null, @@ -326,23 +324,6 @@ export function uiSessionRowMatchesSelectedChat( ); } -export function normalizeAgentId(value: string | undefined | null): string { - const trimmed = normalizeOptionalString(value) ?? ""; - if (!trimmed) { - return DEFAULT_AGENT_ID; - } - if (VALID_ID_RE.test(trimmed)) { - return normalizeLowercaseStringOrEmpty(trimmed); - } - return ( - normalizeLowercaseStringOrEmpty(trimmed) - .replace(INVALID_CHARS_RE, "-") - .replace(LEADING_DASH_RE, "") - .replace(TRAILING_DASH_RE, "") - .slice(0, 64) || DEFAULT_AGENT_ID - ); -} - export function buildAgentMainSessionKey(params: { agentId: string; mainKey?: string | undefined; diff --git a/ui/src/lib/string-coerce.ts b/ui/src/lib/string-coerce.ts deleted file mode 100644 index 069f28a28f47..000000000000 --- a/ui/src/lib/string-coerce.ts +++ /dev/null @@ -1,11 +0,0 @@ -// Control UI module implements string coerce behavior. -export { - normalizeLowercaseStringOrEmpty, - normalizeOptionalLowercaseString, - normalizeOptionalString, -} from "@openclaw/normalization-core/string-coerce"; -export { - normalizeStringEntries, - sortUniqueStrings, - uniqueStrings, -} from "@openclaw/normalization-core/string-normalization"; diff --git a/ui/src/pages/activity/view.ts b/ui/src/pages/activity/view.ts index ed2e3bcc069e..263cd9ce1ad6 100644 --- a/ui/src/pages/activity/view.ts +++ b/ui/src/pages/activity/view.ts @@ -1,3 +1,5 @@ +import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce"; +import { sortUniqueStrings } from "@openclaw/normalization-core/string-normalization"; // Control UI view renders activity screen content. import { html, nothing } from "lit"; import { icons } from "../../components/icons.ts"; @@ -9,7 +11,6 @@ import { import { t } from "../../i18n/index.ts"; import { registerActivityEnglish } from "../../i18n/locales/en-activity.ts"; import { formatDurationCompact, formatTimeMs } from "../../lib/format.ts"; -import { normalizeLowercaseStringOrEmpty, sortUniqueStrings } from "../../lib/string-coerce.ts"; import "../../styles/activity.css"; import type { ActivityEntry, ActivityStatus } from "./tool-activity.ts"; diff --git a/ui/src/pages/agents/agents-page.ts b/ui/src/pages/agents/agents-page.ts index 097e2024627a..58148f346dc0 100644 --- a/ui/src/pages/agents/agents-page.ts +++ b/ui/src/pages/agents/agents-page.ts @@ -1,4 +1,5 @@ import { consume } from "@lit/context"; +import { normalizeStringEntries } from "@openclaw/normalization-core/string-normalization"; import { html, type PropertyValues } from "lit"; import { property, state } from "lit/decorators.js"; import type { GatewayBrowserClient } from "../../api/gateway.ts"; @@ -42,7 +43,6 @@ import { type GatewayMethodOperatorScope, } from "../../lib/gateway-methods.ts"; import { parseAgentSessionKey } from "../../lib/sessions/session-key.ts"; -import { normalizeStringEntries } from "../../lib/string-coerce.ts"; import { GatewayPageController } from "../../lit/gateway-page-controller.ts"; import { OpenClawLightDomElement } from "../../lit/openclaw-element.ts"; import { SubscriptionsController } from "../../lit/subscriptions-controller.ts"; diff --git a/ui/src/pages/agents/model-config.ts b/ui/src/pages/agents/model-config.ts index e22e674dafca..36a49a1bc53f 100644 --- a/ui/src/pages/agents/model-config.ts +++ b/ui/src/pages/agents/model-config.ts @@ -1,3 +1,4 @@ +import { normalizeStringEntries } from "@openclaw/normalization-core/string-normalization"; // Agent model selection staged against the runtime config form, split out of // agents-page.ts to keep that page inside the TS LOC ratchet. import type { ApplicationContext } from "../../app/context.ts"; @@ -10,7 +11,6 @@ import { currentConfigObject, type AgentConfigEntryTarget, } from "../../lib/config/config-state-model.ts"; -import { normalizeStringEntries } from "../../lib/string-coerce.ts"; type RuntimeConfig = ApplicationContext["runtimeConfig"]; diff --git a/ui/src/pages/agents/panels-tools-skills.ts b/ui/src/pages/agents/panels-tools-skills.ts index 5bf8ac207ad5..03688e287b44 100644 --- a/ui/src/pages/agents/panels-tools-skills.ts +++ b/ui/src/pages/agents/panels-tools-skills.ts @@ -1,3 +1,5 @@ +import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce"; +import { normalizeStringEntries } from "@openclaw/normalization-core/string-normalization"; // Control UI view renders agents panels tools skills screen content. import { html, nothing } from "lit"; import { normalizeToolPolicyName } from "../../../../src/agents/tool-policy-shared.js"; @@ -32,10 +34,6 @@ import { computeSkillReasons, renderSkillStatusChips, } from "../../lib/skills-shared.ts"; -import { - normalizeLowercaseStringOrEmpty, - normalizeStringEntries, -} from "../../lib/string-coerce.ts"; function renderToolMetaBadges(labels: string[]) { if (labels.length === 0) { diff --git a/ui/src/pages/chat/chat-command-executor.ts b/ui/src/pages/chat/chat-command-executor.ts index f1a439ae0c42..769d99183dfa 100644 --- a/ui/src/pages/chat/chat-command-executor.ts +++ b/ui/src/pages/chat/chat-command-executor.ts @@ -3,6 +3,10 @@ * Calls gateway RPC methods and returns formatted results. */ +import { + normalizeLowercaseStringOrEmpty, + normalizeOptionalLowercaseString, +} from "@openclaw/normalization-core/string-coerce"; import type { GatewayBrowserClient } from "../../api/gateway.ts"; import type { AgentsListResult, @@ -41,10 +45,6 @@ import { DEFAULT_MAIN_KEY, parseAgentSessionKey, } from "../../lib/sessions/session-key.ts"; -import { - normalizeLowercaseStringOrEmpty, - normalizeOptionalLowercaseString, -} from "../../lib/string-coerce.ts"; import { generateUUID } from "../../lib/uuid.ts"; import { patchChatCommandSessionSettings, selectedGlobalScope } from "./chat-settings-patches.ts"; diff --git a/ui/src/pages/chat/chat-gateway.ts b/ui/src/pages/chat/chat-gateway.ts index 2804ca394381..53b2492dd88a 100644 --- a/ui/src/pages/chat/chat-gateway.ts +++ b/ui/src/pages/chat/chat-gateway.ts @@ -2,11 +2,11 @@ import { hasSessionProjectionAcceptedFinal, reduceSessionProjectionRunEvent, } from "@openclaw/gateway-client/browser"; +import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce"; import { isAssistantHeartbeatAckForDisplay } from "../../lib/chat/heartbeat-display.ts"; import { extractText } from "../../lib/chat/message-extract.ts"; // Control UI page module reconciles Chat Gateway events into Chat state. import { isUiGlobalSessionKey, resolveUiDefaultAgentId } from "../../lib/sessions/session-key.ts"; -import { normalizeLowercaseStringOrEmpty } from "../../lib/string-coerce.ts"; import { chatScopedEventSessionMatches, isHiddenAssistantStreamText, diff --git a/ui/src/pages/chat/chat-history.ts b/ui/src/pages/chat/chat-history.ts index d73af9a44b24..7715266283ae 100644 --- a/ui/src/pages/chat/chat-history.ts +++ b/ui/src/pages/chat/chat-history.ts @@ -3,6 +3,7 @@ import { readSessionMessageIdentity, readSessionMessageSequence, } from "@openclaw/gateway-client/browser"; +import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce"; import type { CommandsListResult } from "../../../../packages/gateway-protocol/src/index.js"; import type { GatewayBrowserClient, GatewayHelloOk } from "../../api/gateway.ts"; import type { @@ -43,7 +44,6 @@ import { resolveUiSelectedGlobalAgentId, resolveUiSelectedSessionAgentId, } from "../../lib/sessions/session-key.ts"; -import { normalizeLowercaseStringOrEmpty } from "../../lib/string-coerce.ts"; import { replaceChatAttachmentsFromEditor } from "./attachment-payload-store.ts"; import type { ChatHistoryPagination } from "./chat-history-pagination.ts"; import { diff --git a/ui/src/pages/chat/chat-pane-header.ts b/ui/src/pages/chat/chat-pane-header.ts index c43f13cae835..8a9e0a8448e0 100644 --- a/ui/src/pages/chat/chat-pane-header.ts +++ b/ui/src/pages/chat/chat-pane-header.ts @@ -1,3 +1,4 @@ +import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce"; import { html, nothing } from "lit"; import type { SessionDiscussionInfo } from "../../../../packages/gateway-protocol/src/index.js"; import type { GatewaySessionRow } from "../../api/types.ts"; @@ -22,7 +23,6 @@ import { canDeleteSessionRows, resolveUiConfiguredMainKey, } from "../../lib/sessions/session-key.ts"; -import { normalizeOptionalString } from "../../lib/string-coerce.ts"; import { isActiveTask } from "../../lib/tasks/data.ts"; import { renderBoardViewSwitch } from "./board-session-surface.ts"; import { resolveChatPanePlacement } from "./chat-pane-placement.ts"; diff --git a/ui/src/pages/chat/chat-pane-session-menu.ts b/ui/src/pages/chat/chat-pane-session-menu.ts index 1027483bb47b..c84c85392c72 100644 --- a/ui/src/pages/chat/chat-pane-session-menu.ts +++ b/ui/src/pages/chat/chat-pane-session-menu.ts @@ -1,3 +1,4 @@ +import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce"; import type { SessionsFilesRevealResult, SystemInfoResult, @@ -16,7 +17,6 @@ import { openEditor } from "../../lib/editor-links.ts"; import { isGatewayMethodAdvertised } from "../../lib/gateway-methods.ts"; import { readSessionMethodAccess } from "../../lib/session-method-access.ts"; import { parseAgentSessionKey } from "../../lib/sessions/session-key.ts"; -import { normalizeOptionalString } from "../../lib/string-coerce.ts"; import { ChatPaneContext } from "./chat-pane-context.ts"; import { headerPlatformByClient } from "./chat-pane-shared.ts"; import { patchChatSessionLabel } from "./chat-state-route.ts"; diff --git a/ui/src/pages/chat/chat-session.ts b/ui/src/pages/chat/chat-session.ts index fc232d15e31d..dd917d266fa7 100644 --- a/ui/src/pages/chat/chat-session.ts +++ b/ui/src/pages/chat/chat-session.ts @@ -1,3 +1,4 @@ +import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce"; import { normalizeThinkLevel } from "../../../../src/auto-reply/thinking.shared.js"; import type { FastMode, GatewaySessionRow, SessionsListResult } from "../../api/types.ts"; import { resolveChatModelOverrideValue } from "../../lib/chat/model-select-state.ts"; @@ -20,7 +21,6 @@ import { resolveUiGlobalAliasAgentId, resolveUiSelectedGlobalAgentId, } from "../../lib/sessions/session-key.ts"; -import { normalizeOptionalString } from "../../lib/string-coerce.ts"; import type { ChatHistoryResult } from "./chat-history.ts"; import { getPendingChatPickerPatch, patchChatSessionSettings } from "./chat-settings-patches.ts"; export { getPendingChatPickerPatch }; diff --git a/ui/src/pages/chat/chat-settings-patches.ts b/ui/src/pages/chat/chat-settings-patches.ts index 766123c32cda..2e1622954514 100644 --- a/ui/src/pages/chat/chat-settings-patches.ts +++ b/ui/src/pages/chat/chat-settings-patches.ts @@ -1,3 +1,4 @@ +import { normalizeOptionalLowercaseString } from "@openclaw/normalization-core/string-coerce"; import type { SessionsPatchResult } from "../../api/types.ts"; import { resolveSessionKey, @@ -16,7 +17,6 @@ import { resolveUiDefaultAgentId, resolveUiSelectedGlobalAgentId, } from "../../lib/sessions/session-key.ts"; -import { normalizeOptionalLowercaseString } from "../../lib/string-coerce.ts"; type ChatPickerPatchHost = SessionScopeHost & { sessions: SessionCapability }; type ChatCommandSettingsContext = { diff --git a/ui/src/pages/chat/chat-thread-build.ts b/ui/src/pages/chat/chat-thread-build.ts index 076ba3ae541a..93642a99c1cc 100644 --- a/ui/src/pages/chat/chat-thread-build.ts +++ b/ui/src/pages/chat/chat-thread-build.ts @@ -1,4 +1,5 @@ import { asNullableRecord as asRecord } from "@openclaw/normalization-core/record-coerce"; +import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce"; import { isToolCallContentType, isToolResultContentType, @@ -20,7 +21,6 @@ import { import { extractTextCached } from "../../lib/chat/message-extract.ts"; import { normalizeRoleForGrouping } from "../../lib/chat/message-normalizer.ts"; import { areUiSessionKeysEquivalent } from "../../lib/sessions/session-key.ts"; -import { normalizeOptionalString } from "../../lib/string-coerce.ts"; import { buildCompactionDividerItem, buildResetDividerItem, diff --git a/ui/src/pages/chat/chat-thread-grouping.ts b/ui/src/pages/chat/chat-thread-grouping.ts index 165aa6d2356d..e5d56058ab76 100644 --- a/ui/src/pages/chat/chat-thread-grouping.ts +++ b/ui/src/pages/chat/chat-thread-grouping.ts @@ -1,4 +1,5 @@ import { asNullableRecord as asRecord } from "@openclaw/normalization-core/record-coerce"; +import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce"; import { isToolCallContentType, isToolResultContentType, @@ -8,7 +9,6 @@ import { extractTextCached } from "../../lib/chat/message-extract.ts"; import { normalizeMessage, normalizeRoleForGrouping } from "../../lib/chat/message-normalizer.ts"; import { senderIdentityKey } from "../../lib/chat/sender-label.ts"; import { extractToolCardsCached, isToolCardError } from "../../lib/chat/tool-cards.ts"; -import { normalizeLowercaseStringOrEmpty } from "../../lib/string-coerce.ts"; import { resolveMessageToolUseId, resolveToolBlockId } from "./chat-thread-items.ts"; import { assistantGroupIsForwardedBoundary, diff --git a/ui/src/pages/chat/chat-thread-items.ts b/ui/src/pages/chat/chat-thread-items.ts index ebe183111740..db38ce866212 100644 --- a/ui/src/pages/chat/chat-thread-items.ts +++ b/ui/src/pages/chat/chat-thread-items.ts @@ -1,6 +1,7 @@ import { readSessionMessageIdentity } from "@openclaw/gateway-client/browser"; import { asFiniteNumber } from "@openclaw/normalization-core/number-coercion"; import { asNullableRecord as asRecord } from "@openclaw/normalization-core/record-coerce"; +import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce"; import { resolveToolUseId } from "../../../../src/chat/tool-content.js"; import { escapeRegExp } from "../../../../src/shared/regexp.js"; import type { ChatItem, ChatQueueItem, ToolCard } from "../../lib/chat/chat-types.ts"; @@ -9,7 +10,6 @@ import { stripMessageDisplayMetadataText } from "../../lib/chat/message-normaliz import { normalizeRoleForGrouping } from "../../lib/chat/message-normalizer.ts"; import { extractToolCardsCached, extractToolPreview } from "../../lib/chat/tool-cards.ts"; import { fnv1aUtf16 } from "../../lib/fnv1a.ts"; -import { normalizeLowercaseStringOrEmpty } from "../../lib/string-coerce.ts"; import { chatItemStartsUserTurn, safeNormalizeMessage } from "./chat-turn-boundary.ts"; import { buildUserChatMessageContentBlocks } from "./user-message-content.ts"; diff --git a/ui/src/pages/chat/components/chat-background-tasks.ts b/ui/src/pages/chat/components/chat-background-tasks.ts index bb8ba5554a9a..60948aaf482a 100644 --- a/ui/src/pages/chat/components/chat-background-tasks.ts +++ b/ui/src/pages/chat/components/chat-background-tasks.ts @@ -1,9 +1,9 @@ +import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce"; import type { GatewayBrowserClient, GatewayHelloOk } from "../../../api/gateway.ts"; import { hasOperatorWriteAccess } from "../../../app/operator-access.ts"; import { t } from "../../../i18n/index.ts"; import type { SessionScopeHost } from "../../../lib/sessions/index.ts"; import { canonicalUiSessionKeyForPersistence } from "../../../lib/sessions/session-key.ts"; -import { normalizeOptionalString } from "../../../lib/string-coerce.ts"; import { applyTaskEvent, isActiveTask, diff --git a/ui/src/pages/chat/components/chat-session-workspace.ts b/ui/src/pages/chat/components/chat-session-workspace.ts index ed5952cc02a6..911f242fa066 100644 --- a/ui/src/pages/chat/components/chat-session-workspace.ts +++ b/ui/src/pages/chat/components/chat-session-workspace.ts @@ -1,3 +1,4 @@ +import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce"; import { html, nothing, type TemplateResult } from "lit"; import type { SessionsDiffResult } from "../../../../../packages/gateway-protocol/src/index.js"; import { @@ -18,11 +19,11 @@ import { type UiSettings, } from "../../../app/settings.ts"; import { icons } from "../../../components/icons.ts"; +import "../../../components/tooltip.ts"; import { BROWSER_PANEL_TOGGLE_EVENT, TERMINAL_PANEL_TOGGLE_EVENT, } from "../../../components/panel-toggle-contract.ts"; -import "../../../components/tooltip.ts"; import { t } from "../../../i18n/index.ts"; import { copyToClipboard } from "../../../lib/clipboard.ts"; import { formatByteSize } from "../../../lib/format.ts"; @@ -37,7 +38,6 @@ import { resolveAgentIdFromSessionKey, normalizeAgentId, } from "../../../lib/sessions/session-key.ts"; -import { normalizeOptionalString } from "../../../lib/string-coerce.ts"; import { hasUniformLineEndings, type SidebarContent } from "./chat-sidebar.ts"; export type SessionWorkspaceProps = { diff --git a/ui/src/pages/chat/connect-error.ts b/ui/src/pages/chat/connect-error.ts index 49a563a82c89..94a03b2d688e 100644 --- a/ui/src/pages/chat/connect-error.ts +++ b/ui/src/pages/chat/connect-error.ts @@ -1,3 +1,4 @@ +import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce"; // Control UI module implements connect error behavior. import { ConnectErrorDetailCodes, @@ -7,7 +8,6 @@ import { readPairingConnectErrorDetails, } from "../../../../packages/gateway-protocol/src/connect-error-details.js"; import { resolveGatewayErrorDetailCode } from "../../api/gateway.ts"; -import { normalizeLowercaseStringOrEmpty } from "../../lib/string-coerce.ts"; type ErrorWithMessageAndDetails = { message?: unknown; diff --git a/ui/src/pages/chat/run-lifecycle.ts b/ui/src/pages/chat/run-lifecycle.ts index 23533a22ffb0..f232705cb6cd 100644 --- a/ui/src/pages/chat/run-lifecycle.ts +++ b/ui/src/pages/chat/run-lifecycle.ts @@ -1,3 +1,4 @@ +import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce"; import type { GatewayBrowserClient } from "../../api/gateway.ts"; import type { GatewaySessionRow, SessionRunStatus, SessionsListResult } from "../../api/types.ts"; import { t } from "../../i18n/index.ts"; @@ -16,7 +17,6 @@ import { resolveUiGlobalAliasAgentId, uiSessionRowMatchesSelectedChat, } from "../../lib/sessions/session-key.ts"; -import { normalizeLowercaseStringOrEmpty } from "../../lib/string-coerce.ts"; import type { ChatRunStartupState } from "./chat-run-startup.ts"; import { readChatSessionActionAccess } from "./chat-session-action-access.ts"; import { formatConnectError } from "./connect-error.ts"; diff --git a/ui/src/pages/chat/stream-reconciliation.ts b/ui/src/pages/chat/stream-reconciliation.ts index 7985a418da83..4f0a88100c19 100644 --- a/ui/src/pages/chat/stream-reconciliation.ts +++ b/ui/src/pages/chat/stream-reconciliation.ts @@ -1,15 +1,15 @@ // Control UI chat module implements stream reconciliation behavior. import { asFiniteNumber } from "@openclaw/normalization-core/number-coercion"; +import { + normalizeLowercaseStringOrEmpty, + normalizeOptionalString, +} from "@openclaw/normalization-core/string-coerce"; import { streamSegmentHasItemId, streamSegmentUsesAccumulatedText, trimAccumulatedStreamPrefix, } from "../../lib/chat/chat-types.ts"; import { extractText } from "../../lib/chat/message-extract.ts"; -import { - normalizeLowercaseStringOrEmpty, - normalizeOptionalString, -} from "../../lib/string-coerce.ts"; import { extractToolMessageRefs, resolveLiveToolStreamRefs, diff --git a/ui/src/pages/chat/tool-stream-identity.ts b/ui/src/pages/chat/tool-stream-identity.ts index 243e06f58953..406fbb49de03 100644 --- a/ui/src/pages/chat/tool-stream-identity.ts +++ b/ui/src/pages/chat/tool-stream-identity.ts @@ -1,11 +1,11 @@ import { asNullableRecord as asToolRecord } from "@openclaw/normalization-core/record-coerce"; +import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce"; import { isToolCallContentType, isToolResultContentType, resolveToolUseId, } from "../../../../src/chat/tool-content.js"; import { normalizeRoleForGrouping } from "../../lib/chat/message-normalizer.ts"; -import { normalizeOptionalString } from "../../lib/string-coerce.ts"; type ToolMessageRef = { id: string; diff --git a/ui/src/pages/chat/tool-stream.ts b/ui/src/pages/chat/tool-stream.ts index a70d10e0eda2..adec399846e9 100644 --- a/ui/src/pages/chat/tool-stream.ts +++ b/ui/src/pages/chat/tool-stream.ts @@ -1,6 +1,7 @@ // Control UI module implements app tool stream behavior. import { asNullableObjectRecord as readRecord } from "@openclaw/normalization-core/record-coerce"; import { normalizeNullableString as toTrimmedString } from "@openclaw/normalization-core/string-coerce"; +import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce"; import { stripInlineDirectiveTagsForDelivery } from "../../../../src/utils/directive-tags.js"; import type { ExecApprovalRequest } from "../../app/exec-approval.ts"; import type { ChatQueueItem, ChatStreamSegment } from "../../lib/chat/chat-types.ts"; @@ -8,7 +9,6 @@ import type { DiffStat } from "../../lib/chat/tool-call-diff.ts"; import { formatUnknownText, truncateText } from "../../lib/format.ts"; import type { SessionCapability } from "../../lib/sessions/index.ts"; import { uiSessionEventMatches } from "../../lib/sessions/session-key.ts"; -import { normalizeLowercaseStringOrEmpty } from "../../lib/string-coerce.ts"; import type { ChatRunStartupState } from "./chat-run-startup.ts"; import { buildToolStreamIdentity } from "./tool-stream-identity.ts"; diff --git a/ui/src/pages/cron/form-suggestions.ts b/ui/src/pages/cron/form-suggestions.ts index 29ebc594b489..a67ffd5a6972 100644 --- a/ui/src/pages/cron/form-suggestions.ts +++ b/ui/src/pages/cron/form-suggestions.ts @@ -1,3 +1,4 @@ +import { sortUniqueStrings } from "@openclaw/normalization-core/string-normalization"; import type { AgentsListResult } from "../../api/types.ts"; import type { ApplicationContext } from "../../app/context.ts"; import { listSelectableAgents } from "../../lib/agents/display.ts"; @@ -7,7 +8,6 @@ import { resolveConfiguredCronModelSuggestions, type CronState, } from "../../lib/cron/index.ts"; -import { sortUniqueStrings } from "../../lib/string-coerce.ts"; import { resolveCronTimezoneSuggestions } from "./timezone-suggestions.ts"; export const THINKING_SUGGESTIONS = ["off", "minimal", "low", "medium", "high"]; diff --git a/ui/src/pages/cron/timezone-suggestions.ts b/ui/src/pages/cron/timezone-suggestions.ts index 58bf8052496d..dc33bc42410c 100644 --- a/ui/src/pages/cron/timezone-suggestions.ts +++ b/ui/src/pages/cron/timezone-suggestions.ts @@ -1,5 +1,5 @@ +import { sortUniqueStrings } from "@openclaw/normalization-core/string-normalization"; import type { CronJob } from "../../api/types.ts"; -import { sortUniqueStrings } from "../../lib/string-coerce.ts"; function resolveBrowserTimezone(): string { try { diff --git a/ui/src/pages/cron/view.ts b/ui/src/pages/cron/view.ts index b9ae6cf45001..e38f2aa55841 100644 --- a/ui/src/pages/cron/view.ts +++ b/ui/src/pages/cron/view.ts @@ -1,12 +1,16 @@ +import { + normalizeStringEntries, + uniqueStrings, +} from "@openclaw/normalization-core/string-normalization"; // Control UI view renders the Automations (cron) screen: a full-width list (stats, task table, // starter ideas) and a full-page detail view for creating or editing a single automation. import { html, nothing } from "lit"; import { ifDefined } from "lit/directives/if-defined.js"; import { repeat } from "lit/directives/repeat.js"; import { unsafeHTML } from "lit/directives/unsafe-html.js"; -import type { ChannelUiMetaEntry, CronJob, CronRunLogEntry, CronStatus } from "../../api/types.ts"; import "../../styles/chat/text.css"; import "../../styles/cron.css"; +import type { ChannelUiMetaEntry, CronJob, CronRunLogEntry, CronStatus } from "../../api/types.ts"; import type { CronDeliveryStatus, CronJobsEnabledFilter, @@ -17,6 +21,9 @@ import type { import { renderCronJobsPagination } from "../../components/cron-jobs-pagination.ts"; import { icon, icons } from "../../components/icons.ts"; import { highlightCodeHtml } from "../../components/markdown-code-blocks.ts"; +import "../../components/tooltip.ts"; +import "../../components/web-awesome.ts"; +import "../../components/web-awesome-popover.ts"; import { renderSettingsPage, renderSettingsRow, @@ -24,9 +31,6 @@ import { renderSettingsToggle, renderSettingsToggleRow, } from "../../components/settings-ui.ts"; -import "../../components/tooltip.ts"; -import "../../components/web-awesome.ts"; -import "../../components/web-awesome-popover.ts"; import { t } from "../../i18n/index.ts"; import { isCronJobActiveFailure, resolveCronJobLastRunStatus } from "../../lib/cron-status.ts"; import { parseCronEveryMs } from "../../lib/cron/decimal.ts"; @@ -39,7 +43,6 @@ import type { } from "../../lib/cron/index.ts"; import { formatRelativeTimestamp, formatMs } from "../../lib/format.ts"; import { formatCronSchedule } from "../../lib/presenter.ts"; -import { normalizeStringEntries, uniqueStrings } from "../../lib/string-coerce.ts"; import { renderSegmented } from "./segmented-control.ts"; import { renderCronStats } from "./stats.ts"; import { CRON_SUGGESTIONS, suggestionFormPatch } from "./suggestions.ts"; diff --git a/ui/src/pages/devices/view-inventory.ts b/ui/src/pages/devices/view-inventory.ts index 5284b4b3fda1..0c568ea99912 100644 --- a/ui/src/pages/devices/view-inventory.ts +++ b/ui/src/pages/devices/view-inventory.ts @@ -1,3 +1,4 @@ +import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce"; // Devices page renders the unified paired-device / node inventory sections. import { html, nothing, type TemplateResult } from "lit"; import type { PresenceEntry } from "../../api/types.ts"; @@ -20,7 +21,6 @@ import { type DeviceInventoryGroup, } from "../../lib/nodes/inventory.ts"; import { prettifyPlatform } from "../../lib/platform-label.ts"; -import { normalizeOptionalString } from "../../lib/string-coerce.ts"; import { renderPendingDeviceRows } from "./view-pending-devices.ts"; import { deviceIcon, renderDeviceTile } from "./view-shared.ts"; import type { DevicesProps } from "./view.types.ts"; diff --git a/ui/src/pages/devices/view-pending-devices.ts b/ui/src/pages/devices/view-pending-devices.ts index 4ee776fbed56..760850b8eec1 100644 --- a/ui/src/pages/devices/view-pending-devices.ts +++ b/ui/src/pages/devices/view-pending-devices.ts @@ -1,3 +1,4 @@ +import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce"; // Devices page renders the pending device pairing-request rows. import { html, nothing } from "lit"; import { @@ -9,7 +10,6 @@ import { icons } from "../../components/icons.ts"; import { t } from "../../i18n/index.ts"; import { formatList, formatRelativeTimestamp } from "../../lib/format.ts"; import type { PairedDevice, PendingDevice } from "../../lib/nodes/index.ts"; -import { normalizeOptionalString } from "../../lib/string-coerce.ts"; import { renderDeviceTile } from "./view-shared.ts"; import type { DevicesProps } from "./view.types.ts"; diff --git a/ui/src/pages/devices/view-shared.ts b/ui/src/pages/devices/view-shared.ts index 175cc1ea1b40..9ad92696096a 100644 --- a/ui/src/pages/devices/view-shared.ts +++ b/ui/src/pages/devices/view-shared.ts @@ -1,12 +1,12 @@ // Devices page owns these pure view helpers. import { isRecord } from "@openclaw/normalization-core/record-coerce"; +import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce"; import { html, type TemplateResult } from "lit"; import { GATEWAY_CLIENT_IDS, GATEWAY_CLIENT_MODES, } from "../../../../packages/gateway-protocol/src/client-info.js"; import { icons } from "../../components/icons.ts"; -import { normalizeOptionalString } from "../../lib/string-coerce.ts"; export type NodeTargetOption = { id: string; diff --git a/ui/src/pages/logs/log-lines.ts b/ui/src/pages/logs/log-lines.ts index 7391c896bde5..3b0909f9faa3 100644 --- a/ui/src/pages/logs/log-lines.ts +++ b/ui/src/pages/logs/log-lines.ts @@ -1,6 +1,6 @@ +import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce"; import { stripAnsi } from "../../../../packages/terminal-core/src/ansi.js"; import { parseLogLine as parseCoreLogLine } from "../../../../src/logging/parse-log-line.js"; -import { normalizeLowercaseStringOrEmpty } from "../../lib/string-coerce.ts"; export type LogLevel = "trace" | "debug" | "info" | "warn" | "error" | "fatal"; diff --git a/ui/src/pages/logs/view.ts b/ui/src/pages/logs/view.ts index 85401ba62ce5..777429a245a4 100644 --- a/ui/src/pages/logs/view.ts +++ b/ui/src/pages/logs/view.ts @@ -1,3 +1,4 @@ +import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce"; // Control UI view renders logs screen content. import { html, nothing } from "lit"; import { @@ -12,7 +13,6 @@ import { } from "../../components/settings-ui.ts"; import { t } from "../../i18n/index.ts"; import { formatTimeMs } from "../../lib/format.ts"; -import { normalizeLowercaseStringOrEmpty } from "../../lib/string-coerce.ts"; import type { LogEntry, LogLevel } from "./log-lines.ts"; const LEVELS: LogLevel[] = ["trace", "debug", "info", "warn", "error", "fatal"]; diff --git a/ui/src/pages/new-session/create-params.ts b/ui/src/pages/new-session/create-params.ts index 0165718ab7ee..3cf5d3c57456 100644 --- a/ui/src/pages/new-session/create-params.ts +++ b/ui/src/pages/new-session/create-params.ts @@ -1,5 +1,5 @@ +import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce"; import { normalizeAgentId } from "../../lib/sessions/session-key.ts"; -import { normalizeOptionalString } from "../../lib/string-coerce.ts"; const WORKTREE_NAME_PATTERN = /^[a-z0-9][a-z0-9-]{0,63}$/; diff --git a/ui/src/pages/new-session/discovery.ts b/ui/src/pages/new-session/discovery.ts index d2cd5255044a..03e1024eba20 100644 --- a/ui/src/pages/new-session/discovery.ts +++ b/ui/src/pages/new-session/discovery.ts @@ -1,5 +1,5 @@ import { isRecord } from "@openclaw/normalization-core/record-coerce"; -import { normalizeOptionalString } from "../../lib/string-coerce.ts"; +import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce"; export type DraftBranches = { repoRoot: string; diff --git a/ui/src/pages/new-session/draft-place-state.ts b/ui/src/pages/new-session/draft-place-state.ts index 28b0ea48379d..57166f7b1188 100644 --- a/ui/src/pages/new-session/draft-place-state.ts +++ b/ui/src/pages/new-session/draft-place-state.ts @@ -1,3 +1,4 @@ +import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce"; import type { FsListDirResult, WorktreesBranchesResult, @@ -7,7 +8,6 @@ import { hasOperatorAdminAccess, hasOperatorWriteAccess } from "../../app/operat import { t } from "../../i18n/index.ts"; import { listSelectableAgents } from "../../lib/agents/display.ts"; import { normalizeAgentId } from "../../lib/sessions/session-key.ts"; -import { normalizeOptionalString } from "../../lib/string-coerce.ts"; import * as catalog from "./catalog-target.ts"; import type { DraftNode, DraftRepositoryState } from "./discovery.ts"; import { readDraftNodes } from "./discovery.ts"; diff --git a/ui/src/pages/new-session/gateway-name-discovery.ts b/ui/src/pages/new-session/gateway-name-discovery.ts index 79f8549b47d5..b10cb1bc229b 100644 --- a/ui/src/pages/new-session/gateway-name-discovery.ts +++ b/ui/src/pages/new-session/gateway-name-discovery.ts @@ -1,6 +1,6 @@ +import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce"; import type { SystemInfoResult } from "../../../../packages/gateway-protocol/src/index.js"; import type { GatewayBrowserClient } from "../../api/gateway.ts"; -import { normalizeOptionalString } from "../../lib/string-coerce.ts"; export async function discoverGatewayName( client: GatewayBrowserClient | null, diff --git a/ui/src/pages/new-session/preferences.ts b/ui/src/pages/new-session/preferences.ts index b7cb482423c5..43629371c4aa 100644 --- a/ui/src/pages/new-session/preferences.ts +++ b/ui/src/pages/new-session/preferences.ts @@ -1,7 +1,7 @@ import { gatewayOriginScope } from "@openclaw/gateway-client/browser"; import { isRecord } from "@openclaw/normalization-core/record-coerce"; +import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce"; import { normalizeAgentId } from "../../lib/sessions/session-key.ts"; -import { normalizeOptionalString } from "../../lib/string-coerce.ts"; import { getSafeLocalStorage } from "../../local-storage.ts"; const STORAGE_KEY_PREFIX = "openclaw.new-session.preferences.v1:"; diff --git a/ui/src/pages/new-session/recent-places.ts b/ui/src/pages/new-session/recent-places.ts index 3ccb1843b284..5d6b77c3a896 100644 --- a/ui/src/pages/new-session/recent-places.ts +++ b/ui/src/pages/new-session/recent-places.ts @@ -1,4 +1,4 @@ -import { normalizeOptionalString } from "../../lib/string-coerce.ts"; +import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce"; import type { DraftNode } from "./discovery.ts"; type RecentPlaceSource = { diff --git a/ui/src/pages/sessions/sessions-page.ts b/ui/src/pages/sessions/sessions-page.ts index 5354f74f9d0a..70df145d9940 100644 --- a/ui/src/pages/sessions/sessions-page.ts +++ b/ui/src/pages/sessions/sessions-page.ts @@ -1,5 +1,6 @@ import { consume } from "@lit/context"; import { initialState, Task, TaskStatus } from "@lit/task"; +import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce"; import { html, nothing, type PropertyValues } from "lit"; import { property, state } from "lit/decorators.js"; import type { GatewayBrowserClient } from "../../api/gateway.ts"; @@ -20,8 +21,8 @@ import { import { showConfirmDialog } from "../../components/confirm-dialog.ts"; import { sessionMenuReasons } from "../../components/session-menu-access.ts"; import { fetchSessionMenuWork } from "../../components/session-menu-work.ts"; -import type { SessionMenuAction, SessionMenuWork } from "../../components/session-menu.ts"; import "../../components/session-menu.ts"; +import type { SessionMenuAction, SessionMenuWork } from "../../components/session-menu.ts"; import { renderSessionsHubHeader } from "../../components/sessions-hub-header.ts"; import { renderDocsLink } from "../../components/settings-ui.ts"; import { renderSettingsWorkspace } from "../../components/settings-workspace.ts"; @@ -58,7 +59,6 @@ import { parseAgentSessionKey, resolveUiConfiguredMainKey, } from "../../lib/sessions/session-key.ts"; -import { normalizeOptionalString } from "../../lib/string-coerce.ts"; import { showToast } from "../../lib/toast.ts"; import { isActiveWorkboardCard } from "../../lib/workboard/card-state.ts"; import { captureSessionToWorkboard } from "../../lib/workboard/index.ts"; diff --git a/ui/src/pages/sessions/view.ts b/ui/src/pages/sessions/view.ts index 2327f534ceaf..39bc99d2353a 100644 --- a/ui/src/pages/sessions/view.ts +++ b/ui/src/pages/sessions/view.ts @@ -1,6 +1,11 @@ +import { + normalizeLowercaseStringOrEmpty, + normalizeOptionalString, +} from "@openclaw/normalization-core/string-coerce"; // Control UI view renders sessions screen content. import { html, nothing } from "lit"; import type { SessionsSearchHit } from "../../../../packages/gateway-protocol/src/index.js"; +import "../../styles/sessions.css"; import type { AgentIdentityResult, GatewaySessionRow, @@ -10,15 +15,14 @@ import type { SessionCompactionCheckpoint, SessionsListResult, } from "../../api/types.ts"; -import "../../styles/sessions.css"; import { icons } from "../../components/icons.ts"; +import "../../components/tooltip.ts"; import { renderSettingsPage, renderSettingsSegmented, renderSettingsSection, renderSettingsStatus, } from "../../components/settings-ui.ts"; -import "../../components/tooltip.ts"; import { t } from "../../i18n/index.ts"; import { resolveAgentRuntimeLabel } from "../../lib/agents/display.ts"; import { @@ -53,10 +57,6 @@ import { sessionNavigationTarget, } from "../../lib/sessions/route-navigation.ts"; import { parseSessionKeyParts } from "../../lib/sessions/session-key.ts"; -import { - normalizeLowercaseStringOrEmpty, - normalizeOptionalString, -} from "../../lib/string-coerce.ts"; import { parseFilterInteger } from "./page-state.ts"; export type TranscriptSearchState = diff --git a/ui/src/pages/skills/view.ts b/ui/src/pages/skills/view.ts index ad74bd2046b4..19cb29371eb2 100644 --- a/ui/src/pages/skills/view.ts +++ b/ui/src/pages/skills/view.ts @@ -1,3 +1,4 @@ +import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce"; // Control UI page renders skills screen content. The list surfaces follow the // settings design language (ui/docs/design-system/settings-design.md): section // headings outside one group surface, rows with a control cluster, dot+text @@ -6,12 +7,12 @@ import { html, nothing, type TemplateResult } from "lit"; import { repeat } from "lit/directives/repeat.js"; import { unsafeHTML } from "lit/directives/unsafe-html.js"; -import type { AgentsListResult, SkillStatusEntry, SkillStatusReport } from "../../api/types.ts"; import "../../components/agent-select-registration.ts"; +import type { AgentsListResult, SkillStatusEntry, SkillStatusReport } from "../../api/types.ts"; import { renderHubTabs } from "../../components/hub-tabs.ts"; import { icons } from "../../components/icons.ts"; -import { toSanitizedMarkdownHtml } from "../../components/markdown.ts"; import "../../components/modal-dialog.ts"; +import { toSanitizedMarkdownHtml } from "../../components/markdown.ts"; import { renderSettingsEmpty, renderSettingsPage, @@ -25,9 +26,9 @@ import { t } from "../../i18n/index.ts"; import { listSelectableAgents, normalizeAgentLabel } from "../../lib/agents/display.ts"; import { clampText } from "../../lib/format.ts"; import { resolveSafeExternalUrl } from "../../lib/open-external-url.ts"; -import { groupSkills, type SkillGroup } from "../../lib/skills-grouping.ts"; import "../../styles/plugins.css"; import "../../styles/sidebar-markdown.css"; +import { groupSkills, type SkillGroup } from "../../lib/skills-grouping.ts"; import { computeSkillMissing, computeSkillReasons, @@ -42,7 +43,6 @@ import { type SkillOperation, type SkillMessageMap, } from "../../lib/skills/index.ts"; -import { normalizeLowercaseStringOrEmpty } from "../../lib/string-coerce.ts"; function safeExternalHref(raw?: string): string | null { if (!raw) { diff --git a/ui/src/pages/usage/helpers.ts b/ui/src/pages/usage/helpers.ts index fad8e38f0dc9..09572a237fd9 100644 --- a/ui/src/pages/usage/helpers.ts +++ b/ui/src/pages/usage/helpers.ts @@ -1,5 +1,5 @@ // Control UI module implements usage helpers behavior. -import { normalizeLowercaseStringOrEmpty } from "../../lib/string-coerce.ts"; +import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce"; type UsageQueryTerm = { key?: string; diff --git a/ui/src/pages/usage/metrics.ts b/ui/src/pages/usage/metrics.ts index 426cb81081b8..2903e089f64c 100644 --- a/ui/src/pages/usage/metrics.ts +++ b/ui/src/pages/usage/metrics.ts @@ -1,3 +1,4 @@ +import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce"; // Control UI view renders usage metrics screen content. import { html } from "lit"; import { @@ -8,7 +9,6 @@ import { import { renderSettingsSection } from "../../components/settings-ui.ts"; import { t } from "../../i18n/index.ts"; import { formatCompactTokenCount } from "../../lib/format.ts"; -import { normalizeLowercaseStringOrEmpty } from "../../lib/string-coerce.ts"; import type { UsageSessionEntry, UsageTotals, UsageAggregates } from "./types.ts"; const CHARS_PER_TOKEN = 4; diff --git a/ui/src/pages/usage/query.ts b/ui/src/pages/usage/query.ts index e5f5cdac55a0..32fce9ebf4bd 100644 --- a/ui/src/pages/usage/query.ts +++ b/ui/src/pages/usage/query.ts @@ -1,6 +1,7 @@ // Control UI view renders usage query screen content. import { timestampMsToIsoString } from "@openclaw/normalization-core/number-coercion"; -import { normalizeLowercaseStringOrEmpty, uniqueStrings } from "../../lib/string-coerce.ts"; +import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce"; +import { uniqueStrings } from "@openclaw/normalization-core/string-normalization"; import { extractQueryTerms } from "./helpers.ts"; import type { CostDailyEntry, UsageAggregates, UsageSessionEntry } from "./types.ts"; diff --git a/ui/src/pages/usage/usage-page.ts b/ui/src/pages/usage/usage-page.ts index 5cdaf7e4a284..d4a85d402376 100644 --- a/ui/src/pages/usage/usage-page.ts +++ b/ui/src/pages/usage/usage-page.ts @@ -1,5 +1,6 @@ import { consume } from "@lit/context"; import { initialState, Task, TaskStatus } from "@lit/task"; +import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce"; import type { PropertyValues } from "lit"; import { property, state } from "lit/decorators.js"; import type { GatewayBrowserClient } from "../../api/gateway.ts"; @@ -28,7 +29,6 @@ import { requestSessionUsageLogs, requestSessionUsageTimeSeries, } from "../../lib/sessions/index.ts"; -import { normalizeLowercaseStringOrEmpty } from "../../lib/string-coerce.ts"; import { GatewayPageController, type GatewayPageChange, diff --git a/ui/src/pages/usage/view-details.ts b/ui/src/pages/usage/view-details.ts index 203b7a23b089..30c58f968216 100644 --- a/ui/src/pages/usage/view-details.ts +++ b/ui/src/pages/usage/view-details.ts @@ -1,4 +1,5 @@ import { expectDefined } from "@openclaw/normalization-core"; +import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce"; import { truncateUtf16Safe } from "@openclaw/normalization-core/utf16-slice"; // Control UI view renders usage render details screen content. import { html, svg, nothing } from "lit"; @@ -7,10 +8,9 @@ import { type PanelRefreshStatus, } from "../../components/panel-refresh-status.ts"; import { t } from "../../i18n/index.ts"; -import { formatDurationCompact } from "../../lib/format.ts"; import "../../components/tooltip.ts"; +import { formatDurationCompact } from "../../lib/format.ts"; import { formatDateTimeMs, formatMs, formatTimeMs } from "../../lib/format.ts"; -import { normalizeLowercaseStringOrEmpty } from "../../lib/string-coerce.ts"; import { parseToolSummary } from "./helpers.ts"; import { charsToTokens, formatUsageCost, formatUsageTokens } from "./metrics.ts"; import type { diff --git a/ui/src/pages/usage/view-overview.ts b/ui/src/pages/usage/view-overview.ts index bcdc62f74dbc..d7f32b41e3fb 100644 --- a/ui/src/pages/usage/view-overview.ts +++ b/ui/src/pages/usage/view-overview.ts @@ -1,14 +1,14 @@ import { expectDefined } from "@openclaw/normalization-core"; +import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce"; import { truncateUtf16Safe } from "@openclaw/normalization-core/utf16-slice"; // Control UI view renders usage render overview screen content. import { html, nothing } from "lit"; import { ifDefined } from "lit/directives/if-defined.js"; import { renderSettingsSection } from "../../components/settings-ui.ts"; import { t } from "../../i18n/index.ts"; -import { copyToClipboard } from "../../lib/clipboard.ts"; import "../../components/tooltip.ts"; +import { copyToClipboard } from "../../lib/clipboard.ts"; import { formatDurationCompact } from "../../lib/format.ts"; -import { normalizeLowercaseStringOrEmpty } from "../../lib/string-coerce.ts"; import { buildUsageCostWindows, buildUsageCostWindowSummary,