feat(tooling): enforce noUncheckedIndexedAccess in the extensions lane (NUIA phase 4) (#105132)

* fix(extensions): make indexed access explicit across channel plugins

Transport-payload-safe burn-down: malformed Telegram/Discord/QQ/LINE
and sibling channel input keeps existing skip paths; no synthesized
fields, no new throws in delivery loops. Zalo escape sentinels preserve
literal matches instead of undefined replacements.

* fix(extensions): make indexed access explicit across provider and memory plugins

Stream and model iteration, tool-block guards, capture guards, and
sparse accumulators; singleton model reads carry named invariants.

* fix(extensions): make indexed access explicit across tooling plugins, flip the extensions lane

Remaining plugins (oc-path, qa-lab, browser, logbook, and siblings) plus
the tsconfig.extensions.json flag flip. Cleanup: logbook sampleFrames
NaN index at max=1, QA retry clamp at non-positive attempts, dead Canvas
probe and OpenShell no-op slice removed, twitch test setup leak excluded
from the prod lane.

* refactor(plugin-sdk): expose expectDefined via a focused SDK subpath

Extensions imported @openclaw/normalization-core directly, crossing the
external-plugin packaging boundary (it only worked because the runtime
builder bundles undeclared workspace helpers). expect-runtime joins the
canonical entrypoints JSON, generated exports, API baseline, docs, and
subpath contract test; all 78 extension imports now use the SDK seam.
Two scanner-shaped locals renamed for review-bundle hygiene.

* chore(plugin-sdk): raise surface budgets for the expect-runtime subpath

One new entrypoint with one callable export, added intentionally as the
packaging-honest seam for extension invariant helpers.
This commit is contained in:
Peter Steinberger
2026-07-12 09:17:31 +01:00
committed by GitHub
parent 43e138cc66
commit 218dcd815a
226 changed files with 1548 additions and 794 deletions
@@ -1,2 +1,2 @@
de87f2acba61514406fe343f0ee2896a101435203c4ef35af8db707b32d109f6 plugin-sdk-api-baseline.json
eb72aa8eb79d0729f12ea7715b0ff098677e06e677050009ba1add89cc357f27 plugin-sdk-api-baseline.jsonl
e9eede54e4d95e441f0d8dace534f3c94cbb4cc68523a5cae72914bca1683c42 plugin-sdk-api-baseline.json
4eff68c554e5c4740c528ea69be16e1f7079865bb363a4c5bc7a436d3c4d94c8 plugin-sdk-api-baseline.jsonl
+1
View File
@@ -324,6 +324,7 @@ SDK.
| Transport readiness waits | `openclaw/plugin-sdk/transport-ready-runtime` |
| Secure token helpers | `openclaw/plugin-sdk/secure-random-runtime` |
| Bounded async task concurrency | `openclaw/plugin-sdk/concurrency-runtime` |
| Required-value assertions for provable invariants | `openclaw/plugin-sdk/expect-runtime` |
| Numeric coercion | `openclaw/plugin-sdk/number-runtime` |
| Process-local async lock | `openclaw/plugin-sdk/async-lock-runtime` |
| File locks | `openclaw/plugin-sdk/file-lock` |
+1
View File
@@ -319,6 +319,7 @@ usage endpoint failed or returned no usable usage data.
| `plugin-sdk/delivery-queue-runtime` | Outbound pending-delivery drain helper |
| `plugin-sdk/file-access-runtime` | Safe local-file and media-source path helpers |
| `plugin-sdk/heartbeat-runtime` | Heartbeat wake, event, and visibility helpers |
| `plugin-sdk/expect-runtime` | Required-value assertion helper for provable runtime invariants |
| `plugin-sdk/number-runtime` | Numeric coercion helper |
| `plugin-sdk/secure-random-runtime` | Secure token/UUID helpers |
| `plugin-sdk/system-event-runtime` | System event queue helpers |
+5 -3
View File
@@ -142,12 +142,14 @@ export function extractTrustedCodexProjectPaths(configToml: string): string[] {
const assignment =
/^(?<key>"(?:\\.|[^"\\])*"|'[^']*'|[A-Za-z0-9_\-/.~:]+)\s*=\s*(?<value>.+)$/.exec(line);
if (!assignment?.groups) {
const rawKey = assignment?.groups?.key;
const rawValue = assignment?.groups?.value;
if (!rawKey || rawValue === undefined) {
continue;
}
const key = parseTomlString(assignment.groups.key) ?? assignment.groups.key;
const value = assignment.groups.value.trim();
const key = parseTomlString(rawKey) ?? rawKey;
const value = rawValue.trim();
if (inProjectsTable && /^\{.*\}$/.test(value)) {
if (/\btrust_level\s*=\s*["']trusted["']/.test(value) && key) {
trusted.add(key);
+7 -4
View File
@@ -198,13 +198,16 @@ function parseProcessList(stdout: string): AcpxProcessInfo[] {
const processes: AcpxProcessInfo[] = [];
for (const line of stdout.split(/\r?\n/)) {
const match = /^\s*(?<pid>\d+)\s+(?<ppid>\d+)\s+(?<command>.+?)\s*$/.exec(line);
if (!match?.groups) {
const pid = match?.groups?.pid;
const ppid = match?.groups?.ppid;
const command = match?.groups?.command;
if (!pid || !ppid || !command) {
continue;
}
processes.push({
pid: Number.parseInt(match.groups.pid, 10),
ppid: Number.parseInt(match.groups.ppid, 10),
command: match.groups.command,
pid: Number.parseInt(pid, 10),
ppid: Number.parseInt(ppid, 10),
command,
});
}
return processes;
+7 -2
View File
@@ -399,11 +399,16 @@ function isEnvAssignment(value: string): boolean {
}
function unwrapEnvCommand(parts: string[]): string[] {
if (!parts.length || basename(parts[0]) !== "env") {
const command = parts.at(0);
if (!command || basename(command) !== "env") {
return parts;
}
let index = 1;
while (index < parts.length && isEnvAssignment(parts[index])) {
while (true) {
const part = parts.at(index);
if (!part || !isEnvAssignment(part)) {
break;
}
index += 1;
}
return parts.slice(index);
@@ -337,8 +337,7 @@ function injectBedrockCachePoints(
// Bedrock Converse uses lowercase roles ("user" / "assistant").
const messages = payload.messages as BedrockMessage[] | undefined;
if (Array.isArray(messages) && messages.length > 0) {
for (let i = messages.length - 1; i >= 0; i--) {
const msg = messages[i];
for (const msg of messages.toReversed()) {
if (msg.role === "user" && Array.isArray(msg.content)) {
if (!hasCachePoint(msg.content)) {
msg.content.push(point);
+9 -6
View File
@@ -26,6 +26,7 @@ import {
} from "@aws-sdk/client-bedrock-runtime";
import { NodeHttpHandler } from "@smithy/node-http-handler";
import type { DocumentType } from "@smithy/types";
import { expectDefined } from "openclaw/plugin-sdk/expect-runtime";
import {
adjustMaxTokensForThinking,
AssistantMessageEventStream,
@@ -50,7 +51,6 @@ import {
type ThinkingLevel,
type Tool,
type ToolCall,
type ToolResultMessage,
} from "openclaw/plugin-sdk/llm";
import {
resolveClaudeFable5ModelIdentity,
@@ -504,7 +504,7 @@ function handleContentBlockDelta(
const newBlock: Block = { type: "text", text: "", index: contentBlockIndex };
output.content.push(newBlock);
index = blocks.length - 1;
block = blocks[index];
block = newBlock;
stream.push({ type: "text_start", contentIndex: index, partial: output });
}
if (block.type === "text") {
@@ -794,7 +794,7 @@ function convertMessages(
const transformedMessages = transformMessages(context.messages, model, normalizeToolCallId);
for (let i = 0; i < transformedMessages.length; i++) {
const m = transformedMessages[i];
const m = expectDefined(transformedMessages[i], "message conversion index is in bounds");
switch (m.role) {
case "user": {
@@ -917,8 +917,11 @@ function convertMessages(
// Look ahead for consecutive toolResult messages
let j = i + 1;
while (j < transformedMessages.length && transformedMessages[j].role === "toolResult") {
const nextMsg = transformedMessages[j] as ToolResultMessage;
while (true) {
const nextMsg = transformedMessages.at(j);
if (nextMsg?.role !== "toolResult") {
break;
}
toolResults.push({
toolResult: {
toolUseId: nextMsg.toolCallId,
@@ -949,7 +952,7 @@ function convertMessages(
// Add cache point to the last user message for supported Claude models when caching is enabled
if (cacheRetention !== "none" && supportsPromptCaching(model) && result.length > 0) {
const lastMessage = result[result.length - 1];
const lastMessage = expectDefined(result.at(-1), "non-empty converted message list");
if (lastMessage.role === ConversationRole.USER && lastMessage.content) {
lastMessage.content.push({
cachePoint: {
+16 -8
View File
@@ -143,13 +143,17 @@ export function normalizeClaudePermissionArgs(
}
const normalized: string[] = [];
let hasPermissionMode = false;
for (let i = 0; i < args.length; i += 1) {
const arg = args[i];
let skipNext = false;
for (const [index, arg] of args.entries()) {
if (skipNext) {
skipNext = false;
continue;
}
if (arg === CLAUDE_LEGACY_SKIP_PERMISSIONS_ARG) {
continue;
}
if (arg === CLAUDE_PERMISSION_MODE_ARG) {
const maybeValue = args[i + 1];
const maybeValue = args.at(index + 1);
if (
typeof maybeValue === "string" &&
maybeValue.trim().length > 0 &&
@@ -160,7 +164,7 @@ export function normalizeClaudePermissionArgs(
normalized.push(arg);
normalized.push(maybeValue);
}
i += 1;
skipNext = true;
}
continue;
}
@@ -189,10 +193,14 @@ export function normalizeClaudeSettingSourcesArgs(args?: string[]): string[] | u
}
const normalized: string[] = [];
let hasSettingSources = false;
for (let i = 0; i < args.length; i += 1) {
const arg = args[i];
let skipNext = false;
for (const [index, arg] of args.entries()) {
if (skipNext) {
skipNext = false;
continue;
}
if (arg === CLAUDE_SETTING_SOURCES_ARG) {
const maybeValue = args[i + 1];
const maybeValue = args.at(index + 1);
if (
typeof maybeValue === "string" &&
maybeValue.trim().length > 0 &&
@@ -200,7 +208,7 @@ export function normalizeClaudeSettingSourcesArgs(args?: string[]): string[] | u
) {
hasSettingSources = true;
normalized.push(arg, CLAUDE_SAFE_SETTING_SOURCES);
i += 1;
skipNext = true;
}
continue;
}
+5 -7
View File
@@ -431,13 +431,11 @@ export async function startGatewayBonjourAdvertiser(
const hostnameRaw =
process.env.OPENCLAW_MDNS_HOSTNAME?.trim() || resolveSystemMdnsHostname() || "openclaw";
const hostname = truncateToDnsLabel(
hostnameRaw
.replace(/\.local$/i, "")
.split(".")[0]
.trim() || "openclaw",
"openclaw",
);
const hostnameWithoutLocal = hostnameRaw.replace(/\.local$/i, "");
const dotIndex = hostnameWithoutLocal.indexOf(".");
const labelEnd = dotIndex === -1 ? hostnameWithoutLocal.length : dotIndex;
const hostnameLabel = hostnameWithoutLocal.slice(0, labelEnd).trim() || "openclaw";
const hostname = truncateToDnsLabel(hostnameLabel, "openclaw");
const instanceName =
typeof opts.instanceName === "string" && opts.instanceName.trim()
? opts.instanceName.trim()
@@ -167,6 +167,9 @@ function normalizeBraveUiLang(value: string | undefined): string | undefined {
return undefined;
}
const [, language, region] = match;
if (!language || !region) {
return undefined;
}
return `${normalizeLowercaseStringOrEmpty(language)}-${region.toUpperCase()}`;
}
+4 -3
View File
@@ -4,6 +4,7 @@
* Provides screenshots, target creation, JavaScript evaluation, ARIA/role
* snapshots, DOM text, and selector lookup on top of the CDP socket helpers.
*/
import { expectDefined } from "openclaw/plugin-sdk/expect-runtime";
import { resolveIntegerOption } from "openclaw/plugin-sdk/number-runtime";
import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
import type { SsrFPolicy } from "../infra/net/ssrf.js";
@@ -517,7 +518,7 @@ function buildRoleTree(nodes: RawAXNode[]): { tree: RoleTreeNode[]; roots: numbe
continue;
}
tree[index]?.children.push(childIndex);
tree[childIndex].parent = index;
expectDefined(tree[childIndex], "CDP child node index").parent = index;
childIndexes.add(childIndex);
}
}
@@ -529,7 +530,7 @@ function buildRoleTree(nodes: RawAXNode[]): { tree: RoleTreeNode[]; roots: numbe
if (!current) {
break;
}
tree[current.index].depth = current.depth;
expectDefined(tree[current.index], "CDP traversal node index").depth = current.depth;
for (const child of (tree[current.index]?.children ?? []).toReversed()) {
stack.push({ index: child, depth: current.depth + 1 });
}
@@ -857,7 +858,7 @@ async function buildCdpRoleSnapshot(params: {
if (node.backendDOMNodeId && iframeFrameIds.has(node.backendDOMNodeId)) {
node.frameId = iframeFrameIds.get(node.backendDOMNodeId);
if (node.ref && refs[node.ref]) {
refs[node.ref].frameId = node.frameId;
expectDefined(refs[node.ref], "owned CDP role reference").frameId = node.frameId;
}
}
}
@@ -55,7 +55,10 @@ function setDeep(obj: Record<string, unknown>, keys: string[], value: unknown) {
}
node = node[key] as Record<string, unknown>;
}
node[keys[keys.length - 1]] = value;
const lastKey = keys.at(-1);
if (lastKey !== undefined) {
node[lastKey] = value;
}
}
function parseHexRgbToSignedArgbInt(hex: string): number | null {
+3 -2
View File
@@ -1,10 +1,11 @@
import { parseBrowserHttpUrl } from "openclaw/plugin-sdk/browser-config";
/**
* Browser profile allocation helpers.
*
* Validates profile names and allocates CDP ports/colors for newly persisted
* browser profiles.
*/
import { parseBrowserHttpUrl } from "openclaw/plugin-sdk/browser-config";
import { expectDefined } from "openclaw/plugin-sdk/expect-runtime";
/**
* CDP port allocation for browser profiles.
@@ -111,7 +112,7 @@ export function allocateColor(usedColors: Set<string>): string {
}
// All colors used, cycle based on count
const index = usedColors.size % PROFILE_COLORS.length;
return PROFILE_COLORS[index] ?? PROFILE_COLORS[0];
return expectDefined(PROFILE_COLORS[index], "cycled browser color palette index");
}
/** Extract currently used profile colors from profile config. */
@@ -4,6 +4,7 @@
* Converts ARIA or AI snapshots into compact role/name text with stable refs
* and duplicate disambiguation for agent actions.
*/
import { expectDefined } from "openclaw/plugin-sdk/expect-runtime";
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/string-coerce-runtime";
import { CONTENT_ROLES, INTERACTIVE_ROLES, STRUCTURAL_ROLES } from "./snapshot-roles.js";
@@ -107,7 +108,8 @@ export function finalizeRoleSnapshot<T extends { role: string }>(params: {
function getIndentLevel(line: string): number {
const match = line.match(/^(\s*)/);
return match ? Math.floor(match[1].length / 2) : 0;
const indent = match?.[1];
return indent === undefined ? 0 : Math.floor(indent.length / 2);
}
function matchInteractiveSnapshotLine(
@@ -125,6 +127,9 @@ function matchInteractiveSnapshotLine(
const roleRaw = match[2];
const name = match[3];
const suffix = match[4];
if (roleRaw === undefined || suffix === undefined) {
return null;
}
if (roleRaw.startsWith("/")) {
return null;
}
@@ -201,13 +206,20 @@ function compactTree(tree: string) {
}
current.entry.keep ||= current.entry.hasRef;
if (current.entry.hasRef && stack.length > 0) {
stack[stack.length - 1].entry.hasRef = true;
const parent = stack.at(-1);
if (parent !== undefined) {
parent.entry.hasRef = true;
}
}
};
for (const line of lines) {
const indent = getIndentLevel(line);
while (stack.length > 0 && stack[stack.length - 1].indent >= indent) {
while (stack.length > 0) {
const lastEntry = expectDefined(stack.at(-1), "non-empty role snapshot stack");
if (lastEntry.indent < indent) {
break;
}
finishEntry();
}
const entry = {
@@ -246,7 +258,13 @@ function processLine(
return options.interactive ? null : line;
}
const [, prefix, roleRaw, name, suffix] = match;
const prefix = match[1];
const roleRaw = match[2];
const name = match[3];
const suffix = match[4];
if (prefix === undefined || roleRaw === undefined || suffix === undefined) {
return options.interactive ? null : line;
}
if (roleRaw.startsWith("/")) {
return options.interactive ? null : line;
}
@@ -414,10 +432,10 @@ export function buildRoleSnapshotFromAriaSnapshot(
function parseAiSnapshotRef(suffix: string): string | null {
const eMatch = suffix.match(/\[ref=(e\d+)\]/i);
if (eMatch) {
return eMatch[1];
return eMatch[1] ?? null;
}
const numMatch = suffix.match(/\[ref=(\d{1,9})\]/);
return numMatch ? numMatch[1] : null;
return numMatch?.[1] ?? null;
}
/**
@@ -466,6 +484,10 @@ export function buildRoleSnapshotFromAiSnapshot(
const roleRaw = match[2];
const name = match[3];
const suffix = match[4];
if (roleRaw === undefined || suffix === undefined) {
out.push(line);
continue;
}
if (roleRaw.startsWith("/")) {
out.push(line);
continue;
+3 -2
View File
@@ -4,6 +4,7 @@
* Manages CDP-backed Playwright connections, page lookup, observed dialogs,
* console/network/page state, role refs, and safe navigation handling.
*/
import { expectDefined } from "openclaw/plugin-sdk/expect-runtime";
import {
isFutureDateTimestampMs,
parseFiniteNumber,
@@ -1152,7 +1153,7 @@ function resolvePendingDialogForResponse(params: {
throw new Error(`Dialog "${dialogId}" is not pending.`);
}
if (params.state.pendingDialogs.length === 1) {
return params.state.pendingDialogs[0];
return expectDefined(params.state.pendingDialogs.at(0), "single pending browser dialog");
}
if (params.state.pendingDialogs.length > 1) {
throw new Error("Multiple dialogs are pending; pass dialogId.");
@@ -1488,7 +1489,7 @@ async function getPageForTargetIdOnce(opts: {
}
throw new Error("No pages available in the connected browser.");
}
const first = accessible[0];
const first = expectDefined(accessible.at(0), "non-empty accessible browser pages");
if (!opts.targetId) {
bindRoleRefsTarget(first.page, opts.cdpUrl, first.targetId);
return first.page;
@@ -1483,6 +1483,10 @@ export async function screenshotWithLabelsViaPlaywright(opts: {
const inputs: RawAnnotationInput[] = [];
let bboxFailures = 0;
for (const ref of refKeys) {
const refInfo = opts.refs[ref];
if (refInfo === undefined) {
continue;
}
const box = await refLocator(page, ref)
.boundingBox()
.catch(() => null);
@@ -1492,8 +1496,8 @@ export async function screenshotWithLabelsViaPlaywright(opts: {
}
inputs.push({
ref,
role: opts.refs[ref].role,
name: opts.refs[ref].name,
role: refInfo.role,
name: refInfo.name,
doc: {
x: box.x + scroll.x,
y: box.y + scroll.y,
@@ -1,10 +1,11 @@
import { setTimeout as sleep } from "node:timers/promises";
/**
* Browser agent action route registration and existing-session execution.
*
* Dispatches normalized actions to either Playwright-backed OpenClaw browser
* control or Chrome MCP existing-session operations with navigation guards.
*/
import { setTimeout as sleep } from "node:timers/promises";
import { expectDefined } from "openclaw/plugin-sdk/expect-runtime";
import { formatErrorMessage } from "../../infra/errors.js";
import {
clickChromeMcpElement,
@@ -216,7 +217,9 @@ function buildExistingSessionWaitPredicate(params: {
if (checks.length === 0) {
return null;
}
return checks.length === 1 ? checks[0] : checks.map((check) => `(${check})`).join(" && ");
return checks.length === 1
? expectDefined(checks.at(0), "single existing-session condition")
: checks.map((check) => `(${check})`).join(" && ");
}
async function waitForExistingSessionCondition(
@@ -73,15 +73,15 @@ function resolveProfileForTest(
state: BrowserServerState,
profileName: string,
): ResolvedBrowserProfile {
const rawProfile = state.resolved.profiles[profileName] ?? {};
const rawProfile = state.resolved.profiles[profileName];
const cdpPort =
typeof rawProfile.cdpPort === "number"
typeof rawProfile?.cdpPort === "number"
? rawProfile.cdpPort
: profileName === "remote"
? 9222
: state.resolved.cdpPortRangeStart;
const cdpUrl =
typeof rawProfile.cdpUrl === "string"
typeof rawProfile?.cdpUrl === "string"
? rawProfile.cdpUrl
: `${state.resolved.cdpProtocol}://${state.resolved.cdpHost}:${cdpPort}`;
const parsed = new URL(cdpUrl.replace(/^ws/i, "http"));
@@ -93,13 +93,13 @@ function resolveProfileForTest(
cdpUrl,
cdpHost,
cdpIsLoopback,
color: rawProfile.color ?? state.resolved.color,
driver: rawProfile.driver === "existing-session" ? "existing-session" : "openclaw",
headless: rawProfile.headless ?? state.resolved.headless,
color: rawProfile?.color ?? state.resolved.color,
driver: rawProfile?.driver === "existing-session" ? "existing-session" : "openclaw",
headless: rawProfile?.headless ?? state.resolved.headless,
headlessSource:
typeof rawProfile.headless === "boolean" ? "profile" : state.resolved.headlessSource,
attachOnly: rawProfile.attachOnly ?? state.resolved.attachOnly,
userDataDir: rawProfile.userDataDir,
typeof rawProfile?.headless === "boolean" ? "profile" : state.resolved.headlessSource,
attachOnly: rawProfile?.attachOnly ?? state.resolved.attachOnly,
userDataDir: rawProfile?.userDataDir,
};
}
@@ -97,10 +97,10 @@ export async function readKeychainSecret(
const raw = Buffer.from(stdout);
let start = 0;
let end = raw.length;
while (start < end && isAsciiWhitespace(raw[start])) {
while (start < end && isAsciiWhitespace(raw.readUInt8(start))) {
start += 1;
}
while (end > start && isAsciiWhitespace(raw[end - 1])) {
while (end > start && isAsciiWhitespace(raw.readUInt8(end - 1))) {
end -= 1;
}
const secret = Buffer.from(raw.subarray(start, end));
+1 -2
View File
@@ -118,8 +118,7 @@ export function neutralizeMediaDirectives(text: string): string {
}
const lines = text.split("\n");
let changed = false;
for (let i = 0; i < lines.length; i += 1) {
const line = lines[i];
for (const [i, line] of lines.entries()) {
const leading = line.length - line.trimStart().length;
const rest = line.slice(leading);
if (/^MEDIA:/i.test(rest)) {
+3
View File
@@ -225,6 +225,9 @@ export function resolveCanvasHttpPathToLocalPath(
return null;
}
const [rawDocumentId, ...entrySegments] = segments;
if (!rawDocumentId) {
return null;
}
try {
const documentId = normalizeCanvasDocumentId(rawDocumentId);
const normalizedEntrypoint = normalizeLogicalPath(entrySegments.join("/"));
+1 -10
View File
@@ -1,7 +1,6 @@
/**
* Canvas host server and static-file/live-reload handler implementation.
*/
import * as fsSync from "node:fs";
import fs from "node:fs/promises";
import http, { type IncomingMessage, type Server, type ServerResponse } from "node:http";
import { createRequire } from "node:module";
@@ -246,15 +245,7 @@ async function resolveDocumentCspSandbox(
}
function resolveDefaultCanvasRoot(): string {
const candidates = [path.join(resolveStateDir(), "canvas")];
const existing = candidates.find((dir) => {
try {
return fsSync.statSync(dir).isDirectory();
} catch {
return false;
}
});
return existing ?? candidates[0];
return path.join(resolveStateDir(), "canvas");
}
function resolveDefaultWatchFactory(): ChokidarWatch {
+2 -1
View File
@@ -1,6 +1,7 @@
/**
* Cerebras model catalog helpers derived from the plugin manifest.
*/
import { expectDefined } from "openclaw/plugin-sdk/expect-runtime";
import { buildManifestModelProviderConfig } from "openclaw/plugin-sdk/provider-catalog-shared";
import type { ModelDefinitionConfig } from "openclaw/plugin-sdk/provider-model-shared";
import manifest from "./openclaw.plugin.json" with { type: "json" };
@@ -28,5 +29,5 @@ export function buildCerebrasModelDefinition(
providerId: "cerebras",
catalog: { ...CEREBRAS_MANIFEST_CATALOG, models: [model] },
});
return providerConfig.models[0];
return expectDefined(providerConfig.models.at(0), "normalized Cerebras manifest model");
}
+4 -1
View File
@@ -2,6 +2,7 @@
* Codex provider plugin and live app-server model catalog discovery.
*/
import { createSubsystemLogger } from "openclaw/plugin-sdk/core";
import { expectDefined } from "openclaw/plugin-sdk/expect-runtime";
import { resolvePluginConfigObject } from "openclaw/plugin-sdk/plugin-config-runtime";
import type { ProviderRuntimeModel } from "openclaw/plugin-sdk/plugin-entry";
import {
@@ -33,7 +34,9 @@ const DEFAULT_DISCOVERY_TIMEOUT_MS = 2500;
const LIVE_DISCOVERY_ENV = "OPENCLAW_CODEX_DISCOVERY_LIVE";
const MODEL_DISCOVERY_PAGE_LIMIT = 100;
const CODEX_APP_SERVER_SETUP_METHOD_ID = "app-server";
const CODEX_DEFAULT_MODEL_REF = `${CODEX_PROVIDER_ID}/${FALLBACK_CODEX_MODELS[0].id}`;
const CODEX_DEFAULT_MODEL_REF = `${CODEX_PROVIDER_ID}/${
expectDefined(FALLBACK_CODEX_MODELS[0], "Codex fallback model catalog must not be empty").id
}`;
const codexCatalogLog = createSubsystemLogger("codex/catalog");
const CODEX_REASONING_EFFORTS = [
"minimal",
@@ -42,6 +42,7 @@ import {
wrapToolWithBeforeToolCallHook,
} from "openclaw/plugin-sdk/agent-harness-runtime";
import { emitTrustedDiagnosticEvent } from "openclaw/plugin-sdk/diagnostic-runtime";
import { expectDefined } from "openclaw/plugin-sdk/expect-runtime";
import type { ImageContent, TextContent } from "openclaw/plugin-sdk/llm";
import { normalizeAgentId } from "openclaw/plugin-sdk/routing";
import {
@@ -387,7 +388,7 @@ function computerFrameImageIdentity(
if (images.length !== 1) {
return undefined;
}
const image = images[0];
const image = expectDefined(images[0], "single Codex computer frame image");
return createHash("sha256")
.update(JSON.stringify([image.mimeType, image.data]))
.digest("hex");
@@ -1139,7 +1140,7 @@ function composeAbortSignals(...signals: Array<AbortSignal | undefined>): AbortS
return new AbortController().signal;
}
if (activeSignals.length === 1) {
return activeSignals[0];
return expectDefined(activeSignals[0], "single active Codex abort signal");
}
return AbortSignal.any(activeSignals);
}
@@ -7,6 +7,7 @@ import { access } from "node:fs/promises";
import { createRequire } from "node:module";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { expectDefined } from "openclaw/plugin-sdk/expect-runtime";
import type { CodexAppServerStartOptions, CodexManagedCommandOrder } from "./config.js";
import { MANAGED_CODEX_APP_SERVER_PACKAGE } from "./version.js";
@@ -57,7 +58,7 @@ export async function resolveManagedCodexAppServerStartOptions(
pathExists,
platform,
});
const commandPath = commandPaths[0];
const commandPath = expectDefined(commandPaths[0], "resolved managed Codex command path");
const managedFallbackCommandPaths = commandPaths.slice(1);
return {
@@ -409,8 +409,10 @@ function sortJsonValue(value: JsonValue): JsonValue {
return value.map(sortJsonValue);
}
const sorted: JsonObject = {};
for (const key of Object.keys(value).toSorted()) {
sorted[key] = sortJsonValue(value[key]);
for (const [key, entry] of Object.entries(value).toSorted(([left], [right]) =>
left.localeCompare(right),
)) {
sorted[key] = sortJsonValue(entry);
}
return sorted;
}
@@ -1482,8 +1482,7 @@ function lastChildAssistantMessage(childState: ChildState, turnId: string): stri
if (!messages) {
return undefined;
}
for (let index = messages.order.length - 1; index >= 0; index -= 1) {
const itemId = messages.order[index];
for (const itemId of messages.order.toReversed()) {
if (messages.finalMessageIds.has(itemId) && !messages.commentaryIds.has(itemId)) {
const text = normalizeOptionalString(messages.texts.get(itemId));
if (text) {
@@ -2,6 +2,7 @@
* Parses Codex account rate-limit payloads into user-facing usage summaries,
* reset hints, and enriched usage-limit error messages.
*/
import { expectDefined } from "openclaw/plugin-sdk/expect-runtime";
import {
MAX_DATE_TIMESTAMP_MS,
resolveExpiresAtMsFromEpochSeconds,
@@ -160,7 +161,9 @@ export function summarizeCodexAccountUsage(
if (snapshots.length === 0) {
return undefined;
}
const usageSnapshot = snapshots.find(isCodexLimitSnapshot) ?? snapshots[0];
const usageSnapshot =
snapshots.find(isCodexLimitSnapshot) ??
expectDefined(snapshots[0], "displayable Codex rate-limit snapshot");
const blockedSnapshots = snapshots.filter(snapshotHasLimitBlock);
const blockingSnapshot =
blockedSnapshots.find(isCodexLimitSnapshot) ?? blockedSnapshots[0] ?? undefined;
@@ -767,8 +767,7 @@ async function startInitializedCodexAppServerClient(params: {
const acquireStartedAt = Date.now();
const timeoutMs = params.timeoutMs ?? 0;
const startOptionsCandidates = resolveManagedFallbackStartOptions(params.startOptions);
for (let index = 0; index < startOptionsCandidates.length; index += 1) {
const startOptions = startOptionsCandidates[index];
for (const [index, startOptions] of startOptionsCandidates.entries()) {
const runtimeArtifactModule = params.runtimeArtifactMode
? await import("./runtime-artifact.js")
: undefined;
@@ -905,8 +904,7 @@ function resolveManagedFallbackStartOptions(
): CodexAppServerStartOptions[] {
const commands = [startOptions.command, ...(startOptions.managedFallbackCommandPaths ?? [])];
const candidates: CodexAppServerStartOptions[] = [];
for (let index = 0; index < commands.length; index += 1) {
const command = commands[index];
for (const [index, command] of commands.entries()) {
const managedFallbackCommandPaths = commands.slice(index + 1);
const candidate = {
...startOptions,
+1 -1
View File
@@ -290,7 +290,7 @@ function resolveLiveAccountProfileId(params: {
return (
params.order.find((profileId) => {
const credential = params.store.profiles[profileId];
if (!isChatGptSubscriptionProfile(credential)) {
if (!credential || !isChatGptSubscriptionProfile(credential)) {
return false;
}
const profileEmail =
+4 -3
View File
@@ -1,6 +1,7 @@
// Codex plugin module implements command handlers behavior.
import crypto from "node:crypto";
import { resolveAgentDir, resolveSessionAgentIds } from "openclaw/plugin-sdk/agent-runtime";
import { expectDefined } from "openclaw/plugin-sdk/expect-runtime";
import {
isModelSelectionLocked,
MODEL_SELECTION_LOCKED_MESSAGE,
@@ -2337,7 +2338,7 @@ function splitArgs(value: string | undefined): string[] {
function parseBindArgs(args: string[]): ParsedBindArgs {
const parsed: ParsedBindArgs = {};
for (let index = 0; index < args.length; index += 1) {
const arg = args[index];
const arg = expectDefined(args[index], "current Codex bind argument");
if (arg === "--help" || arg === "-h") {
parsed.help = true;
continue;
@@ -2389,7 +2390,7 @@ function parseCodexCliSessionsArgs(args: string[]): ParsedCodexCliSessionsArgs {
const parsed: ParsedCodexCliSessionsArgs = { filter: "" };
const filter: string[] = [];
for (let index = 0; index < args.length; index += 1) {
const arg = args[index];
const arg = expectDefined(args[index], "current Codex sessions argument");
if (arg === "--help" || arg === "-h") {
parsed.help = true;
continue;
@@ -2429,7 +2430,7 @@ function parseCodexCliSessionsArgs(args: string[]): ParsedCodexCliSessionsArgs {
function parseResumeArgs(args: string[]): ParsedResumeArgs {
const parsed: ParsedResumeArgs = {};
for (let index = 0; index < args.length; index += 1) {
const arg = args[index];
const arg = expectDefined(args[index], "current Codex resume argument");
if (arg === "--help" || arg === "-h") {
parsed.help = true;
continue;
+2 -1
View File
@@ -4,6 +4,7 @@ import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import process from "node:process";
import { expectDefined } from "openclaw/plugin-sdk/expect-runtime";
import { timestampMsToIsoString } from "openclaw/plugin-sdk/number-runtime";
import type {
OpenClawPluginNodeHostCommand,
@@ -594,7 +595,7 @@ async function resolveCodexCliNode(params: {
if (usable.length > 1) {
throw new Error("Multiple Codex CLI-capable nodes connected. Pass --host <node-id>.");
}
return usable[0];
return expectDefined(usable[0], "single usable Codex CLI node");
}
function parseCodexCliSessionsListResult(raw: unknown): CodexCliSessionsListResult {
+6 -6
View File
@@ -1,3 +1,6 @@
import { resolveDefaultAgentDir } from "openclaw/plugin-sdk/agent-runtime";
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
import { jsonResult, readStringParam, type AnyAgentTool } from "openclaw/plugin-sdk/core";
/**
* Compatibility tools for the retired Codex Supervisor plugin.
*
@@ -6,9 +9,7 @@
* continuation belongs to the Codex harness, which installs approval and tool
* handlers before it starts or resumes the harness-owned Codex thread.
*/
import { resolveDefaultAgentDir } from "openclaw/plugin-sdk/agent-runtime";
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
import { jsonResult, readStringParam, type AnyAgentTool } from "openclaw/plugin-sdk/core";
import { expectDefined } from "openclaw/plugin-sdk/expect-runtime";
import { isRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
import { Type } from "typebox";
import {
@@ -753,7 +754,7 @@ async function resolveEndpointForThread(params: {
}
}
if (matches.length === 1) {
return matches[0];
return expectDefined(matches[0], "single matching Codex supervision endpoint");
}
if (matches.length > 1) {
throw new Error(`Codex thread id is ambiguous across endpoints: ${params.threadId}`);
@@ -763,8 +764,7 @@ async function resolveEndpointForThread(params: {
function findInProgressTurnId(thread: Record<string, unknown>): string | undefined {
const turns = asRecordArray(thread.turns);
for (let index = turns.length - 1; index >= 0; index -= 1) {
const turn = turns[index];
for (const turn of turns.toReversed()) {
if (turn.status === "inProgress" && typeof turn.id === "string") {
return turn.id;
}
+4 -2
View File
@@ -1,6 +1,7 @@
/**
* Cohere model catalog helpers derived from the plugin manifest.
*/
import { expectDefined } from "openclaw/plugin-sdk/expect-runtime";
import { buildManifestModelProviderConfig } from "openclaw/plugin-sdk/provider-catalog-shared";
import type { ModelDefinitionConfig } from "openclaw/plugin-sdk/provider-model-shared";
import manifest from "./openclaw.plugin.json" with { type: "json" };
@@ -35,8 +36,9 @@ export function buildCohereCatalogModels(): ModelDefinitionConfig[] {
export function buildCohereModelDefinition(
model: (typeof COHERE_MODEL_CATALOG)[number],
): ModelDefinitionConfig {
return buildManifestModelProviderConfig({
const providerConfig = buildManifestModelProviderConfig({
providerId: "cohere",
catalog: { ...COHERE_MANIFEST_CATALOG, models: [model] },
}).models[0];
});
return expectDefined(providerConfig.models.at(0), "normalized Cohere manifest model");
}
@@ -26,20 +26,20 @@ export function selectPendingApprovalRequest(params: {
pending: PendingPairingEntry[];
requested?: string;
}): { pending?: PendingPairingEntry; reply?: { text: string } } {
if (params.pending.length === 0) {
const [firstPending, ...remainingPending] = params.pending;
if (!firstPending) {
return { reply: { text: "No pending device pairing requests." } };
}
if (!params.requested) {
return params.pending.length === 1
? { pending: params.pending[0] }
return remainingPending.length === 0
? { pending: firstPending }
: { reply: buildMultiplePendingApprovalReply(params.pending) };
}
if (normalizeLowercaseStringOrEmpty(params.requested) === "latest") {
let latest = params.pending[0];
for (let index = 1; index < params.pending.length; index += 1) {
const pending = params.pending[index];
let latest = firstPending;
for (const pending of remainingPending) {
if ((pending.ts ?? 0) > (latest.ts ?? 0)) {
latest = pending;
}
+3 -10
View File
@@ -1327,13 +1327,10 @@ function assignOtelLogEventAttributes(
if (!eventAttributes) {
return;
}
for (const rawKey in eventAttributes) {
for (const [rawKey, value] of Object.entries(eventAttributes)) {
if (Object.keys(attributes).length >= MAX_OTEL_LOG_ATTRIBUTE_COUNT) {
break;
}
if (!Object.hasOwn(eventAttributes, rawKey)) {
continue;
}
const key = rawKey.trim();
if (BLOCKED_OTEL_LOG_ATTRIBUTE_KEYS.has(key)) {
continue;
@@ -1344,7 +1341,7 @@ function assignOtelLogEventAttributes(
if (!OTEL_LOG_RAW_ATTRIBUTE_KEY_RE.test(key)) {
continue;
}
assignOtelLogAttribute(attributes, `openclaw.${key}`, eventAttributes[rawKey]);
assignOtelLogAttribute(attributes, `openclaw.${key}`, value);
}
}
@@ -1355,13 +1352,10 @@ function assignOtelSecurityEventAttributes(
if (!eventAttributes) {
return;
}
for (const rawKey in eventAttributes) {
for (const [rawKey, value] of Object.entries(eventAttributes)) {
if (Object.keys(attributes).length >= MAX_OTEL_LOG_ATTRIBUTE_COUNT) {
break;
}
if (!Object.hasOwn(eventAttributes, rawKey)) {
continue;
}
const key = rawKey.trim();
if (BLOCKED_OTEL_LOG_ATTRIBUTE_KEYS.has(key)) {
continue;
@@ -1372,7 +1366,6 @@ function assignOtelSecurityEventAttributes(
if (!OTEL_LOG_RAW_ATTRIBUTE_KEY_RE.test(key)) {
continue;
}
const value = eventAttributes[rawKey];
assignOtelLogAttribute(
attributes,
`openclaw.security.attribute.${key}`,
+3 -3
View File
@@ -30,7 +30,7 @@ export function extractDiscordChannelId(sessionKey?: string | null): string | nu
return null;
}
const match = sessionKey.match(/discord:(?:channel|group):(\d+)/);
return match ? match[1] : null;
return match?.[1] ?? null;
}
function extractDiscordSessionKind(sessionKey?: string | null): "channel" | "group" | "dm" | null {
@@ -48,7 +48,7 @@ function extractDiscordSessionKind(sessionKey?: string | null): "channel" | "gro
if (raw === "direct") {
return "dm";
}
return raw as "channel" | "group" | "dm";
return raw === "channel" || raw === "group" || raw === "dm" ? raw : null;
}
function normalizeDiscordOriginChannelId(value?: string | null): string | null {
@@ -61,7 +61,7 @@ function normalizeDiscordOriginChannelId(value?: string | null): string | null {
}
const prefixed = trimmed.match(/^(?:channel|group):(\d+)$/i);
if (prefixed) {
return prefixed[1];
return prefixed[1] ?? null;
}
return /^\d+$/.test(trimmed) ? trimmed : null;
}
+5 -4
View File
@@ -1,4 +1,5 @@
// Discord plugin module implements chunk behavior.
import { expectDefined } from "openclaw/plugin-sdk/expect-runtime";
import { resolveIntegerOption } from "openclaw/plugin-sdk/number-runtime";
import { chunkMarkdownTextWithMode, type ChunkMode } from "openclaw/plugin-sdk/reply-chunking";
@@ -93,7 +94,7 @@ function clampToCodePointBoundary(text: string, index: number) {
function findWhitespaceBreak(window: string) {
for (let i = window.length - 1; i >= 0; i--) {
if (/\s/.test(window[i])) {
if (/\s/.test(window.charAt(i))) {
// Return the separator index so whitespace stays with the next segment.
return i;
}
@@ -244,7 +245,7 @@ export function chunkDiscordText(text: string, opts: ChunkDiscordTextOpts = {}):
currentLines += 1;
}
} else {
current = segment;
current = expectDefined(segment, "current Discord chunk segment");
currentLines = 1;
}
}
@@ -305,7 +306,7 @@ function rebalanceReasoningItalics(source: string, chunks: string[]): string[] {
const adjusted = [...chunks];
for (let i = 0; i < adjusted.length; i++) {
const isLast = i === adjusted.length - 1;
const current = adjusted[i];
const current = expectDefined(adjusted[i], "Discord chunk adjustment index");
// Ensure current chunk closes italics so Discord renders it italicized.
const needsClosing = !current.trimEnd().endsWith("_");
@@ -318,7 +319,7 @@ function rebalanceReasoningItalics(source: string, chunks: string[]): string[] {
}
// Re-open italics on the next chunk if needed.
const next = adjusted[i + 1];
const next = expectDefined(adjusted[i + 1], "non-final Discord chunk successor");
const leadingWhitespaceLen = next.length - next.trimStart().length;
const leadingWhitespace = next.slice(0, leadingWhitespaceLen);
const nextBody = next.slice(leadingWhitespaceLen);
+4 -3
View File
@@ -1,6 +1,7 @@
// Discord plugin module implements doctor behavior.
import type { ChannelDoctorAdapter } from "openclaw/plugin-sdk/channel-contract";
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
// Discord plugin module implements doctor behavior.
import { expectDefined } from "openclaw/plugin-sdk/expect-runtime";
import { collectProviderDangerousNameMatchingScopes } from "openclaw/plugin-sdk/runtime-doctor";
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
import { inspectDiscordAccount } from "./account-inspect.js";
@@ -161,14 +162,14 @@ export function collectDiscordNumericIdWarnings(params: {
const lines: string[] = [];
if (repairableHits.length > 0) {
const sample = repairableHits[0];
const sample = expectDefined(repairableHits.at(0), "non-empty repairable Discord ID hits");
lines.push(
`- Discord allowlists contain ${repairableHits.length} numeric ${repairableHits.length === 1 ? "entry" : "entries"} (e.g. ${sanitizeForLog(sample.path)}=${sanitizeForLog(String(sample.entry))}).`,
`- Discord IDs must be strings; run "${params.doctorFixCommand}" to convert numeric IDs to quoted strings.`,
);
}
if (blockedHits.length > 0) {
const sample = blockedHits[0];
const sample = expectDefined(blockedHits.at(0), "non-empty blocked Discord ID hits");
lines.push(
`- Discord allowlists contain ${blockedHits.length} numeric ${blockedHits.length === 1 ? "entry" : "entries"} in lists that cannot be auto-repaired (e.g. ${sanitizeForLog(sample.path)}).`,
`- These lists include invalid or precision-losing numeric IDs; manually quote the original values in your config file, then rerun "${params.doctorFixCommand}".`,
@@ -1,4 +1,5 @@
// Discord plugin module implements components.base behavior.
import { expectDefined } from "openclaw/plugin-sdk/expect-runtime";
import type { BaseComponentInteraction } from "./interactions.js";
export type ComponentParserResult = {
@@ -13,9 +14,13 @@ export type ComponentData<
export type ConditionalComponentOption = (interaction: BaseComponentInteraction) => boolean;
export function parseCustomId(id: string): ComponentParserResult {
const [rawKey, ...parts] = id.split(";");
const [rawKeyValue, ...parts] = id.split(";");
const rawKey = expectDefined(rawKeyValue, "custom id split first segment");
const [keyPart, firstValue] = rawKey.split("=");
const key = keyPart.includes(":") ? keyPart.split(":")[0] : keyPart;
const definedKeyPart = expectDefined(keyPart, "custom id key segment");
const key = definedKeyPart.includes(":")
? expectDefined(definedKeyPart.split(":").at(0), "namespaced custom id key")
: definedKeyPart;
const data: ComponentParserResult["data"] = {};
const entries = firstValue === undefined ? parts : [rawKey.slice(key.length + 1), ...parts];
for (const entry of entries) {
+2 -1
View File
@@ -1,4 +1,5 @@
// Discord plugin module implements mentions behavior.
import { expectDefined } from "openclaw/plugin-sdk/expect-runtime";
import {
normalizeLowercaseStringOrEmpty,
normalizeOptionalString,
@@ -38,7 +39,7 @@ export function formatMention(params: {
if (values.length !== 1) {
throw new Error("formatMention requires exactly one of userId, roleId, or channelId");
}
const target = values[0];
const target = expectDefined(values.at(0), "single Discord mention target");
if (target.kind === "user") {
return `<@${target.id}>`;
}
@@ -1,4 +1,3 @@
// Discord plugin module implements message handlerraft preview behavior.
import { EmbeddedBlockChunker } from "openclaw/plugin-sdk/agent-runtime";
import {
type ChannelProgressDraftLine,
@@ -10,6 +9,8 @@ import {
resolveChannelStreamingSuppressDefaultToolProgressMessages,
} from "openclaw/plugin-sdk/channel-outbound";
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
// Discord plugin module implements message handlerraft preview behavior.
import { expectDefined } from "openclaw/plugin-sdk/expect-runtime";
import {
convertMarkdownTables,
stripInlineDirectiveTagsForDelivery,
@@ -213,7 +214,7 @@ export function createDiscordDraftPreviewController(params: {
if (chunks.length !== 1) {
return undefined;
}
const trimmed = chunks[0].trim();
const trimmed = expectDefined(chunks.at(0), "single Discord preview chunk").trim();
if (!trimmed) {
return undefined;
}
@@ -1,6 +1,7 @@
// Discord plugin module implements model picker.state behavior.
import { createHash } from "node:crypto";
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
import { expectDefined } from "openclaw/plugin-sdk/expect-runtime";
import { createLazyRuntimeModule } from "openclaw/plugin-sdk/lazy-runtime";
import type { ModelsProviderData } from "openclaw/plugin-sdk/models-provider-runtime";
import { parseStrictInteger, parseStrictPositiveInteger } from "openclaw/plugin-sdk/number-runtime";
@@ -398,9 +399,8 @@ export function computeAlphaBuckets(sortedItems: string[]): DiscordModelPickerBu
}
const firstLetter = (value: string): string => value.charAt(0).toLowerCase();
const allSamePrefix = sortedItems.every(
(item) => firstLetter(item) === firstLetter(sortedItems[0]),
);
const firstItem = expectDefined(sortedItems.at(0), "non-empty sorted model picker items");
const allSamePrefix = sortedItems.every((item) => firstLetter(item) === firstLetter(firstItem));
if (allSamePrefix) {
return chunkBucketsByCount(sortedItems);
}
@@ -418,13 +418,16 @@ export function computeAlphaBuckets(sortedItems: string[]): DiscordModelPickerBu
let end = Math.min(sortedItems.length, start + target);
// Extend `end` so we don't split a letter group across two buckets.
if (end < sortedItems.length) {
const last = firstLetter(sortedItems[end - 1]);
while (end < sortedItems.length && firstLetter(sortedItems[end]) === last) {
const last = firstLetter(expectDefined(sortedItems[end - 1], "bucket end predecessor"));
while (
end < sortedItems.length &&
firstLetter(expectDefined(sortedItems[end], "bucket extension index")) === last
) {
end += 1;
}
}
const startLetter = firstLetter(sortedItems[start]);
const endLetter = firstLetter(sortedItems[end - 1]);
const startLetter = firstLetter(expectDefined(sortedItems[start], "bucket start index"));
const endLetter = firstLetter(expectDefined(sortedItems[end - 1], "bucket end predecessor"));
const id = startLetter === endLetter ? startLetter : `${startLetter}-${endLetter}`;
const label =
startLetter === endLetter
@@ -477,9 +480,12 @@ function resolveBucket(
return null;
}
if (!id) {
return buckets[0];
return expectDefined(buckets.at(0), "non-empty model picker buckets");
}
return buckets.find((bucket) => bucket.id === id) ?? buckets[0];
return (
buckets.find((bucket) => bucket.id === id) ??
expectDefined(buckets.at(0), "non-empty model picker buckets")
);
}
/**
@@ -103,10 +103,14 @@ function parseCurrentModelRef(raw?: string): DiscordModelPickerCurrentModelRef |
if (!match) {
return null;
}
const provider = normalizeProviderId(match[1]);
const providerText = match[1];
const model = match[2];
if (providerText === undefined || model === undefined) {
return null;
}
const provider = normalizeProviderId(providerText);
// Preserve the model suffix exactly as entered after "/" so select defaults
// continue to mirror the stored ref for Discord interactions.
const model = match[2];
if (!provider || !model) {
return null;
}
@@ -938,8 +942,7 @@ export function renderDiscordModelPickerRecentsView(
);
// Recent model buttons — slot 2+.
for (let i = 0; i < dedupedQuickModels.length; i++) {
const modelRef = dedupedQuickModels[i];
for (const [i, modelRef] of dedupedQuickModels.entries()) {
rows.push(
new Row([
createModelPickerButton({
@@ -1,6 +1,7 @@
// Discord plugin module implements thread bindings.lifecycle behavior.
import { readAcpSessionEntry, type AcpSessionStoreEntry } from "openclaw/plugin-sdk/acp-runtime";
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
// Discord plugin module implements thread bindings.lifecycle behavior.
import { expectDefined } from "openclaw/plugin-sdk/expect-runtime";
import {
normalizeOptionalLowercaseString,
normalizeOptionalString,
@@ -71,13 +72,16 @@ async function mapWithConcurrency<TItem, TResult>(params: {
if (index >= params.items.length) {
return;
}
resultsByIndex.set(index, await params.worker(params.items[index], index));
const item = expectDefined(params.items[index], "bounded worker item index");
resultsByIndex.set(index, await params.worker(item, index));
}
};
const workers = Array.from({ length: Math.min(limit, params.items.length) }, () => runWorker());
await Promise.all(workers);
return params.items.map((_item, index) => resultsByIndex.get(index)!);
return params.items.map((_item, index) =>
expectDefined(resultsByIndex.get(index), "completed bounded worker result"),
);
}
export function listThreadBindingsForAccount(accountId?: string): ThreadBindingRecord[] {
+4 -2
View File
@@ -1,8 +1,9 @@
// Discord plugin module implements outbound payload behavior.
import {
attachChannelToResult,
type ChannelOutboundAdapter,
} from "openclaw/plugin-sdk/channel-send-result";
// Discord plugin module implements outbound payload behavior.
import { expectDefined } from "openclaw/plugin-sdk/expect-runtime";
import {
getReplyPayloadTtsSupplement,
resolvePayloadMediaUrls,
@@ -101,7 +102,8 @@ export async function sendDiscordOutboundPayload(params: {
let deliveredVoice = false;
let lastResult: Awaited<ReturnType<DiscordPayloadSendContext["send"]>>;
try {
lastResult = await sendContext.sendVoice(sendContext.target, mediaUrls[0], {
const voiceUrl = expectDefined(mediaUrls.at(0), "non-empty Discord voice media URLs");
lastResult = await sendContext.sendVoice(sendContext.target, voiceUrl, {
...resolveDiscordDeliveryOptions(ctx, sendContext, voiceReply),
});
deliveredVoice = true;
+4 -2
View File
@@ -1,8 +1,9 @@
// Discord plugin module implements send.shared behavior.
import { PollLayoutType } from "discord-api-types/payloads/v10";
import type { RESTAPIPoll } from "discord-api-types/rest/v10";
import type { APIChannel } from "discord-api-types/v10";
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
// Discord plugin module implements send.shared behavior.
import { expectDefined } from "openclaw/plugin-sdk/expect-runtime";
import { buildOutboundMediaLoadOptions } from "openclaw/plugin-sdk/media-runtime";
import { extensionForMime } from "openclaw/plugin-sdk/media-runtime";
import {
@@ -378,7 +379,8 @@ async function sendDiscordText(params: DiscordTextSendParams) {
return { result, replyToId: chunkReplyTo };
};
if (chunks.length === 1) {
const { result, replyToId } = await sendChunk(chunks[0], true);
const chunk = expectDefined(chunks.at(0), "single Discord text chunk");
const { result, replyToId } = await sendChunk(chunk, true);
await onResult?.(result, "text", replyToId);
return { ...result, platformMessageIds: result.id ? [result.id] : [] };
}
+2 -1
View File
@@ -14,6 +14,7 @@ import crypto from "node:crypto";
import fs from "node:fs/promises";
import path from "node:path";
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
import { expectDefined } from "openclaw/plugin-sdk/expect-runtime";
import {
parseFfprobeCodecAndSampleRate,
runFfmpeg,
@@ -168,7 +169,7 @@ async function generateWaveformFromPcm(filePath: string): Promise<string> {
let sum = 0;
let count = 0;
for (let j = 0; j < step && i * step + j < samples.length; j++) {
sum += Math.abs(samples[i * step + j]);
sum += Math.abs(expectDefined(samples.at(i * step + j), "bounded PCM waveform sample"));
count++;
}
const avg = count > 0 ? sum / count : 0;
+22 -8
View File
@@ -1,6 +1,7 @@
// Discord plugin module implements manager behavior.
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
import type { DiscordAccountConfig } from "openclaw/plugin-sdk/config-contracts";
// Discord plugin module implements manager behavior.
import { expectDefined } from "openclaw/plugin-sdk/expect-runtime";
import { resolveAgentRoute } from "openclaw/plugin-sdk/routing";
import { createSubsystemLogger } from "openclaw/plugin-sdk/runtime-env";
import type { RuntimeEnv } from "openclaw/plugin-sdk/runtime-env";
@@ -1225,7 +1226,10 @@ export class DiscordVoiceManager {
if (this.botUserId && remainingLookups === 1) {
break;
}
const guildId = guildIds[(start + offset) % guildIds.length];
const guildId = expectDefined(
guildIds[(start + offset) % guildIds.length],
"voice reconciliation guild index",
);
const userLimit = this.resolveFollowUserReconcileUserLookupLimit(
followedUserIds.length,
remainingLookups,
@@ -1269,7 +1273,10 @@ export class DiscordVoiceManager {
let scanned = 0;
let assigned = 0;
for (; scanned < guildIds.length && assigned < remainingLookups; scanned += 1) {
const guildId = guildIds[(start + scanned) % guildIds.length];
const guildId = expectDefined(
guildIds[(start + scanned) % guildIds.length],
"bot voice reconciliation guild index",
);
const plan = plansByGuild.get(guildId);
if (!plan?.checkedAllUsers) {
continue;
@@ -1301,10 +1308,15 @@ export class DiscordVoiceManager {
return { userIds: followedUserIds, completedCycle: true };
}
const start = this.followUsersReconcileUserCursors.get(guildId) ?? 0;
const selected = Array.from(
{ length: limit },
(_, offset) => followedUserIds[(start + offset) % followedUserIds.length],
);
const selected: string[] = [];
for (let offset = 0; offset < limit; offset += 1) {
selected.push(
expectDefined(
followedUserIds[(start + offset) % followedUserIds.length],
"followed user selection index",
),
);
}
const completedCycle = start + selected.length >= followedUserIds.length;
this.followUsersReconcileUserCursors.set(
guildId,
@@ -1464,7 +1476,9 @@ export class DiscordVoiceManager {
return null;
}
const guildAllowed = this.allowedChannels.filter((entry) => entry.guildId === guildId);
return guildAllowed.length === 1 ? guildAllowed[0] : null;
return guildAllowed.length === 1
? expectDefined(guildAllowed.at(0), "single allowed guild voice channel")
: null;
}
private enqueueProcessing(entry: VoiceSessionEntry, task: () => Promise<void>) {
@@ -93,13 +93,12 @@ async function extractPdfContent(
try {
const images: DocumentExtractedImage[] = [];
let remainingPixels = request.maxPixels;
for (let index = 0; index < imagePages.length; index += 1) {
for (const [index, pageNumber] of imagePages.entries()) {
if (remainingPixels <= 0) {
break;
}
const pagesRemaining = imagePages.length - index;
const maxPixelsPerPage = Math.max(1, Math.ceil(remainingPixels / pagesRemaining));
const pageNumber = imagePages[index];
const imageResult = await pdf.extract({
mode: "images",
pages: [pageNumber],
+9 -3
View File
@@ -82,13 +82,19 @@ function parseBitableUrl(url: string): { token: string; tableId?: string; isWiki
// Wiki format: /wiki/XXXXX?table=YYY
const wikiMatch = u.pathname.match(/\/wiki\/([A-Za-z0-9]+)/);
if (wikiMatch) {
return { token: wikiMatch[1], tableId, isWiki: true };
const wikiPathSegment = wikiMatch[1];
return wikiPathSegment === undefined
? null
: { token: wikiPathSegment, tableId, isWiki: true };
}
// Base format: /base/XXXXX?table=YYY
const baseMatch = u.pathname.match(/\/base\/([A-Za-z0-9]+)/);
if (baseMatch) {
return { token: baseMatch[1], tableId, isWiki: false };
const basePathSegment = baseMatch[1];
return basePathSegment === undefined
? null
: { token: basePathSegment, tableId, isWiki: false };
}
return null;
@@ -407,7 +413,7 @@ async function createApp(
path: { app_token: appToken },
});
if (tablesRes.code === 0 && tablesRes.data?.items && tablesRes.data.items.length > 0) {
tableId = tablesRes.data.items[0].table_id ?? undefined;
tableId = tablesRes.data.items.at(0)?.table_id;
if (tableId) {
const cleanup = await cleanupNewBitable(client, appToken, tableId, name, log);
cleanedRows = cleanup.cleanedRows;
+7 -3
View File
@@ -1736,10 +1736,14 @@ export async function handleFeishuMessage(params: {
}
} else {
const results = await Promise.allSettled(broadcastAgents.map(dispatchForAgent));
for (let i = 0; i < results.length; i++) {
if (results[i].status === "rejected") {
for (const [i, result] of results.entries()) {
if (result.status === "rejected") {
const agentId = broadcastAgents.at(i);
if (agentId === undefined) {
continue;
}
log(
`feishu[${account.accountId}]: broadcast dispatch failed for agent=${broadcastAgents[i]}: ${String((results[i] as PromiseRejectedResult).reason)}`,
`feishu[${account.accountId}]: broadcast dispatch failed for agent=${agentId}: ${String(result.reason)}`,
);
}
}
+9
View File
@@ -124,6 +124,9 @@ export function parseFeishuConversationId(params: {
const topicSenderMatch = conversationId.match(/^(.+):topic:([^:]+):sender:([^:]+)$/i);
if (topicSenderMatch) {
const [, chatId, topicId, senderOpenId] = topicSenderMatch;
if (chatId === undefined || topicId === undefined || senderOpenId === undefined) {
return null;
}
return {
canonicalConversationId: buildFeishuConversationId({
chatId,
@@ -141,6 +144,9 @@ export function parseFeishuConversationId(params: {
const topicMatch = conversationId.match(/^(.+):topic:([^:]+)$/i);
if (topicMatch) {
const [, chatId, topicId] = topicMatch;
if (chatId === undefined || topicId === undefined) {
return null;
}
return {
canonicalConversationId: buildFeishuConversationId({
chatId,
@@ -156,6 +162,9 @@ export function parseFeishuConversationId(params: {
const senderMatch = conversationId.match(/^(.+):sender:([^:]+)$/i);
if (senderMatch) {
const [, chatId, senderOpenId] = senderMatch;
if (chatId === undefined || senderOpenId === undefined) {
return null;
}
return {
canonicalConversationId: buildFeishuConversationId({
chatId,
+1 -2
View File
@@ -197,8 +197,7 @@ export async function insertBlocksInBatches(
// When startIndex == -1 (append to end), each batch appends after the previous.
// When startIndex >= 0, each batch starts at startIndex + count of first-level IDs already inserted.
let currentIndex = startIndex;
for (let i = 0; i < batches.length; i++) {
const batch = batches[i];
for (const [i, batch] of batches.entries()) {
logger?.info?.(
`feishu_doc: Inserting batch ${i + 1}/${batches.length} (${batch.blocks.length} blocks)...`,
);
+3
View File
@@ -89,6 +89,9 @@ function parseColorMarkup(content: string): Segment[] {
// Tagged segment
const tagStr = normalizeLowercaseStringOrEmpty(match[1]);
const text = match[2];
if (text === undefined) {
continue;
}
const tags = tagStr.split(/\s+/);
const segment: Segment = { text };
+7 -3
View File
@@ -126,7 +126,7 @@ function calculateAdaptiveColumnWidths(blocks: FeishuDocxBlock[], tableBlockId:
if (cellId) {
const content = getCellText(cellId);
const length = getWeightedLength(content);
maxLengths[col] = Math.max(maxLengths[col], length);
maxLengths[col] = Math.max(maxLengths[col] ?? 0, length);
}
}
}
@@ -168,8 +168,12 @@ function calculateAdaptiveColumnWidths(blocks: FeishuDocxBlock[], tableBlockId:
}
for (const i of growable) {
const add = Math.min(perColumn, MAX_COLUMN_WIDTH - widths[i]);
widths[i] += add;
const width = widths[i];
if (width === undefined) {
continue;
}
const add = Math.min(perColumn, MAX_COLUMN_WIDTH - width);
widths[i] = width + add;
remaining -= add;
}
}
+6 -2
View File
@@ -54,7 +54,11 @@ function extractImageUrls(markdown: string): string[] {
const urls: string[] = [];
let match;
while ((match = regex.exec(markdown)) !== null) {
const url = match[1].trim();
const capturedUrl = match[1];
if (capturedUrl === undefined) {
continue;
}
const url = capturedUrl.trim();
if (url.startsWith("http://") || url.startsWith("https://")) {
urls.push(url);
}
@@ -669,7 +673,7 @@ async function processImages(
for (let i = 0; i < Math.min(imageUrls.length, imageBlocks.length); i++) {
const url = imageUrls[i];
const blockId = imageBlocks[i]?.block_id;
if (!blockId) {
if (!url || !blockId) {
continue;
}
@@ -31,12 +31,13 @@ async function retryBotIdentityProbe(
const log = runtime?.log ?? console.log;
const error = runtime?.error ?? console.error;
for (let i = 0; i < BOT_IDENTITY_RETRY_DELAYS_MS.length; i += 1) {
const nextDelays = BOT_IDENTITY_RETRY_DELAYS_MS.slice(1)[Symbol.iterator]();
for (const [i, delayMs] of BOT_IDENTITY_RETRY_DELAYS_MS.entries()) {
if (abortSignal?.aborted) {
return;
}
const delayElapsed = await waitForAbortableDelay(BOT_IDENTITY_RETRY_DELAYS_MS[i], abortSignal);
const delayElapsed = await waitForAbortableDelay(delayMs, abortSignal);
if (!delayElapsed) {
return;
}
@@ -50,7 +51,8 @@ async function retryBotIdentityProbe(
return;
}
const nextDelay = BOT_IDENTITY_RETRY_DELAYS_MS[i + 1];
const nextDelayResult = nextDelays.next();
const nextDelay = nextDelayResult.done ? undefined : nextDelayResult.value;
error(
`feishu[${accountId}]: bot identity background retry ${i + 1}/${BOT_IDENTITY_RETRY_DELAYS_MS.length} failed` +
(nextDelay ? `; next attempt in ${nextDelay / 1000}s` : ""),
@@ -138,8 +138,7 @@ function resolveFeishuDebounceMentions(params: {
if (entries.length === 0) {
return undefined;
}
for (let index = entries.length - 1; index >= 0; index -= 1) {
const entry = entries[index];
for (const entry of entries.toReversed()) {
if (isMentionForwardRequest(entry, botOpenId)) {
return mergeFeishuDebounceMentions([entry]);
}
+16 -4
View File
@@ -56,7 +56,10 @@ function resolveFeishuRequesterConversation(params: {
if (requesterSessionKey) {
const existingBindings = manager.listBySessionKey(requesterSessionKey);
if (existingBindings.length === 1) {
const existing = existingBindings[0];
const existing = existingBindings.at(0);
if (existing === undefined) {
return null;
}
return {
accountId: existing.accountId,
conversationId: existing.conversationId,
@@ -72,7 +75,10 @@ function resolveFeishuRequesterConversation(params: {
!entry.parentConversationId,
);
if (directMatches.length === 1) {
const existing = directMatches[0];
const existing = directMatches.at(0);
if (existing === undefined) {
return null;
}
return {
accountId: existing.accountId,
conversationId: existing.conversationId,
@@ -93,7 +99,10 @@ function resolveFeishuRequesterConversation(params: {
);
});
if (matchingTopicBindings.length === 1) {
const existing = matchingTopicBindings[0];
const existing = matchingTopicBindings.at(0);
if (existing === undefined) {
return null;
}
return {
accountId: existing.accountId,
conversationId: existing.conversationId,
@@ -111,7 +120,10 @@ function resolveFeishuRequesterConversation(params: {
senderScopedTopicBindings.length === 1 &&
matchingTopicBindings.length === senderScopedTopicBindings.length
) {
const existing = senderScopedTopicBindings[0];
const existing = senderScopedTopicBindings.at(0);
if (existing === undefined) {
return null;
}
return {
accountId: existing.accountId,
conversationId: existing.conversationId,
@@ -113,7 +113,7 @@ async function preflightDu(dirPath: string, maxBytes: number): Promise<boolean>
finish(true);
return;
}
const sizeKb = Number.parseInt(match[1], 10);
const sizeKb = Number.parseInt(match[0], 10);
finish(sizeKb <= heuristicKb);
});
du.on("error", () => {
+11 -5
View File
@@ -367,12 +367,18 @@ export async function persistAllowAlways(input: {
);
// Use hasOwnProperty so a node with displayName "constructor" doesn't
// accidentally hit Object.prototype.constructor and pretend to match.
let key = candidates.find((c) => Object.hasOwn(fileTransfer, c));
if (!key) {
key = assertSafeConfigKey(input.nodeDisplayName ?? input.nodeId);
fileTransfer[key] = {};
let entry: NodeFilePolicyConfig | undefined;
for (const candidate of candidates) {
entry = Object.entries(fileTransfer).find(([key]) => key === candidate)?.[1];
if (entry) {
break;
}
}
if (!entry) {
const key = assertSafeConfigKey(input.nodeDisplayName ?? input.nodeId);
entry = {};
fileTransfer[key] = entry;
}
const entry = fileTransfer[key];
const list = Array.isArray(entry[field]) ? entry[field] : [];
if (!list.includes(input.path)) {
list.push(input.path);
@@ -257,9 +257,14 @@ async function preValidateTarball(
};
}
for (let i = 0; i < paths.length; i++) {
const entryPath = paths[i];
const t = typeChars[i];
for (const [index, entryPath] of paths.entries()) {
const t = typeChars.at(index);
if (t === undefined) {
return {
ok: false,
reason: `tar -tzf and tar -tzvf disagree on entry count (${paths.length} vs ${typeChars.length}); refusing`,
};
}
if (t === "l" || t === "h") {
return { ok: false, reason: `archive contains link entry: ${entryPath}` };
}
+3 -2
View File
@@ -154,8 +154,9 @@ function pickBestModel(available: string[], userModel?: string): string {
return preferred;
}
}
if (available.length > 0) {
return available[0];
const [firstAvailable] = available;
if (firstAvailable) {
return firstAvailable;
}
throw new Error("No embedding models available from GitHub Copilot");
}
+19 -10
View File
@@ -37,7 +37,11 @@ import {
} from "./replay-policy.js";
import { wrapCopilotProviderStream } from "./stream.js";
const COPILOT_ENV_VARS = ["COPILOT_GITHUB_TOKEN", "GH_TOKEN", "GITHUB_TOKEN"];
const COPILOT_ENV_VARS: [string, string, string] = [
"COPILOT_GITHUB_TOKEN",
"GH_TOKEN",
"GITHUB_TOKEN",
];
const DEFAULT_COPILOT_MODEL = "github-copilot/claude-opus-4.7";
const DEFAULT_COPILOT_PROFILE_ID = "github-copilot:github";
@@ -165,25 +169,30 @@ function applyGithubCopilotDomainToConfig(
const models = config.models ?? {};
const providers = models.providers ?? {};
const provider = providers[PROVIDER_ID] ?? {};
const params = { ...provider.params } as Record<string, unknown>;
const provider = providers[PROVIDER_ID];
const params: Record<string, unknown> = {};
if (provider?.params) {
Object.assign(params, provider.params);
}
if (isEnterprise) {
params.githubDomain = domain;
} else {
delete params.githubDomain;
}
const nextProviders = { ...providers };
if (provider) {
nextProviders[PROVIDER_ID] = { ...provider, params };
} else {
// Source config accepts partial provider inputs; catalog materialization
// supplies baseUrl/models before runtime consumption.
Object.assign(nextProviders, { [PROVIDER_ID]: { params } });
}
return {
...config,
models: {
...models,
providers: {
...providers,
[PROVIDER_ID]: {
...provider,
params,
},
},
providers: nextProviders,
},
};
}
+11 -6
View File
@@ -368,8 +368,8 @@ export function withGithubCopilotDomainConfig(cfg: OpenClawConfig, domain: strin
// `T | undefined`, which exactOptionalPropertyTypes rejects.
const models: NonNullable<OpenClawConfig["models"]> = cfg.models ?? {};
const providers: NonNullable<typeof models.providers> = models.providers ?? {};
const provider: NonNullable<(typeof providers)[string]> = providers["github-copilot"] ?? {};
const params = provider.params;
const provider = providers["github-copilot"];
const params = provider?.params;
const isDefault = domain === PUBLIC_GITHUB_COPILOT_DOMAIN;
if (isDefault && !(params && "githubDomain" in params)) {
return cfg;
@@ -380,14 +380,19 @@ export function withGithubCopilotDomainConfig(cfg: OpenClawConfig, domain: strin
} else {
nextParams.githubDomain = domain;
}
const nextProviders = { ...providers };
if (provider) {
nextProviders["github-copilot"] = { ...provider, params: nextParams };
} else {
// Source config accepts partial provider inputs; catalog materialization
// supplies baseUrl/models before runtime consumption.
Object.assign(nextProviders, { "github-copilot": { params: nextParams } });
}
return {
...cfg,
models: {
...models,
providers: {
...providers,
"github-copilot": { ...provider, params: nextParams },
},
providers: nextProviders,
},
};
}
+6 -1
View File
@@ -5,6 +5,7 @@ import { createInterface } from "node:readline/promises";
import { format } from "node:util";
import type { Command } from "commander";
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
import { expectDefined } from "openclaw/plugin-sdk/expect-runtime";
import { callGatewayFromCli } from "openclaw/plugin-sdk/gateway-runtime";
import {
clampTimerTimeoutMs,
@@ -1317,7 +1318,11 @@ const CRC32_TABLE = new Uint32Array(
function crc32(buffer: Buffer): number {
let value = 0xffffffff;
for (const byte of buffer) {
value = CRC32_TABLE[(value ^ byte) & 0xff] ^ (value >>> 8);
const tableValue = expectDefined(
CRC32_TABLE.at((value ^ byte) & 0xff),
"CRC32 lookup table entry",
);
value = tableValue ^ (value >>> 8);
}
return (value ^ 0xffffffff) >>> 0;
}
@@ -131,34 +131,35 @@ export async function resolveChromeNodeInfo(params: {
if (requested) {
const list = await listGoogleMeetNodes(params.runtime);
const matches = list.nodes.filter((node) => matchesRequestedNode(node, requested));
if (matches.length === 1) {
const [node] = matches;
if (isGoogleMeetNode(node)) {
return node;
}
throw new Error(
`Configured Google Meet node ${requested} is not usable (${formatNodeLabel(node)}): ${describeNodeUsabilityIssues(node).join("; ")}. Start or reinstall \`openclaw node run\` on that Chrome host, approve pairing, and allow googlemeet.chrome plus browser.proxy.`,
);
}
if (matches.length > 1) {
throw new Error(
`Configured Google Meet node ${requested} is ambiguous (${matches.length} matches). Pin chromeNode.node to a unique node id, display name, or remote IP.`,
);
}
const [node] = matches;
if (!node) {
throw new Error(
`Configured Google Meet node ${requested} was not found. Run \`openclaw nodes status\` and start or approve the Chrome node.`,
);
}
if (isGoogleMeetNode(node)) {
return node;
}
throw new Error(
`Configured Google Meet node ${requested} was not found. Run \`openclaw nodes status\` and start or approve the Chrome node.`,
`Configured Google Meet node ${requested} is not usable (${formatNodeLabel(node)}): ${describeNodeUsabilityIssues(node).join("; ")}. Start or reinstall \`openclaw node run\` on that Chrome host, approve pairing, and allow googlemeet.chrome plus browser.proxy.`,
);
}
const list = await listGoogleMeetNodes(params.runtime, { connected: true });
const nodes = list.nodes.filter(isGoogleMeetNode);
if (nodes.length === 0) {
const [node] = nodes;
if (!node) {
throw new Error(
"No connected Google Meet-capable node with browser proxy. Run `openclaw node run` on the Chrome host with browser proxy enabled, approve pairing, and allow googlemeet.chrome plus browser.proxy.",
);
}
if (nodes.length === 1) {
return nodes[0];
return node;
}
throw new Error(
"Multiple Google Meet-capable nodes connected. Set plugins.entries.google-meet.config.chromeNode.node.",
@@ -13,8 +13,7 @@ export const GOOGLE_MAX_INPUT_IMAGES = 10;
export const DEFAULT_GOOGLE_VIDEO_MODEL = "veo-3.1-fast-generate-preview";
export const GOOGLE_VIDEO_ALLOWED_DURATION_SECONDS = [4, 6, 8] as const;
export const GOOGLE_VIDEO_MIN_DURATION_SECONDS = GOOGLE_VIDEO_ALLOWED_DURATION_SECONDS[0];
export const GOOGLE_VIDEO_MAX_DURATION_SECONDS =
GOOGLE_VIDEO_ALLOWED_DURATION_SECONDS[GOOGLE_VIDEO_ALLOWED_DURATION_SECONDS.length - 1];
export const GOOGLE_VIDEO_MAX_DURATION_SECONDS = GOOGLE_VIDEO_ALLOWED_DURATION_SECONDS[2];
function isGoogleProviderConfigured(
ctx: { agentDir?: string } | VideoGenerationProviderConfiguredContext,
+2 -2
View File
@@ -266,8 +266,8 @@ async function processMessageWithPipeline(params: {
let mediaPath: string | undefined;
let mediaType: string | undefined;
if (attachments.length > 0) {
const first = attachments[0];
const first = attachments.at(0);
if (first) {
const attachmentData = await downloadAttachment(first, account, mediaMaxMb, core);
if (attachmentData) {
mediaPath = attachmentData.path;
+5 -3
View File
@@ -1,4 +1,3 @@
// Imessage plugin module implements accounts behavior.
import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/account-id";
import {
createAccountListHelpers,
@@ -6,6 +5,8 @@ import {
resolveMergedAccountConfig,
type OpenClawConfig,
} from "openclaw/plugin-sdk/account-resolution";
// Imessage plugin module implements accounts behavior.
import { expectDefined } from "openclaw/plugin-sdk/expect-runtime";
import { resolveAccountEntry } from "openclaw/plugin-sdk/routing";
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
import type { IMessageAccountConfig } from "./account-types.js";
@@ -219,11 +220,12 @@ export function collectIMessageDuplicateAccountSourceWarnings(params: {
if (collisions.length < 2) {
continue;
}
const firstCollision = expectDefined(collisions[0], "duplicate iMessage account source");
const ownerId = resolveIMessageAccountSourceOwner({
cfg: params.cfg,
signature: resolveIMessageAccountSourceSignature(collisions[0]),
signature: resolveIMessageAccountSourceSignature(firstCollision),
});
const owner = collisions.find((a) => a.accountId === ownerId) ?? collisions[0];
const owner = collisions.find((a) => a.accountId === ownerId) ?? firstCollision;
const duplicates = collisions.filter((a) => a.accountId !== owner.accountId);
const dupIds = duplicates.map((a) => `"${a.accountId}"`).join(", ");
const cliPath = normalizeIMessageCliPath(owner.config.cliPath);
+14 -4
View File
@@ -414,10 +414,16 @@ function visibleApprovalBindingMatches(
const visibleDecisions: ExecApprovalReplyDecision[] = [];
for (const line of lines) {
const match = line.match(APPROVE_COMMAND_LINE_RE);
if (!match || (match[1] !== binding.approvalId && match[1] !== binding.approvalSlug)) {
const approvalId = match?.[1];
const decisionsText = match?.[2];
if (
!approvalId ||
!decisionsText ||
(approvalId !== binding.approvalId && approvalId !== binding.approvalSlug)
) {
continue;
}
for (const token of match[2].split(/[\s|,]+/)) {
for (const token of decisionsText.split(/[\s|,]+/)) {
const decision = normalizeApprovalDecision(token);
if (decision && !visibleDecisions.includes(decision)) {
visibleDecisions.push(decision);
@@ -539,13 +545,17 @@ export function extractIMessageApprovalPromptBinding(text: string): {
return null;
}
const approvalId = idHeaderMatch[1];
if (!approvalId) {
return null;
}
const allowedDecisions: ExecApprovalReplyDecision[] = [];
for (const line of lines) {
const match = line.match(APPROVE_COMMAND_LINE_RE);
if (!match || match[1] !== approvalId) {
const decisionsText = match?.[2];
if (!match || match[1] !== approvalId || !decisionsText) {
continue;
}
const decisions = match[2].split(/[\s|,]+/);
const decisions = decisionsText.split(/[\s|,]+/);
for (const decisionText of decisions) {
const decision = normalizeApprovalDecision(decisionText);
if (decision && !allowedDecisions.includes(decision)) {
+1 -2
View File
@@ -233,8 +233,7 @@ export function capFailureRetriesMap(
// debugging).
entries.sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]));
const capped: Record<string, number> = {};
for (let i = 0; i < entries.length && i < maxSize; i++) {
const [guid, count] = entries[i];
for (const [guid, count] of entries.slice(0, maxSize)) {
capped[guid] = count;
if (textEncoder.encode(JSON.stringify(capped)).byteLength > maxBytes) {
delete capped[guid];
+4 -3
View File
@@ -1,4 +1,5 @@
// Imessage plugin module implements the same-sender inbound debounce merge.
import { expectDefined } from "openclaw/plugin-sdk/expect-runtime";
import { sliceUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
import type { IMessagePayload } from "./types.js";
@@ -104,12 +105,12 @@ export function combineIMessagePayloads(payloads: IMessagePayload[]): CoalescedI
if (payloads.length === 0) {
throw new Error("combineIMessagePayloads: cannot combine empty payloads");
}
const first = expectDefined(payloads[0], "first iMessage payload to coalesce");
if (payloads.length === 1) {
return payloads[0];
return first;
}
const first = payloads[0];
const last = payloads[payloads.length - 1];
const last = expectDefined(payloads.at(-1), "last iMessage payload to coalesce");
// Cap entries: keep first (preserves command/context) + most recent
// (preserves latest payload) when a flood exceeds the cap.
@@ -24,6 +24,7 @@ import {
upsertChannelPairingRequest,
} from "openclaw/plugin-sdk/conversation-runtime";
import { recordInboundSession } from "openclaw/plugin-sdk/conversation-runtime";
import { expectDefined } from "openclaw/plugin-sdk/expect-runtime";
import { normalizeScpRemoteHost } from "openclaw/plugin-sdk/host-runtime";
import { isInboundPathAllowed, kindFromMime } from "openclaw/plugin-sdk/media-runtime";
import { DEFAULT_GROUP_HISTORY_LIMIT, type HistoryEntry } from "openclaw/plugin-sdk/reply-history";
@@ -772,7 +773,10 @@ export async function monitorIMessageProvider(opts: MonitorIMessageOpts = {}): P
};
if (entries.length === 1) {
await dispatchUnit(entries, entries[0].message);
await dispatchUnit(
entries,
expectDefined(entries[0], "single iMessage dispatch entry").message,
);
return;
}
@@ -1,5 +1,6 @@
// Imessage plugin module implements self chat cache behavior.
import { createHash } from "node:crypto";
import { expectDefined } from "openclaw/plugin-sdk/expect-runtime";
import { formatIMessageChatTarget } from "../targets.js";
type SelfChatCacheKeyParts = {
@@ -134,7 +135,10 @@ class DefaultSelfChatCache implements SelfChatCache {
this.entryCount > MAX_SELF_CHAT_CACHE_ENTRIES &&
this.insertionOrderOffset < this.insertionOrder.length
) {
const oldest = this.insertionOrder[this.insertionOrderOffset];
const oldest = expectDefined(
this.insertionOrder[this.insertionOrderOffset],
"oldest iMessage self-chat cache entry",
);
this.insertionOrderOffset += 1;
const entries = this.cache.get(oldest.key);
if (!entries) {
@@ -13,6 +13,9 @@ function readVarint(buf: Uint8Array, start: number): Varint | null {
while (offset < buf.length && shift <= 28) {
const byte = buf[offset];
if (byte === undefined) {
return null;
}
offset += 1;
value |= (byte & 0x7f) << shift;
if ((byte & 0x80) === 0) {
+5 -3
View File
@@ -1,5 +1,6 @@
// Line plugin module implements card command behavior.
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core";
// Line plugin module implements card command behavior.
import { expectDefined } from "openclaw/plugin-sdk/expect-runtime";
import type { ReplyPayload } from "openclaw/plugin-sdk/reply-runtime";
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/string-coerce-runtime";
import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
@@ -144,13 +145,14 @@ function parseCardArgs(argsStrInput: string): {
const quotedRegex = /"([^"]*?)"/g;
let match;
while ((match = quotedRegex.exec(argsStr)) !== null) {
result.args.push(match[1]);
result.args.push(expectDefined(match[1], "quoted card argument capture"));
}
// Extract flags (--key value or --key "value")
const flagRegex = /--(\w+)\s+(?:"([^"]*?)"|(\S+))/g;
while ((match = flagRegex.exec(argsStr)) !== null) {
result.flags[match[1]] = match[2] ?? match[3];
const key = expectDefined(match[1], "card flag name capture");
result.flags[key] = expectDefined(match[2] ?? match[3], "card flag value capture");
}
return result;
@@ -511,8 +511,7 @@ export function createDeviceControlCard(params: {
for (let i = 0; i < limitedControls.length; i += 2) {
const rowButtons: FlexComponent[] = [];
for (let j = i; j < Math.min(i + 2, limitedControls.length); j++) {
const ctrl = limitedControls[j];
for (const [offset, ctrl] of limitedControls.slice(i, i + 2).entries()) {
const buttonLabel = ctrl.icon ? `${ctrl.icon} ${ctrl.label}` : ctrl.label;
rowButtons.push({
@@ -525,7 +524,7 @@ export function createDeviceControlCard(params: {
style: ctrl.style ?? "secondary",
flex: 1,
height: "sm",
margin: j > i ? "md" : undefined,
margin: offset > 0 ? "md" : undefined,
} as FlexButton);
}
+10 -11
View File
@@ -1,5 +1,6 @@
// Line plugin module implements markdown to line behavior.
import type { messagingApi } from "@line/bot-sdk";
import { expectDefined } from "openclaw/plugin-sdk/expect-runtime";
import { stripMarkdown } from "openclaw/plugin-sdk/text-chunking";
import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
import { uriAction } from "./actions.js";
@@ -42,9 +43,9 @@ export function extractMarkdownTables(text: string): {
const matches: { fullMatch: string; table: MarkdownTable }[] = [];
while ((match = MARKDOWN_TABLE_REGEX.exec(text)) !== null) {
const fullMatch = match[0];
const headerLine = match[1];
const bodyLines = match[2];
const fullMatch = expectDefined(match[0], "Markdown table match");
const headerLine = expectDefined(match[1], "Markdown table header capture");
const bodyLines = expectDefined(match[2], "Markdown table body capture");
const headers = parseTableRow(headerLine);
const rows = bodyLines
@@ -62,8 +63,7 @@ export function extractMarkdownTables(text: string): {
}
// Remove tables from text in reverse order to preserve indices
for (let i = matches.length - 1; i >= 0; i--) {
const { fullMatch, table } = matches[i];
for (const { fullMatch, table } of matches.toReversed()) {
tables.unshift(table);
textWithoutTables = textWithoutTables.replace(fullMatch, "");
}
@@ -203,9 +203,9 @@ export function extractCodeBlocks(text: string): {
const matches: { fullMatch: string; block: CodeBlock }[] = [];
while ((match = MARKDOWN_CODE_BLOCK_REGEX.exec(text)) !== null) {
const fullMatch = match[0];
const fullMatch = expectDefined(match[0], "Markdown code block match");
const language = match[1] || undefined;
const code = match[2];
const code = expectDefined(match[2], "Markdown code body capture");
matches.push({
fullMatch,
@@ -214,8 +214,7 @@ export function extractCodeBlocks(text: string): {
}
// Remove code blocks in reverse order
for (let i = matches.length - 1; i >= 0; i--) {
const { fullMatch, block } = matches[i];
for (const { fullMatch, block } of matches.toReversed()) {
codeBlocks.unshift(block);
textWithoutCode = textWithoutCode.replace(fullMatch, "");
}
@@ -286,8 +285,8 @@ export function extractLinks(text: string): { links: MarkdownLink[]; textWithLin
let match: RegExpExecArray | null;
while ((match = MARKDOWN_LINK_REGEX.exec(text)) !== null) {
links.push({
text: match[1],
url: match[2],
text: expectDefined(match[1], "Markdown link text capture"),
url: expectDefined(match[2], "Markdown link URL capture"),
});
}
+3 -3
View File
@@ -237,11 +237,11 @@ export const lineOutboundAdapter: NonNullable<ChannelPlugin<ResolvedLineAccount>
}
if (chunks.length > 0) {
for (let i = 0; i < chunks.length; i += 1) {
for (const [i, chunk] of chunks.entries()) {
const isLast = i === chunks.length - 1;
if (isLast && hasQuickReplies) {
await recordResult(
sendQuickReplies(to, chunks[i], quickReplies, {
sendQuickReplies(to, chunk, quickReplies, {
verbose: false,
cfg,
accountId: accountId ?? undefined,
@@ -249,7 +249,7 @@ export const lineOutboundAdapter: NonNullable<ChannelPlugin<ResolvedLineAccount>
);
} else {
await recordResult(
sendText(to, chunks[i], {
sendText(to, chunk, {
verbose: false,
cfg,
accountId: accountId ?? undefined,
+19 -22
View File
@@ -1,6 +1,7 @@
// Line plugin module implements reply chunks behavior.
import type { messagingApi } from "@line/bot-sdk";
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
import { expectDefined } from "openclaw/plugin-sdk/expect-runtime";
type LineReplyMessage = messagingApi.TextMessage;
@@ -35,7 +36,7 @@ export type SendLineReplyChunksParams = {
export async function sendLineReplyChunks(
params: SendLineReplyChunksParams,
): Promise<{ replyTokenUsed: boolean }> {
const hasQuickReplies = Boolean(params.quickReplies?.length);
const quickReplies = params.quickReplies?.length ? params.quickReplies : undefined;
let replyTokenUsed = Boolean(params.replyTokenUsed);
if (params.chunks.length === 0) {
@@ -52,11 +53,11 @@ export async function sendLineReplyChunks(
text: chunk,
}));
if (hasQuickReplies && remaining.length === 0 && replyMessages.length > 0) {
if (quickReplies && remaining.length === 0 && replyMessages.length > 0) {
const lastIndex = replyMessages.length - 1;
replyMessages[lastIndex] = params.createTextMessageWithQuickReplies(
replyBatch[lastIndex],
params.quickReplies!,
expectDefined(replyBatch[lastIndex], "last non-empty LINE reply batch chunk"),
quickReplies,
);
}
@@ -66,17 +67,15 @@ export async function sendLineReplyChunks(
});
replyTokenUsed = true;
for (let i = 0; i < remaining.length; i += 1) {
for (const [i, chunk] of remaining.entries()) {
const isLastChunk = i === remaining.length - 1;
if (isLastChunk && hasQuickReplies) {
await params.pushTextMessageWithQuickReplies(
params.to,
remaining[i],
params.quickReplies!,
{ cfg: params.cfg, accountId: params.accountId },
);
if (isLastChunk && quickReplies) {
await params.pushTextMessageWithQuickReplies(params.to, chunk, quickReplies, {
cfg: params.cfg,
accountId: params.accountId,
});
} else {
await params.pushMessageLine(params.to, remaining[i], {
await params.pushMessageLine(params.to, chunk, {
cfg: params.cfg,
accountId: params.accountId,
});
@@ -90,17 +89,15 @@ export async function sendLineReplyChunks(
}
}
for (let i = 0; i < params.chunks.length; i += 1) {
for (const [i, chunk] of params.chunks.entries()) {
const isLastChunk = i === params.chunks.length - 1;
if (isLastChunk && hasQuickReplies) {
await params.pushTextMessageWithQuickReplies(
params.to,
params.chunks[i],
params.quickReplies!,
{ cfg: params.cfg, accountId: params.accountId },
);
if (isLastChunk && quickReplies) {
await params.pushTextMessageWithQuickReplies(params.to, chunk, quickReplies, {
cfg: params.cfg,
accountId: params.accountId,
});
} else {
await params.pushMessageLine(params.to, params.chunks[i], {
await params.pushMessageLine(params.to, chunk, {
cfg: params.cfg,
accountId: params.accountId,
});
+61 -28
View File
@@ -1,4 +1,5 @@
// Line plugin module implements reply payload transform behavior.
import { expectDefined } from "openclaw/plugin-sdk/expect-runtime";
import { parseStrictFiniteNumber } from "openclaw/plugin-sdk/number-runtime";
import type { ReplyPayload } from "openclaw/plugin-sdk/reply-runtime";
import {
@@ -51,10 +52,21 @@ export function parseLineDirectives(payload: ReplyPayload): ReplyPayload {
}
return base.join("&");
};
const parseConfirmAction = (part: string): { label: string; data: string } => {
const colonIndex = part.indexOf(":");
if (colonIndex === -1) {
return { label: part, data: normalizeLowercaseStringOrEmpty(part) };
}
return {
label: part.slice(0, colonIndex).trim(),
data: part.slice(colonIndex + 1).trim(),
};
};
const quickRepliesMatch = text.match(/\[\[quick_replies:\s*([^\]]+)\]\]/i);
if (quickRepliesMatch) {
const options = normalizeStringEntries(quickRepliesMatch[1].split(","));
const body = expectDefined(quickRepliesMatch[1], "quick replies directive body");
const options = normalizeStringEntries(body.split(","));
if (options.length > 0) {
lineData.quickReplies = [...(lineData.quickReplies || []), ...options];
}
@@ -63,9 +75,13 @@ export function parseLineDirectives(payload: ReplyPayload): ReplyPayload {
const locationMatch = text.match(/\[\[location:\s*([^\]]+)\]\]/i);
if (locationMatch && !lineData.location) {
const parts = locationMatch[1].split("|").map((s) => s.trim());
const body = expectDefined(locationMatch[1], "location directive body");
const parts = body.split("|").map((s) => s.trim());
if (parts.length >= 4) {
const [title, address, latStr, lonStr] = parts;
const title = expectDefined(parts[0], "location title field");
const address = expectDefined(parts[1], "location address field");
const latStr = expectDefined(parts[2], "location latitude field");
const lonStr = expectDefined(parts[3], "location longitude field");
const latitude = parseStrictFiniteNumber(latStr);
const longitude = parseStrictFiniteNumber(lonStr);
if (latitude !== undefined && longitude !== undefined) {
@@ -82,23 +98,22 @@ export function parseLineDirectives(payload: ReplyPayload): ReplyPayload {
const confirmMatch = text.match(/\[\[confirm:\s*([^\]]+)\]\]/i);
if (confirmMatch && !lineData.templateMessage) {
const parts = confirmMatch[1].split("|").map((s) => s.trim());
const body = expectDefined(confirmMatch[1], "confirm directive body");
const parts = body.split("|").map((s) => s.trim());
if (parts.length >= 3) {
const [question, yesPart, noPart] = parts;
const [yesLabel, yesData] = yesPart.includes(":")
? yesPart.split(":").map((s) => s.trim())
: [yesPart, normalizeLowercaseStringOrEmpty(yesPart)];
const [noLabel, noData] = noPart.includes(":")
? noPart.split(":").map((s) => s.trim())
: [noPart, normalizeLowercaseStringOrEmpty(noPart)];
const question = expectDefined(parts[0], "confirm question field");
const yesPart = expectDefined(parts[1], "confirm yes field");
const noPart = expectDefined(parts[2], "confirm no field");
const yesAction = parseConfirmAction(yesPart);
const noAction = parseConfirmAction(noPart);
lineData.templateMessage = {
type: "confirm",
text: question,
confirmLabel: yesLabel,
confirmData: yesData,
cancelLabel: noLabel,
cancelData: noData,
confirmLabel: yesAction.label,
confirmData: yesAction.data,
cancelLabel: noAction.label,
cancelData: noAction.data,
altText: question,
};
}
@@ -107,9 +122,12 @@ export function parseLineDirectives(payload: ReplyPayload): ReplyPayload {
const buttonsMatch = text.match(/\[\[buttons:\s*([^\]]+)\]\]/i);
if (buttonsMatch && !lineData.templateMessage) {
const parts = buttonsMatch[1].split("|").map((s) => s.trim());
const body = expectDefined(buttonsMatch[1], "buttons directive body");
const parts = body.split("|").map((s) => s.trim());
if (parts.length >= 3) {
const [title, bodyText, actionsStr] = parts;
const title = expectDefined(parts[0], "buttons title field");
const bodyText = expectDefined(parts[1], "buttons text field");
const actionsStr = expectDefined(parts[2], "buttons actions field");
const actions = actionsStr.split(",").map((actionStr) => {
const trimmed = actionStr.trim();
@@ -160,9 +178,11 @@ export function parseLineDirectives(payload: ReplyPayload): ReplyPayload {
const mediaPlayerMatch = text.match(/\[\[media_player:\s*([^\]]+)\]\]/i);
if (mediaPlayerMatch && !lineData.flexMessage) {
const parts = mediaPlayerMatch[1].split("|").map((s) => s.trim());
const body = expectDefined(mediaPlayerMatch[1], "media player directive body");
const parts = body.split("|").map((s) => s.trim());
if (parts.length >= 1) {
const [title, artist, source, imageUrl, statusStr] = parts;
const title = expectDefined(parts[0], "media player title field");
const [, artist, source, imageUrl, statusStr] = parts;
const isPlaying = normalizeLowercaseStringOrEmpty(statusStr) === "playing";
const validImageUrl = imageUrl?.startsWith("https://") ? imageUrl : undefined;
const deviceKey = toSlug(source || title || "media");
@@ -190,9 +210,14 @@ export function parseLineDirectives(payload: ReplyPayload): ReplyPayload {
const eventMatch = text.match(/\[\[event:\s*([^\]]+)\]\]/i);
if (eventMatch && !lineData.flexMessage) {
const parts = eventMatch[1].split("|").map((s) => s.trim());
const body = expectDefined(eventMatch[1], "event directive body");
const parts = body.split("|").map((s) => s.trim());
if (parts.length >= 2) {
const [title, date, time, location, description] = parts;
const title = expectDefined(parts[0], "event title field");
const date = expectDefined(parts[1], "event date field");
const time = parts[2];
const location = parts[3];
const description = parts[4];
const card = createEventCard({
title: title || "Event",
@@ -212,9 +237,11 @@ export function parseLineDirectives(payload: ReplyPayload): ReplyPayload {
const appleTvMatch = text.match(/\[\[appletv_remote:\s*([^\]]+)\]\]/i);
if (appleTvMatch && !lineData.flexMessage) {
const parts = appleTvMatch[1].split("|").map((s) => s.trim());
const body = expectDefined(appleTvMatch[1], "Apple TV directive body");
const parts = body.split("|").map((s) => s.trim());
if (parts.length >= 1) {
const [deviceName, status] = parts;
const deviceName = expectDefined(parts[0], "Apple TV device name field");
const [, status] = parts;
const deviceKey = toSlug(deviceName || "apple_tv");
const card = createAppleTvRemoteCard({
@@ -246,9 +273,11 @@ export function parseLineDirectives(payload: ReplyPayload): ReplyPayload {
const agendaMatch = text.match(/\[\[agenda:\s*([^\]]+)\]\]/i);
if (agendaMatch && !lineData.flexMessage) {
const parts = agendaMatch[1].split("|").map((s) => s.trim());
const body = expectDefined(agendaMatch[1], "agenda directive body");
const parts = body.split("|").map((s) => s.trim());
if (parts.length >= 2) {
const [title, eventsStr] = parts;
const title = expectDefined(parts[0], "agenda title field");
const eventsStr = expectDefined(parts[1], "agenda events field");
const events = eventsStr.split(",").map((eventStr) => {
const trimmed = eventStr.trim();
const colonIdx = trimmed.lastIndexOf(":");
@@ -276,13 +305,17 @@ export function parseLineDirectives(payload: ReplyPayload): ReplyPayload {
const deviceMatch = text.match(/\[\[device:\s*([^\]]+)\]\]/i);
if (deviceMatch && !lineData.flexMessage) {
const parts = deviceMatch[1].split("|").map((s) => s.trim());
const body = expectDefined(deviceMatch[1], "device directive body");
const parts = body.split("|").map((s) => s.trim());
if (parts.length >= 1) {
const [deviceName, deviceType, status, controlsStr] = parts;
const deviceName = expectDefined(parts[0], "device name field");
const [, deviceType, status, controlsStr] = parts;
const deviceKey = toSlug(deviceName || "device");
const controls = controlsStr
? controlsStr.split(",").map((ctrlStr) => {
const [label, data] = ctrlStr.split(":").map((s) => s.trim());
const controlParts = ctrlStr.split(":").map((s) => s.trim());
const label = expectDefined(controlParts[0], "device control label");
const data = controlParts[1];
const action = data || normalizeLowercaseStringOrEmpty(label).replace(/\s+/g, "_");
return { label, data: lineActionData(action, { "line.device": deviceKey }) };
})
+9 -5
View File
@@ -303,11 +303,15 @@ function normalizeConfiguredReasoningEffortMap(value: unknown): Record<string, s
if (!value || typeof value !== "object" || Array.isArray(value)) {
return undefined;
}
const normalized = Object.fromEntries(
Object.entries(value)
.map(([key, mapped]) => [key.trim(), typeof mapped === "string" ? mapped.trim() : ""])
.filter(([key, mapped]) => key.length > 0 && mapped.length > 0),
);
const entries: Array<[string, string]> = [];
for (const [key, mapped] of Object.entries(value)) {
const normalizedKey = key.trim();
const normalizedValue = typeof mapped === "string" ? mapped.trim() : "";
if (normalizedKey && normalizedValue) {
entries.push([normalizedKey, normalizedValue]);
}
}
const normalized = Object.fromEntries(entries);
return Object.keys(normalized).length > 0 ? normalized : undefined;
}
+2
View File
@@ -235,6 +235,8 @@ describe("selectBatchFrames", () => {
describe("sampleFrames", () => {
it("keeps small sets and evenly samples large ones", () => {
expect(sampleFrames([1, 2, 3], 16)).toEqual([1, 2, 3]);
expect(sampleFrames([1, 2, 3], 1)).toEqual([1]);
expect(sampleFrames([1, 2, 3], 0)).toEqual([]);
const sampled = sampleFrames(
Array.from({ length: 100 }, (_, i) => i),
16,
+24 -9
View File
@@ -1,5 +1,6 @@
// Logbook analysis pipeline: frames -> observations -> revised timeline cards.
// Pure parsing/validation lives here so tests can cover it without the SDK.
import { expectDefined } from "openclaw/plugin-sdk/expect-runtime";
import { CARD_CATEGORIES } from "./prompts.js";
import { dayKeyFor } from "./store.js";
import type { LogbookCard, LogbookCardDraft, LogbookDistraction } from "./types.js";
@@ -224,20 +225,28 @@ export function parseCardsJson(params: {
return { ok: false, error: "Output contained no valid cards." };
}
const sorted = drafts.toSorted((a, b) => a.startMs - b.startMs);
for (let i = 1; i < sorted.length; i += 1) {
const overlapMs = sorted[i - 1].endMs - sorted[i].startMs;
const normalized: LogbookCardDraft[] = [];
for (const current of sorted) {
const previous = normalized.at(-1);
if (!previous) {
normalized.push(current);
continue;
}
const overlapMs = previous.endMs - current.startMs;
if (overlapMs > 60 * 1000) {
return {
ok: false,
error: `Cards ${i - 1} and ${i} overlap by ${Math.round(overlapMs / 60000)} minutes; adjacent cards must meet cleanly.`,
error: `Cards ${normalized.length - 1} and ${normalized.length} overlap by ${Math.round(overlapMs / 60000)} minutes; adjacent cards must meet cleanly.`,
};
}
if (overlapMs > 0) {
// Trim sub-minute overlaps instead of round-tripping to the model again.
sorted[i] = { ...sorted[i], startMs: sorted[i - 1].endMs };
normalized.push({ ...current, startMs: previous.endMs });
} else {
normalized.push(current);
}
}
return { ok: true, drafts: sorted };
return { ok: true, drafts: normalized };
}
/** Sub-minute slack so minute-rounded model times do not fail coverage checks. */
@@ -329,7 +338,7 @@ export function selectBatchFrames(params: {
if (params.frames.length === 0) {
return null;
}
const first = params.frames[0];
const first = expectDefined(params.frames[0], "first pending Logbook frame");
const firstDay = dayKeyFor(first.capturedAtMs);
const nextDayStart = new Date(first.capturedAtMs);
nextDayStart.setHours(24, 0, 0, 0);
@@ -358,7 +367,7 @@ export function selectBatchFrames(params: {
if (selected.length === 0) {
return null;
}
const last = selected[selected.length - 1];
const last = expectDefined(selected.at(-1), "last selected Logbook frame");
// Only close a batch once its window has elapsed (or a gap/midnight ended
// it), so a window in progress keeps accumulating frames; `force` closes an
// in-progress window immediately (analyze now).
@@ -380,13 +389,19 @@ export function selectBatchFrames(params: {
/** Evenly samples frames so a batch stays within the per-call image budget. */
export function sampleFrames<T>(frames: T[], max: number): T[] {
if (max <= 0) {
return [];
}
if (frames.length <= max) {
return frames;
}
if (max === 1) {
return [expectDefined(frames[0], "first Logbook frame sample")];
}
const sampled: T[] = [];
const step = (frames.length - 1) / (max - 1);
for (let i = 0; i < max; i += 1) {
sampled.push(frames[Math.round(i * step)]);
sampled.push(expectDefined(frames[Math.round(i * step)], "sampled Logbook frame"));
}
return [...new Set(sampled)];
}
@@ -400,7 +415,7 @@ export function pickKeyframeId(
return undefined;
}
const midpoint = card.startMs + (card.endMs - card.startMs) / 2;
let best = frames[0];
let best = expectDefined(frames[0], "first Logbook keyframe candidate");
for (const frame of frames) {
if (Math.abs(frame.capturedAtMs - midpoint) < Math.abs(best.capturedAtMs - midpoint)) {
best = frame;
+5 -1
View File
@@ -84,7 +84,11 @@ export function listMatrixEnvAccountIds(env: NodeJS.ProcessEnv = process.env): s
if (!match) {
continue;
}
const accountId = decodeMatrixEnvAccountToken(match[1]);
const encodedAccountId = match[1];
if (!encodedAccountId) {
continue;
}
const accountId = decodeMatrixEnvAccountToken(encodedAccountId);
if (accountId) {
ids.add(accountId);
}
+8 -2
View File
@@ -349,8 +349,14 @@ function compactLooseListTokens(tokens: MarkdownToken[]): void {
item.immediateParagraphOpenIndexes.length === 1 &&
item.immediateParagraphCloseIndexes.length === 1
) {
tokens[item.immediateParagraphOpenIndexes[0]].hidden = true;
tokens[item.immediateParagraphCloseIndexes[0]].hidden = true;
const openIndex = item.immediateParagraphOpenIndexes[0];
const closeIndex = item.immediateParagraphCloseIndexes[0];
const openToken = openIndex === undefined ? undefined : tokens[openIndex];
const closeToken = closeIndex === undefined ? undefined : tokens[closeIndex];
if (openToken && closeToken) {
openToken.hidden = true;
closeToken.hidden = true;
}
}
continue;
}
@@ -37,6 +37,9 @@ function parseGeoUri(value: string): GeoUriParams | null {
}
const payload = trimmed.slice(4);
const [coordsPart, ...paramParts] = payload.split(";");
if (!coordsPart) {
return null;
}
const coords = coordsPart.split(",");
if (coords.length < 2) {
return null;
@@ -74,9 +74,14 @@ export function parseMxc(url: string): { server: string; mediaId: string } | nul
if (!match) {
return null;
}
const server = match[1];
const mediaId = match[2];
if (!server || !mediaId) {
return null;
}
return {
server: match[1],
mediaId: match[2],
server,
mediaId,
};
}
@@ -1,10 +1,11 @@
// Matrix plugin module implements verification manager behavior.
import {
VerificationPhase,
VerificationRequestEvent,
VerifierEvent,
} from "matrix-js-sdk/lib/crypto-api/verification.js";
import { VerificationMethod } from "matrix-js-sdk/lib/types.js";
// Matrix plugin module implements verification manager behavior.
import { expectDefined } from "openclaw/plugin-sdk/expect-runtime";
import {
resolveDateTimestampMs,
resolveTimestampMsToIsoString,
@@ -341,7 +342,7 @@ export class MatrixVerificationManager {
return txId === id;
});
if (transactionMatches.length === 1) {
return transactionMatches[0];
return expectDefined(transactionMatches[0], "single Matrix verification session");
}
if (transactionMatches.length > 1) {
throw new Error(
+4 -1
View File
@@ -78,7 +78,10 @@ export function resolveSingleAccountPromotionTarget(params: {
([accountId, value]) => accountId && typeof value === "object" && value,
);
if (namedAccounts.length === 1) {
return namedAccounts[0][0];
const onlyAccount = namedAccounts[0];
if (onlyAccount) {
return onlyAccount[0];
}
}
if (
namedAccounts.length > 1 &&
@@ -507,7 +507,11 @@ function isRetryableError(error: Error): boolean {
if (!clientErrorMatch) {
continue;
}
const statusCode = Number.parseInt(clientErrorMatch[1], 10);
const statusCodeText = clientErrorMatch[1];
if (!statusCodeText) {
continue;
}
const statusCode = Number.parseInt(statusCodeText, 10);
if (statusCode >= 400 && statusCode < 500) {
return false;
}
@@ -134,6 +134,9 @@ export async function listMattermostDirectoryPeers(
// All bots see the same user list, so one client suffices (unlike channels
// where private channel membership varies per bot).
const client = clients[0];
if (!client) {
return [];
}
try {
const me = await fetchMattermostMe(client);
const teams = await client.request<{ id: string }[]>("/users/me/teams");
@@ -141,7 +144,11 @@ export async function listMattermostDirectoryPeers(
return [];
}
// Uses first team — multi-team setups may need iteration in the future
const teamId = teams[0].id;
const team = teams[0];
if (!team) {
return [];
}
const teamId = team.id;
const q = normalizeLowercaseStringOrEmpty(params.query);
let users: MattermostUser[];
@@ -45,7 +45,11 @@ function splitModelRef(modelRef?: string | null): { provider: string; model: str
if (!match) {
return null;
}
const provider = normalizeProviderId(match[1]);
const rawProvider = match[1];
if (!rawProvider) {
return null;
}
const provider = normalizeProviderId(rawProvider);
// Mattermost copy should normalize accidental whitespace around the model.
const model = normalizeOptionalString(match[2]);
if (!provider || !model) {
@@ -522,7 +522,11 @@ function buildMattermostAttachmentPlaceholder(mediaList: MattermostMediaInfo[]):
return "";
}
if (mediaList.length === 1) {
const kind = mediaList[0].kind === "unknown" ? "document" : mediaList[0].kind;
const media = mediaList[0];
if (!media) {
return "";
}
const kind = media.kind === "unknown" ? "document" : media.kind;
return `<media:${kind}>`;
}
const allImages = mediaList.every((media) => media.kind === "image");
@@ -102,11 +102,15 @@ export function resolveSlashHandlerForToken(token: string): SlashHandlerMatch {
return { kind: "none" };
}
if (matches.length === 1) {
const match = matches[0];
if (!match) {
return { kind: "none" };
}
return {
kind: "single",
source: "token",
handler: matches[0].handler,
accountIds: [matches[0].accountId],
handler: match.handler,
accountIds: [match.accountId],
};
}
@@ -146,11 +150,15 @@ export function resolveSlashHandlerForCommand(params: {
return { kind: "none" };
}
if (matches.length === 1) {
const match = matches[0];
if (!match) {
return { kind: "none" };
}
return {
kind: "single",
source: "command",
handler: matches[0].handler,
accountIds: [matches[0].accountId],
handler: match.handler,
accountIds: [match.accountId],
};
}
@@ -301,8 +309,8 @@ export function registerSlashCommandRoute(api: OpenClawPluginApi) {
// If there's only one active account (common case), route directly.
if (accountStates.size === 1) {
const [, state] = [...accountStates.entries()][0];
if (!state.handler) {
const state = accountStates.values().next().value;
if (!state?.handler) {
res.statusCode = 503;
res.setHeader("Content-Type", "application/json; charset=utf-8");
res.end(
@@ -3,6 +3,7 @@ import { createHash } from "node:crypto";
import type { Dirent } from "node:fs";
import fs from "node:fs/promises";
import path from "node:path";
import { expectDefined } from "openclaw/plugin-sdk/expect-runtime";
import { truncateUtf16Safe } from "openclaw/plugin-sdk/memory-core-host-engine-foundation";
import {
buildSessionEntry,
@@ -954,13 +955,13 @@ async function collectSessionIngestionBatches(params: {
};
const cursorAtEnd = previous !== undefined && previous.lastContentLine >= previous.lineCount;
const unchanged =
Boolean(previous) &&
previous !== undefined &&
previous.mtimeMs === fingerprint.mtimeMs &&
previous.size === fingerprint.size &&
previous.contentHash.length > 0 &&
cursorAtEnd;
if (unchanged) {
nextFiles[stateKey] = previous!;
nextFiles[stateKey] = expectDefined(previous, "unchanged dreaming file state");
continue;
}

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