refactor: consolidate coercion ownership (#122692)

* refactor: consolidate coercion ownership

* test: align shard check with weighted planning

* chore: refresh plugin SDK API baseline
This commit is contained in:
Peter Steinberger
2026-08-12 09:25:28 -07:00
committed by GitHub
parent 7c58151445
commit c23d66e3b5
481 changed files with 1611 additions and 2632 deletions
+1
View File
@@ -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!",
@@ -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"}
+1 -1
View File
@@ -1 +1 @@
{"contentHash":"dbaf8a557d0aba2a387280f134f76b5dfc156aaaa27abef5ff2dbfd21d96830b","entrypoint":"agent-harness","importSpecifier":"openclaw/plugin-sdk/agent-harness"}
{"contentHash":"9d3b87f5ecf49e5d95ac0359ae35bd029238b11e34a88d452c05d50ffc7a60a4","entrypoint":"agent-harness","importSpecifier":"openclaw/plugin-sdk/agent-harness"}
+1 -1
View File
@@ -1 +1 @@
{"contentHash":"aa63172e6cc1bb34ba6fcc47c0c6cb20ab9c6efb061939b23351a54ec563c933","entrypoint":"channel-core","importSpecifier":"openclaw/plugin-sdk/channel-core"}
{"contentHash":"fa576f6d45ac6077714529a5d8d496800191ed5e28015e4744dada75c8a22e2c","entrypoint":"channel-core","importSpecifier":"openclaw/plugin-sdk/channel-core"}
@@ -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"}
@@ -1 +1 @@
{"contentHash":"345577990d31114e6e94bb9ee4b4c1d382446c713b7d5743b80688fd906ae36d","entrypoint":"channel-message","importSpecifier":"openclaw/plugin-sdk/channel-message"}
{"contentHash":"08b9e89e4603dde1b13ce98bc534f5578fc66f8e9d7236f799954a7baaca39cb","entrypoint":"channel-message","importSpecifier":"openclaw/plugin-sdk/channel-message"}
@@ -1 +1 @@
{"contentHash":"84c644ecaadfd74ec78bd5dfde78f92e6de1f12a05cfd156539f71797fb02ea8","entrypoint":"channel-outbound","importSpecifier":"openclaw/plugin-sdk/channel-outbound"}
{"contentHash":"0d20ad92fe85134d8dd0d4fbd144cba687a65d91930056f08eb8a03b791bf963","entrypoint":"channel-outbound","importSpecifier":"openclaw/plugin-sdk/channel-outbound"}
@@ -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"}
+1 -1
View File
@@ -1 +1 @@
{"contentHash":"e141ebb54f0102829e7edcd2bd0d1e9b21cc71ae972284dcb916e3a62031324a","entrypoint":"core","importSpecifier":"openclaw/plugin-sdk/core"}
{"contentHash":"28f52a46ac1d7bb3c522d9f5ff62d9b3b6e10cdec2be9f23b7ca2b8d8e4ca12c","entrypoint":"core","importSpecifier":"openclaw/plugin-sdk/core"}
+1 -1
View File
@@ -1 +1 @@
{"contentHash":"0e33b7a69d8c8a038922448b5ff5c049825d3872bb708b2c35bcd5472be98fb4","entrypoint":"discord","importSpecifier":"openclaw/plugin-sdk/discord"}
{"contentHash":"fb5d2a6e3e29e2a227b891b39e42462dab3dc62b804a2b7268d14346278fa1ea","entrypoint":"discord","importSpecifier":"openclaw/plugin-sdk/discord"}
@@ -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"}
@@ -1 +1 @@
{"contentHash":"3d54eabf64f44c0597a4273fa3a993d91ce335bca196fc12c49b9a472a752f22","entrypoint":"meeting-runtime","importSpecifier":"openclaw/plugin-sdk/meeting-runtime"}
{"contentHash":"8ead34b0bfed6204ad25b2753b27dc6b239b19afc8afbd0f498379124227eb4a","entrypoint":"meeting-runtime","importSpecifier":"openclaw/plugin-sdk/meeting-runtime"}
+1 -1
View File
@@ -1 +1 @@
{"contentHash":"fbaffdb6c6ea7f5713950c64f984dd1c297f9e556d6889acd0f60dc75e65fd4c","entrypoint":"plugin-entry","importSpecifier":"openclaw/plugin-sdk/plugin-entry"}
{"contentHash":"03c4a48091e9d1db55ef24461f1e4a5fdb333a4be2d90574fea86fba01c5cc4f","entrypoint":"plugin-entry","importSpecifier":"openclaw/plugin-sdk/plugin-entry"}
+1 -1
View File
@@ -1 +1 @@
{"contentHash":"cf61011d1a62cfa55ada8245928bd6877b6bbd5fa9f9fea63837697853c99067","entrypoint":"plugin-runtime","importSpecifier":"openclaw/plugin-sdk/plugin-runtime"}
{"contentHash":"ca0acf4e21e1978c6f51dce666d0f92b025f3184423be50cd659c8ae3d3e3cc5","entrypoint":"plugin-runtime","importSpecifier":"openclaw/plugin-sdk/plugin-runtime"}
@@ -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"}
+1 -1
View File
@@ -1 +1 @@
{"contentHash":"98ad69c4e05380e77b259132c66463b536675331ae77fea7465230b93c9daca6","entrypoint":"tool-plugin","importSpecifier":"openclaw/plugin-sdk/tool-plugin"}
{"contentHash":"d38378e903e47a17e5d3b86ae87c9e93e7513c883b2e6a49e1a6d2ea2f3b1514","entrypoint":"tool-plugin","importSpecifier":"openclaw/plugin-sdk/tool-plugin"}
@@ -1 +1 @@
{"contentHash":"0a5b8c7968e6761a2b73be6b88131758da62eb9cd3d00d20d58a18ba01b8ad87","entrypoint":"webhook-ingress","importSpecifier":"openclaw/plugin-sdk/webhook-ingress"}
{"contentHash":"e150792598fe5067b2eeaa80b4f3cf417c48ffe96f3c15cecced6fd3c0683992","entrypoint":"webhook-ingress","importSpecifier":"openclaw/plugin-sdk/webhook-ingress"}
+11 -15
View File
@@ -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 <id>` | Filter to one plugin or compatibility owner. |
| `--fail-on-cross-owner` | Exit non-zero on cross-owner reserved SDK imports. |
| `--owner <id>` | 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
+10 -13
View File
@@ -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.
<AccordionGroup>
<Accordion title="Channel subpaths">
@@ -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) |
</Accordion>
</AccordionGroup>
+4 -10
View File
@@ -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)}`,
);
});
});
+4 -8
View File
@@ -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}`,
);
+7 -3
View File
@@ -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,
};
+2 -1
View File
@@ -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`);
}
@@ -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);
+1 -1
View File
@@ -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,
-4
View File
@@ -1,4 +0,0 @@
/**
* Boolean parsing helper re-exported for Browser CLI/config code.
*/
export { parseBooleanValue } from "../sdk-config.js";
@@ -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",
};
+14 -25
View File
@@ -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 `<originator>/<codex-version> ...`; 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(
+7 -10
View File
@@ -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. */
@@ -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,
@@ -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;
}
@@ -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<v2.HooksListResponse>);
} 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)}`,
});
}
}
@@ -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,
+5 -5
View File
@@ -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));
}
}
+5 -4
View File
@@ -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) };
}
}
+1 -4
View File
@@ -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<string, unknown> & {
@@ -25,10 +26,6 @@ type RawSubcommandOption = {
options?: RawSubcommandOption[];
};
function clean<T extends Record<string, unknown>>(value: T): T {
return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== undefined)) as T;
}
function resolveConditionalCommandOption(
value: boolean | ConditionalCommandOption,
interaction: unknown,
@@ -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<T extends Record<string, unknown>>(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;
+1 -4
View File
@@ -1,9 +1,6 @@
// Discord plugin module implements embeds behavior.
import type { APIEmbed } from "discord-api-types/v10";
function clean<T extends Record<string, unknown>>(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;
+1 -4
View File
@@ -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<T extends Record<string, unknown>>(value: T): T {
return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== undefined)) as T;
}
function serializeAnyComponent(component: { serialize: () => unknown }): unknown {
return component.serialize();
}
@@ -0,0 +1,4 @@
// Discord plugin module implements undefined field filtering.
export function stripUndefinedFields<T extends object>(value: T): T {
return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== undefined)) as T;
}
@@ -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<T extends object>(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));
}
+1 -7
View File
@@ -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,
+12 -13
View File
@@ -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") ||
+1 -1
View File
@@ -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" }),
};
});
@@ -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 : []),
);
@@ -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<string | number>) {
return normalizeStringEntries(list);
}
+1 -2
View File
@@ -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<Record<MatrixResolvedStringField, string>>;
+1 -1
View File
@@ -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 {
@@ -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";
@@ -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 {
@@ -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";
@@ -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";
-4
View File
@@ -1,4 +0,0 @@
// Matrix plugin module implements record shared behavior.
import { isRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
export { isRecord };
@@ -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<string, unknown> | undefined {
return value !== null && typeof value === "object"
? (value as Record<string, unknown>)
: undefined;
}
function resolveConfiguredAgentIds(config: unknown): string[] {
const cfg = config as { agents?: { entries?: unknown; list?: unknown } };
const entries = readLegacyObjectRecord(cfg.agents?.entries);
+2 -2
View File
@@ -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 };
}
@@ -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 =
-5
View File
@@ -1,5 +0,0 @@
// Ollama plugin module implements ollama json behavior.
export {
parseJsonObjectPreservingUnsafeIntegers,
parseJsonPreservingUnsafeIntegers,
} from "openclaw/plugin-sdk/json-unsafe-integers";
+4 -4
View File
@@ -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,
+14 -37
View File
@@ -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<string, unknown>)[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<string, unknown>;
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<string, unknown>)[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,
+2 -2
View File
@@ -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",
+1 -4
View File
@@ -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) => {
@@ -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) {
@@ -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();
}
+1 -11
View File
@@ -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;
}
+6
View File
@@ -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"
+4 -7
View File
@@ -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 {
@@ -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";
@@ -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. */
@@ -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. */
@@ -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;
+5 -8
View File
@@ -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> | void } | null | undefined,
): Promise<Error | undefined> {
@@ -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 });
+13 -13
View File
@@ -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`,
);
}
}
-6
View File
@@ -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) {
+2 -2
View File
@@ -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(
-8
View File
@@ -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({
+4 -7
View File
@@ -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,
+3 -6
View File
@@ -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<string, { baseUrl?: string } | undefined> } } | 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;
}
+8 -12
View File
@@ -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
});
+7 -11
View File
@@ -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<string> {
const ids = new Set<string>();
for (const group of [file.channels, file.groups, file.ims]) {
@@ -624,7 +620,7 @@ function collectSlackDirectShareChannelIds(file: SlackFileInfoSummary): Set<stri
if (typeof entry !== "string") {
continue;
}
const normalized = normalizeSlackScopeValue(entry);
const normalized = normalizeOptionalString(entry);
if (normalized) {
ids.add(normalized);
}
@@ -648,7 +644,7 @@ function collectSlackSharedChannelIds(file: SlackFileInfoSummary): Set<string> {
const ids = new Set<string>();
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<string, unknown>;
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);
+3 -17
View File
@@ -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 : "";
+1 -7
View File
@@ -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,
@@ -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<T>(
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;
}
@@ -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,
+19 -44
View File
@@ -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<string, unknown>)[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<string, unknown>;
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<string, unknown>)[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,
+1 -2
View File
@@ -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",
@@ -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 {
@@ -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.
@@ -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);
}
@@ -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,
+10 -5
View File
@@ -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",
@@ -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]],
@@ -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`);
}
});
});
@@ -9,6 +9,17 @@ const sanitizeSurrogates = (text: string) =>
text.replace(/[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/g, "");
describe("stableStringify", () => {
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}}');
});
+1 -1
View File
@@ -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.
+1 -1
View File
@@ -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;
-3
View File
@@ -1,3 +0,0 @@
// Shared terminal string normalization helpers.
export { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce";
@@ -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
+7 -14
View File
@@ -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) {
+5 -17
View File
@@ -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<string, unknown>) {
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.
*/
+11 -11
View File
@@ -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<T extends ChangedCheckCommand>(
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,

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