Compare commits

..

4 Commits

Author SHA1 Message Date
Vincent Koc
d9a1731c2c test(plugins): cover bundled tool callback scopes 2026-05-30 13:05:55 +01:00
Vincent Koc
7c6b4994fb fix(plugins): simplify scoped prepare wrapper 2026-05-30 10:54:51 +01:00
Vincent Koc
dab087a24d fix(plugins): scope tool argument preparation 2026-05-30 10:54:50 +01:00
Vincent Koc
8bbc467d5b fix(plugins): scope plugin tool callbacks 2026-05-30 10:54:50 +01:00
342 changed files with 4575 additions and 10273 deletions

View File

@@ -6,10 +6,18 @@ class: standard
capacity:
market: spot
strategy: most-available
# Fail closed instead of silently falling back to on-demand while the
# Azure-backed billing account is the default runner path.
fallback: spot-only
fallback: on-demand-after-120s
hints: true
availabilityZones:
- eu-west-1a
- eu-west-1b
- eu-west-1c
regions:
- eu-west-1
- eu-west-2
- eu-central-1
- us-east-1
- us-west-2
actions:
workflow: .github/workflows/crabbox-hydrate.yml
# Default AWS hydration uses local Actions replay. Use
@@ -29,8 +37,6 @@ blacksmith:
job: check
ref: main
aws:
# AWS-specific overrides still pin direct `--provider aws` runs without
# leaking AWS region names into the Azure default capacity fallback list.
region: eu-west-1
rootGB: 400
sync:

View File

@@ -1420,12 +1420,10 @@ jobs:
find src \
-type f \( -name '*.ts' -o -name '*.tsx' -o -name '*.mts' -o -name '*.cts' -o -name '*.js' -o -name '*.mjs' -o -name '*.json' \) \
-exec touch -t 200001010000 {} +
if [ -d packages/llm-core/src ]; then
find packages/llm-core/src \
-type f \( -name '*.ts' -o -name '*.tsx' -o -name '*.mts' -o -name '*.cts' -o -name '*.js' -o -name '*.mjs' -o -name '*.json' \) \
-exec touch -t 200001010000 {} +
fi
cache_inputs=(
find packages/llm-core/src \
-type f \( -name '*.ts' -o -name '*.tsx' -o -name '*.mts' -o -name '*.cts' -o -name '*.js' -o -name '*.mjs' -o -name '*.json' \) \
-exec touch -t 200001010000 {} +
touch -t 200001010000 \
tsconfig.json \
tsconfig.plugin-sdk.dts.json \
packages/plugin-sdk/tsconfig.json \
@@ -1437,12 +1435,6 @@ jobs:
scripts/lib/plugin-sdk-entries.mjs \
package.json \
pnpm-lock.yaml
)
for cache_input in "${cache_inputs[@]}"; do
if [ -e "$cache_input" ]; then
touch -t 200001010000 "$cache_input"
fi
done
- name: Run additional check shard
env:

View File

@@ -24,7 +24,6 @@ Docs: https://docs.openclaw.ai
### Fixes
- CLI: keep `plugins list --json` on the snapshot-only path so plugin sweeps avoid loading the full runtime status graph.
- Plugins: make PixVerse external-plugin ClawHub metadata explicit and keep it out of bundled dist builds.
- Providers: bound generated media downloads from OpenAI, Runway, xAI, MiniMax, BytePlus, DashScope-compatible, FAL, OpenRouter, Google, Vydra, and Comfy providers.
- Providers: cap GitHub Copilot OAuth request timeouts before creating abort signals.
@@ -34,16 +33,6 @@ Docs: https://docs.openclaw.ai
- Security/config parsing: reject unsafe OAuth/token lifetimes, retry-after delays, inbound timestamps, response body sizes, command timeout config, sandbox observer token TTLs, and gateway WebSocket calls after close.
- Providers/media: cap local service, model, usage, queue, generated media, TTS, music, workflow polling, and provider OAuth request timers across hosted and local providers.
- Release/CI/E2E: bound release candidate reads, beta smoke REST calls, changelog restore, kitchen-sink and bundled plugin readiness probes, secret-provider probes, Vitest routing, and mainline test flakes. (#88127, #88137, #88155, #88160)
- Release/CI/E2E: run the secret-provider integration proof through the repo pnpm runner so native macOS and Windows validation use the hydrated package-manager shim.
- Release/CI/E2E: run the Telegram desktop proof gateway through the repo pnpm runner so native macOS proof uses the hydrated package-manager shim.
- CI/Crabbox: keep default runner capacity spot-only and provider-neutral so OpenClaw remote validation does not silently fall back to on-demand leases or stale AWS region hints.
- CI/Crabbox: route Crabbox wrapper and Testbox workflow edits to their regression tests so changed-test gates do not silently run zero specs.
- CI/workflows: route workflow sanity helper edits to their guard tests and cover composite-action input interpolation checks.
- CI/tooling: route CI scope, dependency, changelog, and docs helper edits to their owner tests instead of silently skipping changed-test coverage.
- CI/tooling: route package, release, and install helper edits to their owner tests so changed-test gates cover publish and installer script changes.
- CI/tooling: route shared script library edits through their owner tests so lock, process, safety, and scan helpers do not skip changed-test coverage.
- CI/tooling: skip expensive import-graph scans once a changed diff already requires broad fallback, keeping local changed-test planning fast while still collecting explicit owner tests.
- CI/tooling: route script edits through conventional owner tests when matching `test/scripts` or `src/scripts` coverage already exists.
- Performance: reuse prepared provider handles, strict tool schemas, gateway runtime metadata, session maintenance config, plugin metadata, bundled skill allowlists, package-local plugin artifacts, and single-entry store writes.
## 2026.5.28

View File

@@ -17,13 +17,6 @@ export {
import { buildAnthropicVertexProvider } from "./provider-catalog.js";
import { hasAnthropicVertexAvailableAuth } from "./region.js";
let streamRuntimeModulePromise: Promise<typeof import("./stream-runtime.js")> | null = null;
const loadStreamRuntimeModule = async () => {
streamRuntimeModulePromise ??= import("./stream-runtime.js");
return await streamRuntimeModulePromise;
};
export function mergeImplicitAnthropicVertexProvider(params: {
existing?: ReturnType<typeof buildAnthropicVertexProvider>;
implicit: ReturnType<typeof buildAnthropicVertexProvider>;
@@ -57,7 +50,7 @@ export function createAnthropicVertexStreamFn(
baseURL?: string,
deps?: AnthropicVertexStreamDeps,
): StreamFn {
const streamFnPromise = loadStreamRuntimeModule().then((runtime) =>
const streamFnPromise = import("./stream-runtime.js").then((runtime) =>
runtime.createAnthropicVertexStreamFn(projectId, region, baseURL, deps),
);
return async (model, context, options) => {
@@ -71,7 +64,7 @@ export function createAnthropicVertexStreamFnForModel(
env: NodeJS.ProcessEnv = process.env,
deps?: AnthropicVertexStreamDeps,
): StreamFn {
const streamFnPromise = loadStreamRuntimeModule().then((runtime) =>
const streamFnPromise = import("./stream-runtime.js").then((runtime) =>
runtime.createAnthropicVertexStreamFnForModel(model, env, deps),
);
return async (...args) => {

View File

@@ -15,15 +15,6 @@ import { BrowserToolSchema } from "./src/browser-tool.schema.js";
const EAGER_BROWSER_CONTROL_SERVICE_ENV = "OPENCLAW_EAGER_BROWSER_CONTROL_SERVER";
let browserRegistrationRuntimeModulePromise: Promise<
typeof import("./register.runtime.js")
> | null = null;
const loadBrowserRegistrationRuntimeModule = async () => {
browserRegistrationRuntimeModulePromise ??= import("./register.runtime.js");
return await browserRegistrationRuntimeModulePromise;
};
function isTruthyEnvValue(value: string | undefined): boolean {
return /^(?:1|true|yes|on)$/iu.test(value?.trim() ?? "");
}
@@ -60,7 +51,7 @@ function createLazyBrowserTool(opts?: {
].join(" "),
parameters: BrowserToolSchema,
execute: async (toolCallId, args, signal, onUpdate) => {
const { createBrowserTool } = await loadBrowserRegistrationRuntimeModule();
const { createBrowserTool } = await import("./register.runtime.js");
const tool = createBrowserTool(opts);
return await tool.execute(toolCallId, args, signal, onUpdate);
},
@@ -74,7 +65,7 @@ export const browserPluginNodeHostCommands: OpenClawPluginNodeHostCommand[] = [
command: "browser.proxy",
cap: "browser",
handle: async (paramsJSON) => {
const { runBrowserProxyCommand } = await loadBrowserRegistrationRuntimeModule();
const { runBrowserProxyCommand } = await import("./register.runtime.js");
return await runBrowserProxyCommand(paramsJSON);
},
},
@@ -82,7 +73,7 @@ export const browserPluginNodeHostCommands: OpenClawPluginNodeHostCommand[] = [
export const browserSecurityAuditCollectors: OpenClawPluginSecurityAuditCollector[] = [
async (ctx) => {
const { collectBrowserSecurityAuditFindings } = await loadBrowserRegistrationRuntimeModule();
const { collectBrowserSecurityAuditFindings } = await import("./register.runtime.js");
return collectBrowserSecurityAuditFindings(ctx);
},
];
@@ -91,7 +82,7 @@ function createLazyBrowserPluginService(): OpenClawPluginService {
let service: OpenClawPluginService | null = null;
const loadService = async () => {
if (!service) {
const { createBrowserPluginService } = await loadBrowserRegistrationRuntimeModule();
const { createBrowserPluginService } = await import("./register.runtime.js");
service = createBrowserPluginService();
}
return service;
@@ -133,7 +124,7 @@ export function registerBrowserPlugin(api: OpenClawPluginApi) {
api.registerGatewayMethod(
BROWSER_REQUEST_GATEWAY_METHOD,
async (opts) => {
const { handleBrowserGatewayRequest } = await loadBrowserRegistrationRuntimeModule();
const { handleBrowserGatewayRequest } = await import("./register.runtime.js");
return await handleBrowserGatewayRequest(opts);
},
{

View File

@@ -1,6 +1,6 @@
import { redactCdpUrl } from "../cdp.helpers.js";
import { snapshotAria } from "../cdp.js";
import { getChromeMcpPid, takeChromeMcpSnapshot } from "../chrome-mcp.js";
import { getChromeMcpPid } from "../chrome-mcp.js";
import { resolveBrowserExecutableForPlatform } from "../chrome.executables.js";
import { resolveManagedBrowserHeadlessMode } from "../config.js";
import { buildBrowserDoctorReport } from "../doctor.js";
@@ -227,6 +227,7 @@ async function runBrowserLiveProbe(req: BrowserRequest, ctx: BrowserRouteContext
try {
const tab = await profileCtx.ensureTabAvailable();
if (capabilities.usesChromeMcp) {
const { takeChromeMcpSnapshot } = await import("../chrome-mcp.js");
await takeChromeMcpSnapshot({
profileName: profileCtx.profile.name,
profile: profileCtx.profile,

View File

@@ -13,20 +13,13 @@ export type CodexAppServerClientFactory = (
config?: AuthProfileOrderConfig,
) => Promise<CodexAppServerClient>;
let sharedClientModulePromise: Promise<typeof import("./shared-client.js")> | null = null;
const loadSharedClientModule = async () => {
sharedClientModulePromise ??= import("./shared-client.js");
return await sharedClientModulePromise;
};
export const defaultCodexAppServerClientFactory: CodexAppServerClientFactory = (
startOptions,
authProfileId,
agentDir,
config,
) =>
loadSharedClientModule().then(({ getSharedCodexAppServerClient }) =>
import("./shared-client.js").then(({ getSharedCodexAppServerClient }) =>
getSharedCodexAppServerClient({ startOptions, authProfileId, agentDir, config }),
);
@@ -36,6 +29,6 @@ export const defaultLeasedCodexAppServerClientFactory: CodexAppServerClientFacto
agentDir,
config,
) =>
loadSharedClientModule().then(({ getLeasedSharedCodexAppServerClient }) =>
import("./shared-client.js").then(({ getLeasedSharedCodexAppServerClient }) =>
getLeasedSharedCodexAppServerClient({ startOptions, authProfileId, agentDir, config }),
);

View File

@@ -54,21 +54,6 @@ describe("DiffArtifactStore", () => {
expect(await store.readHtml(artifact.id)).toBe("<html>demo</html>");
});
it("caps artifact expiry instead of throwing near the Date boundary", async () => {
vi.useFakeTimers();
vi.setSystemTime(new Date(8_640_000_000_000_000 - 1_000));
const artifact = await store.createArtifact({
html: "<html>demo</html>",
title: "Demo",
inputKind: "patch",
fileCount: 1,
ttlMs: 60_000,
});
expect(artifact.expiresAt).toBe("+275760-09-13T00:00:00.000Z");
});
it("expires artifacts after the ttl", async () => {
vi.useFakeTimers();
const now = new Date("2026-02-27T16:00:00Z");
@@ -146,15 +131,6 @@ describe("DiffArtifactStore", () => {
});
});
it("caps standalone file expiry instead of throwing near the Date boundary", async () => {
vi.useFakeTimers();
vi.setSystemTime(new Date(8_640_000_000_000_000 - 1_000));
const standalone = await store.createStandaloneFileArtifact({ ttlMs: 60_000 });
expect(standalone.expiresAt).toBe("+275760-09-13T00:00:00.000Z");
});
it("expires standalone file artifacts using ttl metadata", async () => {
vi.useFakeTimers();
const now = new Date("2026-02-27T16:00:00Z");

View File

@@ -1,7 +1,6 @@
import crypto from "node:crypto";
import fs from "node:fs/promises";
import path from "node:path";
import { MAX_DATE_TIMESTAMP_MS, timestampMsToIsoString } from "openclaw/plugin-sdk/number-runtime";
import { root as fsRoot } from "openclaw/plugin-sdk/security-runtime";
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
import type { PluginLogger } from "../api.js";
@@ -65,16 +64,15 @@ export class DiffArtifactStore {
const htmlPath = path.join(artifactDir, "viewer.html");
const ttlMs = normalizeTtlMs(params.ttlMs);
const createdAt = new Date();
const createdAtIso = createdAt.toISOString();
const expiresAt = resolveExpiresAtIso(createdAt.getTime(), ttlMs);
const expiresAt = new Date(createdAt.getTime() + ttlMs);
const meta: DiffArtifactMeta = {
id,
token,
title: params.title,
inputKind: params.inputKind,
fileCount: params.fileCount,
createdAt: createdAtIso,
expiresAt,
createdAt: createdAt.toISOString(),
expiresAt: expiresAt.toISOString(),
viewerPath: `${VIEWER_PREFIX}/${id}/${token}`,
htmlPath,
...(params.context ? { context: params.context } : {}),
@@ -146,12 +144,11 @@ export class DiffArtifactStore {
const filePath = path.join(artifactDir, `preview.${format}`);
const ttlMs = normalizeTtlMs(params.ttlMs);
const createdAt = new Date();
const createdAtIso = createdAt.toISOString();
const expiresAt = resolveExpiresAtIso(createdAt.getTime(), ttlMs);
const expiresAt = new Date(createdAt.getTime() + ttlMs).toISOString();
const meta: StandaloneFileMeta = {
kind: "standalone_file",
id,
createdAt: createdAtIso,
createdAt: createdAt.toISOString(),
expiresAt,
filePath: this.normalizeStoredPath(filePath, "filePath"),
...(params.context ? { context: params.context } : {}),
@@ -360,14 +357,6 @@ function normalizeTtlMs(value?: number): number {
return Math.min(rounded, MAX_TTL_MS);
}
function resolveExpiresAtIso(createdAtMs: number, ttlMs: number): string {
return (
timestampMsToIsoString(createdAtMs + ttlMs) ??
timestampMsToIsoString(MAX_DATE_TIMESTAMP_MS) ??
"1970-01-01T00:00:00.000Z"
);
}
function isExpired(meta: { expiresAt: string }): boolean {
const expiresAt = Date.parse(meta.expiresAt);
if (!Number.isFinite(expiresAt)) {

View File

@@ -63,88 +63,4 @@ describe("Discord model picker preference migration", () => {
updatedAt: "2026-05-29T00:00:00.001Z",
});
});
it("plans legacy JSON import with max Date timestamps", async () => {
const stateDir = await makeStateDir();
const sourcePath = path.join(stateDir, "discord", "model-picker-preferences.json");
await fs.mkdir(path.dirname(sourcePath), { recursive: true });
await fs.writeFile(
sourcePath,
JSON.stringify({
version: 1,
entries: {
"discord:default:dm:user:max-date": {
recent: ["openai/gpt-5", "openai/gpt-4.1"],
updatedAt: "+275760-09-13T00:00:00.000Z",
},
},
}),
);
const plans = await Promise.resolve(
detectDiscordLegacyStateMigrations({
cfg: {},
env: {},
oauthDir: path.join(stateDir, "credentials"),
stateDir,
}),
);
const plan = plans?.[0];
if (plan?.kind !== "plugin-state-import") {
throw new Error("expected plugin-state import plan");
}
const entries = await plan.readEntries();
expect(
entries.map((entry) => {
const value = entry.value as { updatedAt?: unknown };
return value.updatedAt;
}),
).toEqual(["+275760-09-13T00:00:00.000Z", "+275760-09-12T23:59:59.999Z"]);
});
it("keeps legacy JSON import order near max Date", async () => {
const stateDir = await makeStateDir();
const sourcePath = path.join(stateDir, "discord", "model-picker-preferences.json");
await fs.mkdir(path.dirname(sourcePath), { recursive: true });
await fs.writeFile(
sourcePath,
JSON.stringify({
version: 1,
entries: {
"discord:default:dm:user:near-max-date": {
recent: ["openai/gpt-5", "openai/gpt-4.1"],
updatedAt: "+275760-09-12T23:59:59.999Z",
},
},
}),
);
const plans = await Promise.resolve(
detectDiscordLegacyStateMigrations({
cfg: {},
env: {},
oauthDir: path.join(stateDir, "credentials"),
stateDir,
}),
);
const plan = plans?.[0];
if (plan?.kind !== "plugin-state-import") {
throw new Error("expected plugin-state import plan");
}
const entries = await plan.readEntries();
expect(
entries.map((entry) => {
const value = entry.value as { modelRef?: unknown };
return value.modelRef;
}),
).toEqual(["openai/gpt-5", "openai/gpt-4.1"]);
expect(
entries.map((entry) => {
const value = entry.value as { updatedAt?: unknown };
return value.updatedAt;
}),
).toEqual(["+275760-09-13T00:00:00.000Z", "+275760-09-12T23:59:59.999Z"]);
});
});

View File

@@ -2,7 +2,6 @@ import { createHash } from "node:crypto";
import fs from "node:fs";
import path from "node:path";
import type { BundledChannelLegacyStateMigrationDetector } from "openclaw/plugin-sdk/channel-entry-contract";
import { MAX_DATE_TIMESTAMP_MS, timestampMsToIsoString } from "openclaw/plugin-sdk/number-runtime";
import { normalizeProviderId } from "openclaw/plugin-sdk/provider-model-shared";
const PREFERENCE_MAX_ENTRIES = 2_000;
@@ -93,15 +92,7 @@ function timestampMs(value: unknown): number {
}
function legacyUpdatedAtForIndex(updatedAt: unknown, index: number, total: number): string {
const baseMs = timestampMs(updatedAt);
const anchorMs = Math.min(baseMs + Math.max(0, total), MAX_DATE_TIMESTAMP_MS);
const shiftedMs = anchorMs - Math.max(0, index);
return (
timestampMsToIsoString(shiftedMs) ??
timestampMsToIsoString(baseMs) ??
timestampMsToIsoString(Math.max(0, total - index)) ??
"1970-01-01T00:00:00.000Z"
);
return new Date(timestampMs(updatedAt) + Math.max(0, total - index)).toISOString();
}
export const detectDiscordLegacyStateMigrations: BundledChannelLegacyStateMigrationDetector = ({

View File

@@ -6,7 +6,7 @@ import {
createPluginStateKeyedStoreForTests,
resetPluginStateStoreForTests,
} from "openclaw/plugin-sdk/plugin-state-test-runtime";
import { afterEach, describe, expect, it, vi } from "vitest";
import { afterEach, describe, expect, it } from "vitest";
import { setDiscordRuntime, type DiscordRuntime } from "../runtime.js";
import {
buildDiscordModelPickerPreferenceKey,
@@ -163,68 +163,6 @@ describe("discord model picker preferences", () => {
]);
});
it("imports legacy JSON preferences with max Date timestamps", async () => {
const env = await createStateEnv();
const scope = { accountId: "main", guildId: "guild-max-date", userId: "user-max-date" };
const key = buildDiscordModelPickerPreferenceKey(scope);
expect(key).toBeTruthy();
const legacyPath = path.join(
env.OPENCLAW_STATE_DIR as string,
"discord",
"model-picker-preferences.json",
);
await fs.mkdir(path.dirname(legacyPath), { recursive: true });
await fs.writeFile(
legacyPath,
JSON.stringify({
version: 1,
entries: {
[key as string]: {
recent: ["openai/gpt-4.1", "openai/gpt-4o"],
updatedAt: "+275760-09-13T00:00:00.000Z",
},
},
}),
"utf8",
);
await expect(readDiscordModelPickerRecentModels({ env, scope })).resolves.toEqual([
"openai/gpt-4.1",
"openai/gpt-4o",
]);
});
it("preserves legacy JSON preference order near max Date", async () => {
const env = await createStateEnv();
const scope = { accountId: "main", guildId: "guild-near-max-date", userId: "user-near-max" };
const key = buildDiscordModelPickerPreferenceKey(scope);
expect(key).toBeTruthy();
const legacyPath = path.join(
env.OPENCLAW_STATE_DIR as string,
"discord",
"model-picker-preferences.json",
);
await fs.mkdir(path.dirname(legacyPath), { recursive: true });
await fs.writeFile(
legacyPath,
JSON.stringify({
version: 1,
entries: {
[key as string]: {
recent: ["openai/gpt-4.1", "openai/gpt-4o"],
updatedAt: "+275760-09-12T23:59:59.999Z",
},
},
}),
"utf8",
);
await expect(readDiscordModelPickerRecentModels({ env, scope })).resolves.toEqual([
"openai/gpt-4.1",
"openai/gpt-4o",
]);
});
it("skips malformed legacy JSON entries during import", async () => {
const env = await createStateEnv();
const scope = { userId: "valid-legacy-user" };
@@ -269,34 +207,4 @@ describe("discord model picker preferences", () => {
const recent = await readDiscordModelPickerRecentModels({ env, scope });
expect(new Set(recent)).toEqual(new Set(["openai/gpt-4o", "openai/gpt-4.1"]));
});
it("keeps selections recent when the process clock is outside the Date range", async () => {
const env = await createStateEnv();
const scope = { userId: "invalid-clock-user" };
await recordDiscordModelPickerRecentModel({ env, scope, modelRef: "openai/gpt-4.1" });
await recordDiscordModelPickerRecentModel({ env, scope, modelRef: "openai/gpt-4o" });
const dateNowSpy = vi.spyOn(Date, "now").mockReturnValue(8_640_000_000_000_001);
try {
await recordDiscordModelPickerRecentModel({
env,
scope,
modelRef: "openai/gpt-5.5",
limit: 2,
});
await recordDiscordModelPickerRecentModel({
env,
scope,
modelRef: "openai/gpt-5.6",
limit: 2,
});
} finally {
dateNowSpy.mockRestore();
}
await expect(readDiscordModelPickerRecentModels({ env, scope, limit: 3 })).resolves.toEqual([
"openai/gpt-5.6",
"openai/gpt-5.5",
]);
});
});

View File

@@ -3,12 +3,6 @@ import os from "node:os";
import path from "node:path";
import { normalizeAccountId as normalizeSharedAccountId } from "openclaw/plugin-sdk/account-id";
import { readJsonFileWithFallback } from "openclaw/plugin-sdk/json-store";
import {
MAX_DATE_TIMESTAMP_MS,
resolveDateTimestampMs,
resolveTimestampMsToIsoString,
timestampMsToIsoString,
} from "openclaw/plugin-sdk/number-runtime";
import { normalizeProviderId } from "openclaw/plugin-sdk/provider-model-shared";
import { resolveStateDir } from "openclaw/plugin-sdk/state-paths";
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
@@ -19,13 +13,11 @@ const PREFERENCE_MAX_ENTRIES = 2_000;
const MAX_PLUGIN_STATE_KEY_BYTES = 512;
const textEncoder = new TextEncoder();
let lastPreferenceTimestampMs = 0;
let lastPreferenceOrder = 0;
type ModelPickerPreferencesEntry = {
scopeKey: string;
modelRef: string;
updatedAt: string;
updatedOrder?: number;
};
type LegacyModelPickerPreferencesEntry = {
@@ -132,7 +124,6 @@ function sanitizeStoredPreferenceEntry(value: unknown): ModelPickerPreferencesEn
scopeKey?: unknown;
modelRef?: unknown;
updatedAt?: unknown;
updatedOrder?: unknown;
};
if (typeof typedValue.scopeKey !== "string" || typeof typedValue.modelRef !== "string") {
return undefined;
@@ -145,10 +136,6 @@ function sanitizeStoredPreferenceEntry(value: unknown): ModelPickerPreferencesEn
scopeKey: typedValue.scopeKey,
modelRef,
updatedAt: typeof typedValue.updatedAt === "string" ? typedValue.updatedAt : "",
updatedOrder:
typeof typedValue.updatedOrder === "number" && Number.isSafeInteger(typedValue.updatedOrder)
? typedValue.updatedOrder
: undefined,
};
}
@@ -165,58 +152,13 @@ function timestampMs(value: string): number {
return Number.isFinite(parsed) ? parsed : 0;
}
function timestampOrder(value?: number): number {
return value !== undefined && value >= 0 ? value : 0;
}
function comparePreferenceEntries(
left: { key: string; value: ModelPickerPreferencesEntry },
right: { key: string; value: ModelPickerPreferencesEntry },
): number {
return (
timestampMs(right.value.updatedAt) - timestampMs(left.value.updatedAt) ||
timestampOrder(right.value.updatedOrder) - timestampOrder(left.value.updatedOrder) ||
left.key.localeCompare(right.key)
);
}
function legacyUpdatedAtForIndex(updatedAt: string, index: number, total: number): string {
const baseMs = timestampMs(updatedAt);
const anchorMs = Math.min(baseMs + Math.max(0, total), MAX_DATE_TIMESTAMP_MS);
const shiftedMs = anchorMs - Math.max(0, index);
return (
timestampMsToIsoString(shiftedMs) ??
timestampMsToIsoString(baseMs) ??
timestampMsToIsoString(Math.max(0, total - index)) ??
"1970-01-01T00:00:00.000Z"
);
return new Date(timestampMs(updatedAt) + Math.max(0, total - index)).toISOString();
}
function nextPreferenceTimestamp(existingEntries: ModelPickerPreferencesEntry[]): {
updatedAt: string;
updatedOrder: number;
} {
const existingMaxTimestampMs = existingEntries.reduce(
(max, entry) => Math.max(max, timestampMs(entry.updatedAt)),
0,
);
lastPreferenceTimestampMs = Math.min(
Math.max(
resolveDateTimestampMs(Date.now(), 0),
lastPreferenceTimestampMs + 1,
existingMaxTimestampMs + 1,
),
MAX_DATE_TIMESTAMP_MS,
);
const existingMaxOrder = existingEntries.reduce(
(max, entry) => Math.max(max, timestampOrder(entry.updatedOrder)),
0,
);
lastPreferenceOrder = Math.max(lastPreferenceOrder + 1, existingMaxOrder + 1);
return {
updatedAt: resolveTimestampMsToIsoString(lastPreferenceTimestampMs),
updatedOrder: lastPreferenceOrder,
};
function nextPreferenceTimestampIso(): string {
lastPreferenceTimestampMs = Math.max(Date.now(), lastPreferenceTimestampMs + 1);
return new Date(lastPreferenceTimestampMs).toISOString();
}
function normalizeLegacyPreferenceKey(key: string): string | undefined {
@@ -295,13 +237,10 @@ export async function readDiscordModelPickerRecentModels(params: {
await importLegacyPreferences(params.env);
const store = openPreferenceStore(params.env);
const recent = (await store.entries())
.map((entry) => ({ key: entry.key, value: sanitizeStoredPreferenceEntry(entry.value) }))
.filter(
(entry): entry is { key: string; value: ModelPickerPreferencesEntry } =>
entry.value?.scopeKey === key,
)
.toSorted(comparePreferenceEntries)
.map((entry) => entry.value.modelRef);
.map((entry) => sanitizeStoredPreferenceEntry(entry.value))
.filter((entry): entry is ModelPickerPreferencesEntry => entry?.scopeKey === key)
.toSorted((left, right) => timestampMs(right.updatedAt) - timestampMs(left.updatedAt))
.map((entry) => entry.modelRef);
if (!params.allowedModelRefs || params.allowedModelRefs.size === 0) {
return sanitizeRecentModels(recent, limit);
}
@@ -329,14 +268,10 @@ export async function recordDiscordModelPickerRecentModel(params: {
try {
await importLegacyPreferences(params.env);
const store = openPreferenceStore(params.env);
const existingEntries = (await store.entries())
.map((entry) => sanitizeStoredPreferenceEntry(entry.value))
.filter((entry): entry is ModelPickerPreferencesEntry => entry?.scopeKey === key);
const timestamp = nextPreferenceTimestamp(existingEntries);
await store.register(buildPreferenceModelKey(key, normalizedModelRef), {
scopeKey: key,
modelRef: normalizedModelRef,
...timestamp,
updatedAt: nextPreferenceTimestampIso(),
});
const limit = Math.max(1, Math.min(params.limit ?? DEFAULT_RECENT_LIMIT, 10));
const scopedEntries = (await store.entries())
@@ -345,7 +280,11 @@ export async function recordDiscordModelPickerRecentModel(params: {
(entry): entry is { key: string; value: ModelPickerPreferencesEntry } =>
entry.value?.scopeKey === key,
)
.toSorted(comparePreferenceEntries);
.toSorted(
(left, right) =>
timestampMs(right.value.updatedAt) - timestampMs(left.value.updatedAt) ||
left.key.localeCompare(right.key),
);
await Promise.all(scopedEntries.slice(limit).map((entry) => store.delete(entry.key)));
} catch {
return;

View File

@@ -121,7 +121,6 @@ beforeEach(() => {
afterEach(() => {
vi.useRealTimers();
vi.restoreAllMocks();
});
afterAll(() => {
@@ -323,18 +322,6 @@ describe("sendMessageDiscord", () => {
).toBeTypeOf("string");
});
it("rejects timeout durations outside Date range", async () => {
const { rest, patchMock } = makeDiscordRest();
await expect(
timeoutMemberDiscord(
{ guildId: "g1", userId: "u1", durationMinutes: 8_640_000_000_000_001 },
discordClientOpts(rest),
),
).rejects.toThrow("Discord timeout duration is outside the supported Date range");
expect(patchMock).not.toHaveBeenCalled();
});
it("adds and removes roles", async () => {
const { rest, putMock, deleteMock } = makeDiscordRest();
putMock.mockResolvedValue({});

View File

@@ -6,7 +6,6 @@ import type {
APIVoiceState,
RESTPostAPIGuildScheduledEventJSONBody,
} from "discord-api-types/v10";
import { timestampMsToIsoString } from "openclaw/plugin-sdk/number-runtime";
import { normalizeOptionalLowercaseString } from "openclaw/plugin-sdk/string-coerce-runtime";
import { loadWebMediaRaw } from "openclaw/plugin-sdk/web-media";
import {
@@ -140,10 +139,7 @@ export async function timeoutMemberDiscord(
let until = payload.until;
if (!until && payload.durationMinutes) {
const ms = payload.durationMinutes * 60 * 1000;
until = timestampMsToIsoString(Date.now() + ms);
if (!until) {
throw new Error("Discord timeout duration is outside the supported Date range");
}
until = new Date(Date.now() + ms).toISOString();
}
return await timeoutGuildMember(rest, payload.guildId, payload.userId, {
body: { communication_disabled_until: until ?? null },

View File

@@ -251,13 +251,6 @@ function applyNewAppSecurityPolicy(
// Scan-to-create flow
// ---------------------------------------------------------------------------
let appRegistrationModulePromise: Promise<typeof import("./app-registration.js")> | null = null;
const loadAppRegistrationModule = async () => {
appRegistrationModulePromise ??= import("./app-registration.js");
return await appRegistrationModulePromise;
};
async function promptFeishuDomain(params: {
prompter: WizardPrompter;
initialValue?: FeishuDomain;
@@ -288,7 +281,7 @@ async function runScanToCreate(
domain: FeishuDomain,
): Promise<AppRegistrationResult | null> {
const { beginAppRegistration, initAppRegistration, pollAppRegistration, printQrCode } =
await loadAppRegistrationModule();
await import("./app-registration.js");
try {
await initAppRegistration(domain);
} catch {
@@ -399,7 +392,7 @@ async function runNewAppFlow(params: {
// Fetch openId via API for manual flow.
if (appId && appSecretProbeValue) {
const { getAppOwnerOpenId } = await loadAppRegistrationModule();
const { getAppOwnerOpenId } = await import("./app-registration.js");
scanOpenId = await getAppOwnerOpenId({
appId,
appSecret: appSecretProbeValue,

View File

@@ -23,3 +23,9 @@ export function registerFeishuSubagentHooks(api: OpenClawPluginApi): void {
handleFeishuSubagentEnded(event);
});
}
export {
handleFeishuSubagentDeliveryTarget,
handleFeishuSubagentEnded,
handleFeishuSubagentSpawning,
} from "./src/subagent-hooks.js";

View File

@@ -37,19 +37,6 @@ import { handleGoogleMeetNodeHostCommand } from "./src/node-host.js";
import { GoogleMeetRuntime } from "./src/runtime.js";
import { isGoogleMeetBrowserManualActionError } from "./src/transports/chrome-create.js";
let googleMeetCreateModulePromise: Promise<typeof import("./src/create.js")> | null = null;
let googleMeetCliModulePromise: Promise<typeof import("./src/cli.js")> | null = null;
const loadGoogleMeetCreateModule = async () => {
googleMeetCreateModulePromise ??= import("./src/create.js");
return await googleMeetCreateModulePromise;
};
const loadGoogleMeetCliModule = async () => {
googleMeetCliModulePromise ??= import("./src/cli.js");
return await googleMeetCliModulePromise;
};
const googleMeetConfigSchema = {
parse(value: unknown) {
return resolveGoogleMeetConfig(value);
@@ -506,7 +493,7 @@ async function createMeetFromParams(params: {
runtime: OpenClawPluginApi["runtime"];
raw: Record<string, unknown>;
}) {
const create = await loadGoogleMeetCreateModule();
const create = await import("./src/create.js");
return create.createMeetFromParams(params);
}
@@ -516,7 +503,7 @@ async function createAndJoinMeetFromParams(params: {
raw: Record<string, unknown>;
ensureRuntime: () => Promise<GoogleMeetRuntime>;
}) {
const create = await loadGoogleMeetCreateModule();
const create = await import("./src/create.js");
return create.createAndJoinMeetFromParams(params);
}
@@ -628,7 +615,7 @@ async function exportGoogleMeetBundleFromParams(
}),
]);
const { buildGoogleMeetExportManifest, googleMeetExportFileNames, writeMeetExportBundle } =
await loadGoogleMeetCliModule();
await import("./src/cli.js");
const calendarId = normalizeOptionalString(raw.calendarId);
const request = {
...(resolved.meeting ? { meeting: resolved.meeting } : {}),
@@ -1202,7 +1189,7 @@ export default definePluginEntry({
api.registerCli(
async ({ program }) => {
const { registerGoogleMeetCli } = await loadGoogleMeetCliModule();
const { registerGoogleMeetCli } = await import("./src/cli.js");
registerGoogleMeetCli({
program,
config,

View File

@@ -20,13 +20,6 @@ const ENV_VARS = [
"GEMINI_CLI_OAUTH_CLIENT_SECRET",
] as const;
let oauthRuntimeModulePromise: Promise<typeof import("./oauth.runtime.js")> | null = null;
const loadOauthRuntimeModule = async () => {
oauthRuntimeModulePromise ??= import("./oauth.runtime.js");
return await oauthRuntimeModulePromise;
};
async function fetchGeminiCliUsage(ctx: ProviderFetchUsageSnapshotContext) {
return await fetchGeminiUsage(ctx.token, ctx.timeoutMs, ctx.fetchFn, PROVIDER_ID);
}
@@ -65,7 +58,7 @@ export function buildGoogleGeminiCliProvider(): ProviderPlugin {
const spin = ctx.prompter.progress("Starting Gemini CLI OAuth…");
try {
const { loginGeminiCliOAuth } = await loadOauthRuntimeModule();
const { loginGeminiCliOAuth } = await import("./oauth.runtime.js");
const result = await loginGeminiCliOAuth({
isRemote: ctx.isRemote,
openUrl: ctx.openUrl,
@@ -129,7 +122,7 @@ export function buildGoogleGeminiCliProvider(): ProviderPlugin {
isModernModelRef: ({ modelId }) => isModernGoogleModel(modelId),
formatApiKey: (cred) => formatGoogleOauthApiKey(cred),
refreshOAuth: async (cred) => {
const { refreshGeminiCliOAuthToken } = await loadOauthRuntimeModule();
const { refreshGeminiCliOAuthToken } = await import("./oauth.runtime.js");
return await refreshGeminiCliOAuthToken(cred);
},
resolveUsageAuth: async (ctx) => {

View File

@@ -100,7 +100,6 @@ describe("buildGoogleRealtimeVoiceProvider", () => {
afterEach(() => {
vi.useRealTimers();
vi.restoreAllMocks();
for (const key of ENV_KEYS) {
const value = envSnapshot[key];
if (value === undefined) {
@@ -453,20 +452,6 @@ describe("buildGoogleRealtimeVoiceProvider", () => {
]);
});
it("rejects browser session expiry outside Date range", async () => {
vi.spyOn(Date, "now").mockReturnValue(8_640_000_000_000_001);
const provider = buildGoogleRealtimeVoiceProvider();
await expect(
provider.createBrowserSession?.({
providerConfig: {
apiKey: "gemini-key",
},
}),
).rejects.toThrow("Google realtime browser session expiry is outside the supported Date range");
expect(createTokenMock).not.toHaveBeenCalled();
});
it("can opt out of Google Live session resumption and context compression", async () => {
const provider = buildGoogleRealtimeVoiceProvider();
const bridge = provider.createBridge({

View File

@@ -16,7 +16,6 @@ import type {
ThinkingConfig,
TurnCoverage,
} from "@google/genai";
import { timestampMsToIsoString } from "openclaw/plugin-sdk/number-runtime";
import type { OpenClawConfig } from "openclaw/plugin-sdk/provider-onboard";
import type {
RealtimeVoiceAudioFormat,
@@ -858,11 +857,6 @@ async function createGoogleRealtimeBrowserSession(
const voice = req.voice ?? config.voice ?? GOOGLE_REALTIME_DEFAULT_VOICE;
const expiresAtMs = Date.now() + GOOGLE_REALTIME_BROWSER_SESSION_TTL_MS;
const newSessionExpiresAtMs = Date.now() + GOOGLE_REALTIME_BROWSER_NEW_SESSION_TTL_MS;
const expireTime = timestampMsToIsoString(expiresAtMs);
const newSessionExpireTime = timestampMsToIsoString(newSessionExpiresAtMs);
if (!expireTime || !newSessionExpireTime) {
throw new Error("Google realtime browser session expiry is outside the supported Date range");
}
const ai = createGoogleGenAI({
apiKey,
httpOptions: {
@@ -872,8 +866,8 @@ async function createGoogleRealtimeBrowserSession(
const token = await ai.authTokens.create({
config: {
uses: 1,
expireTime,
newSessionExpireTime,
expireTime: new Date(expiresAtMs).toISOString(),
newSessionExpireTime: new Date(newSessionExpiresAtMs).toISOString(),
liveConnectConstraints: {
model,
config: buildGoogleLiveConnectConfig({

View File

@@ -24,7 +24,7 @@ import {
rememberIMessageReplyCache,
type IMessageChatContext,
} from "./monitor-reply-cache.js";
import { getCachedIMessagePrivateApiStatus, probeIMessagePrivateApi } from "./probe.js";
import { getCachedIMessagePrivateApiStatus } from "./probe.js";
import { parseIMessageTarget, type IMessageTarget } from "./targets.js";
const loadIMessageActionsRuntime = createLazyRuntimeNamedExport(
@@ -417,6 +417,7 @@ export const imessageMessageActions: ChannelMessageActionAdapter = {
// status adapter, which doesn't fire eagerly on first dispatch. Run
// an inline probe so the first react/send-rich attempt after `imsg
// launch` succeeds without requiring a manual `channels status`.
const { probeIMessagePrivateApi } = await import("./probe.js");
privateApiStatus = await probeIMessagePrivateApi(
cliPathForProbe,
account.config.probeTimeoutMs ?? DEFAULT_IMESSAGE_PROBE_TIMEOUT_MS,

View File

@@ -3,7 +3,7 @@ import os from "node:os";
import path from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { runIMessageCatchup } from "./catchup-bridge.js";
import { resolveCatchupConfig, saveIMessageCatchupCursor } from "./catchup.js";
import { resolveCatchupConfig } from "./catchup.js";
import type { IMessagePayload } from "./types.js";
type RpcCall = {
@@ -157,32 +157,6 @@ describe("runIMessageCatchup", () => {
expect(summary.replayed).toBe(1);
});
it("does not crash on Date-invalid persisted cursor timestamps", async () => {
const log = vi.fn();
await saveIMessageCatchupCursor("default", {
lastSeenMs: 8_700_000_000_000_000,
lastSeenRowid: 10,
});
const { client, calls } = makeFakeClient(() => {
throw new Error("unexpected rpc");
});
const summary = await runIMessageCatchup({
client: client as never,
accountId: "default",
config: resolveCatchupConfig({ enabled: true, perRunLimit: 50, maxAgeMinutes: 60 }),
includeAttachments: false,
dispatchPayload: async () => {},
runtime: { log },
});
expect(summary.querySucceeded).toBe(false);
expect(calls).toEqual([]);
expect(log).toHaveBeenCalledWith(
expect.stringContaining("imessage catchup: invalid since timestamp"),
);
});
it("returns querySucceeded=false when chats.list throws", async () => {
vi.useFakeTimers();
vi.setSystemTime(new Date("2026-05-08T12:00:00Z"));

View File

@@ -1,4 +1,3 @@
import { timestampMsToIsoString } from "openclaw/plugin-sdk/number-runtime";
import { warn } from "openclaw/plugin-sdk/runtime-env";
import type { IMessageRpcClient } from "../client.js";
import {
@@ -89,11 +88,6 @@ export async function runIMessageCatchup(
const payloadByGuid = new Map<string, IMessagePayload>();
const fetchFn: CatchupFetchFn = async ({ sinceMs, sinceRowid, limit }) => {
const sinceISO = timestampMsToIsoString(sinceMs);
if (!sinceISO) {
warnLog(`imessage catchup: invalid since timestamp ${sinceMs}`);
return { resolved: false, rows: [] };
}
let chatsResult: { chats?: ChatsListEntry[] } | undefined;
try {
chatsResult = await client.request<{ chats?: ChatsListEntry[] }>(
@@ -106,6 +100,7 @@ export async function runIMessageCatchup(
return { resolved: false, rows: [] };
}
const chats = chatsResult?.chats ?? [];
const sinceISO = new Date(sinceMs).toISOString();
const collected: IMessageCatchupRow[] = [];
const perChatLimit = Math.min(limit, PER_CHAT_HISTORY_LIMIT_CAP);
let historyFetchFailed = false;

View File

@@ -85,12 +85,6 @@ const loadMatrixChannelRuntime = createLazyRuntimeNamedExport(
() => import("./channel.runtime.js"),
"matrixChannelRuntime",
);
let matrixDoctorModulePromise: Promise<typeof import("./doctor.js")> | null = null;
const loadMatrixDoctorModule = async () => {
matrixDoctorModulePromise ??= import("./doctor.js");
return await matrixDoctorModulePromise;
};
const meta = {
id: "matrix",
@@ -124,9 +118,9 @@ const matrixDoctor: ChannelDoctorAdapter = {
legacyConfigRules: MATRIX_LEGACY_CONFIG_RULES,
normalizeCompatibilityConfig: normalizeMatrixCompatibilityConfig,
runConfigSequence: async ({ cfg, env, shouldRepair }) =>
await (await loadMatrixDoctorModule()).runMatrixDoctorSequence({ cfg, env, shouldRepair }),
await (await import("./doctor.js")).runMatrixDoctorSequence({ cfg, env, shouldRepair }),
cleanStaleConfig: async ({ cfg }) =>
await (await loadMatrixDoctorModule()).cleanStaleMatrixPluginConfig(cfg),
await (await import("./doctor.js")).cleanStaleMatrixPluginConfig(cfg),
};
const listMatrixDirectoryPeersFromConfig =

View File

@@ -950,28 +950,6 @@ describe("matrix CLI verification commands", () => {
expect(console.log).toHaveBeenCalledWith("- BritdXC6iL (OpenClaw Gateway)");
});
it("omits invalid matrix device last seen timestamps", async () => {
listMatrixOwnDevicesMock.mockResolvedValue([
{
deviceId: "DEVICE123",
displayName: "OpenClaw Gateway",
lastSeenIp: "127.0.0.1",
lastSeenTs: 8_700_000_000_000_000,
current: true,
},
]);
const program = buildProgram();
await program.parseAsync(["matrix", "devices", "list", "--account", "poe"], { from: "user" });
expect(console.log).toHaveBeenCalledWith("Account: poe");
expect(console.log).toHaveBeenCalledWith("- DEVICE123 (current, OpenClaw Gateway)");
expect(console.log).toHaveBeenCalledWith(" Last IP: 127.0.0.1");
expect(
consoleLogMock.mock.calls.some(([message]) => String(message).startsWith(" Last seen:")),
).toBe(false);
});
it("prunes stale matrix gateway devices", async () => {
pruneMatrixStaleGatewayDevicesMock.mockResolvedValue({
before: [

View File

@@ -1,6 +1,6 @@
import type { Command } from "commander";
import { normalizeAccountId } from "openclaw/plugin-sdk/account-id";
import { parseStrictInteger, timestampMsToIsoString } from "openclaw/plugin-sdk/number-runtime";
import { parseStrictInteger } from "openclaw/plugin-sdk/number-runtime";
import type { ChannelSetupInput } from "openclaw/plugin-sdk/setup";
import { resolveMatrixAccount, resolveMatrixAccountConfig } from "./matrix/accounts.js";
import { listMatrixOwnDevices, pruneMatrixStaleGatewayDevices } from "./matrix/actions/devices.js";
@@ -216,9 +216,8 @@ function printMatrixOwnDevices(
console.log(
`- ${formatMatrixCliText(device.deviceId)}${labels.length ? ` (${labels.join(", ")})` : ""}`,
);
const lastSeenAt = timestampMsToIsoString(device.lastSeenTs);
if (lastSeenAt) {
printTimestamp(" Last seen", lastSeenAt);
if (device.lastSeenTs) {
printTimestamp(" Last seen", new Date(device.lastSeenTs).toISOString());
}
if (device.lastSeenIp) {
console.log(` Last IP: ${formatMatrixCliText(device.lastSeenIp)}`);

View File

@@ -1,7 +1,7 @@
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it, vi } from "vitest";
import { describe, expect, it, vi } from "vitest";
import { ensureMatrixStartupVerification } from "./startup-verification.js";
function createTempStateDir(): string {
@@ -80,10 +80,6 @@ function createHarness(params?: {
}
describe("ensureMatrixStartupVerification", () => {
afterEach(() => {
vi.restoreAllMocks();
});
it("skips automatic requests when the device is already verified", async () => {
const tempHome = createTempStateDir();
const harness = createHarness({ verified: true });
@@ -207,27 +203,6 @@ describe("ensureMatrixStartupVerification", () => {
expect(fs.existsSync(createStateFilePath(tempHome))).toBe(true);
});
it("falls back when startup verification nowMs is outside Date range", async () => {
vi.spyOn(Date, "now").mockReturnValue(Date.parse("2026-05-30T12:00:00.000Z"));
const tempHome = createTempStateDir();
const stateFilePath = createStateFilePath(tempHome);
const harness = createHarness();
const result = await ensureMatrixStartupVerification({
client: harness.client as never,
auth: createAuth(),
accountConfig: {},
stateFilePath,
nowMs: 8_640_000_000_000_001,
});
expect(result.kind).toBe("requested");
const state = JSON.parse(fs.readFileSync(stateFilePath, "utf-8")) as {
attemptedAt?: string;
};
expect(state.attemptedAt).toBe("2026-05-30T12:00:00.000Z");
});
it("keeps startup verification failures non-fatal", async () => {
const tempHome = createTempStateDir();
const harness = createHarness({

View File

@@ -1,7 +1,6 @@
import fs from "node:fs/promises";
import path from "node:path";
import { readJsonFileWithFallback, writeJsonFileAtomically } from "openclaw/plugin-sdk/json-store";
import { timestampMsToIsoString } from "openclaw/plugin-sdk/number-runtime";
import type { MatrixConfig } from "../../types.js";
import { resolveMatrixStoragePaths } from "../client/storage.js";
import type { MatrixAuth } from "../client/types.js";
@@ -96,14 +95,6 @@ function resolveRetryAfterMs(params: {
return remaining > 0 ? remaining : undefined;
}
function resolveStartupVerificationTimestamp(nowMs: unknown): string {
return (
timestampMsToIsoString(nowMs) ??
timestampMsToIsoString(Date.now()) ??
"1970-01-01T00:00:00.000Z"
);
}
function shouldHonorCooldown(params: {
state: MatrixStartupVerificationState | null;
verification: MatrixOwnDeviceVerificationStatus;
@@ -198,7 +189,6 @@ export async function ensureMatrixStartupVerification(params: {
);
const cooldownMs = cooldownHours * 60 * 60 * 1000;
const nowMs = params.nowMs ?? Date.now();
const attemptedAt = resolveStartupVerificationTimestamp(nowMs);
const state = await readStartupVerificationState(statePath);
const stateCooldownMs = resolveStateCooldownMs(state, cooldownMs);
if (shouldHonorCooldown({ state, verification, stateCooldownMs, nowMs })) {
@@ -218,7 +208,7 @@ export async function ensureMatrixStartupVerification(params: {
await writeJsonFileAtomically(statePath, {
userId: verification.userId,
deviceId: verification.deviceId,
attemptedAt,
attemptedAt: new Date(nowMs).toISOString(),
outcome: "requested",
requestId: request.id,
transactionId: request.transactionId,
@@ -234,7 +224,7 @@ export async function ensureMatrixStartupVerification(params: {
await writeJsonFileAtomically(statePath, {
userId: verification.userId,
deviceId: verification.deviceId,
attemptedAt,
attemptedAt: new Date(nowMs).toISOString(),
outcome: "failed",
error,
} satisfies MatrixStartupVerificationState).catch(() => {});

View File

@@ -166,26 +166,6 @@ describe("MatrixVerificationManager", () => {
expect(summary.phaseName).toBe("requested");
});
it("tracks verification requests when the process clock is outside the Date range", () => {
const manager = new MatrixVerificationManager();
const dateNowSpy = vi.spyOn(Date, "now").mockReturnValue(8_640_000_000_000_001);
try {
const summary = manager.trackVerificationRequest(
new MockVerificationRequest({
transactionId: "txn-invalid-clock",
phase: VerificationPhase.Requested,
}),
);
expect(summary.createdAt).toBe("1970-01-01T00:00:00.000Z");
expect(summary.updatedAt).toBe("1970-01-01T00:00:00.000Z");
expect(manager.listVerifications()).toHaveLength(1);
} finally {
dateNowSpy.mockRestore();
}
});
it("reuses the same tracked id for repeated transaction IDs", () => {
const manager = new MatrixVerificationManager();
const first = new MockVerificationRequest({

View File

@@ -4,10 +4,6 @@ import {
VerifierEvent,
} from "matrix-js-sdk/lib/crypto-api/verification.js";
import { VerificationMethod } from "matrix-js-sdk/lib/types.js";
import {
resolveDateTimestampMs,
resolveTimestampMsToIsoString,
} from "openclaw/plugin-sdk/number-runtime";
import { formatMatrixErrorMessage } from "../errors.js";
export type MatrixVerificationMethod = "sas" | "show-qr" | "scan-qr";
@@ -270,7 +266,7 @@ export class MatrixVerificationManager {
}
private touchVerificationSession(session: MatrixVerificationSession): void {
session.updatedAtMs = resolveDateTimestampMs(Date.now());
session.updatedAtMs = Date.now();
this.emitVerificationSummary(session);
}
@@ -321,8 +317,8 @@ export class MatrixVerificationManager {
hasReciprocateQr: Boolean(session.reciprocateQrCallbacks),
completed: phase === VerificationPhase.Done,
error: session.error,
createdAt: resolveTimestampMsToIsoString(session.createdAtMs),
updatedAt: resolveTimestampMsToIsoString(session.updatedAtMs),
createdAt: new Date(session.createdAtMs).toISOString(),
updatedAt: new Date(session.updatedAtMs).toISOString(),
};
}
@@ -598,7 +594,7 @@ export class MatrixVerificationManager {
}
}
const now = resolveDateTimestampMs(Date.now());
const now = Date.now();
const id = `verification-${++this.verificationSessionCounter}`;
const session: MatrixVerificationSession = {
id,

View File

@@ -23,3 +23,9 @@ export function registerMatrixSubagentHooks(api: OpenClawPluginApi): void {
return handleMatrixSubagentDeliveryTarget(event);
});
}
export {
handleMatrixSubagentDeliveryTarget,
handleMatrixSubagentEnded,
handleMatrixSubagentSpawning,
} from "./src/matrix/subagent-hooks.js";

View File

@@ -1,15 +1,11 @@
import fs from "node:fs/promises";
import path from "node:path";
import { afterEach, describe, expect, it, vi } from "vitest";
import { describe, expect, it } from "vitest";
import { writeDailyDreamingPhaseBlock, writeDeepDreamingReport } from "./dreaming-markdown.js";
import { createMemoryCoreTestHarness } from "./test-helpers.js";
const { createTempWorkspace } = createMemoryCoreTestHarness();
afterEach(() => {
vi.restoreAllMocks();
});
async function expectPathMissing(targetPath: string): Promise<void> {
const error = await fs.access(targetPath).then(
() => undefined,
@@ -59,25 +55,6 @@ describe("dreaming markdown storage", () => {
expect(content).toContain("- Candidate: remember the API key is fake");
});
it("falls back when the injected timestamp is outside Date range", async () => {
vi.spyOn(Date, "now").mockReturnValue(Date.UTC(2026, 4, 30, 12, 0, 0));
const workspaceDir = await createTempWorkspace("openclaw-dreaming-markdown-");
const result = await writeDailyDreamingPhaseBlock({
workspaceDir,
phase: "light",
bodyLines: ["- Candidate: bounded fallback"],
nowMs: 8_640_000_000_000_001,
timezone,
storage: {
mode: "inline",
separateReports: false,
},
});
expect(requireInlinePath(result)).toBe(path.join(workspaceDir, "memory", "2026-05-30.md"));
});
it("keeps multiple inline phases in the shared daily memory file", async () => {
const workspaceDir = await createTempWorkspace("openclaw-dreaming-markdown-");

View File

@@ -10,7 +10,6 @@ import {
replaceManagedMarkdownBlock,
withTrailingNewline,
} from "openclaw/plugin-sdk/memory-host-markdown";
import { resolveMemoryCoreNowMs, resolveMemoryCoreTimestamp } from "./time.js";
const DAILY_PHASE_HEADINGS: Record<Exclude<MemoryDreamingPhaseName, "deep">, string> = {
light: "## Light Sleep",
@@ -64,7 +63,7 @@ export async function writeDailyDreamingPhaseBlock(params: {
timezone?: string;
storage: MemoryDreamingStorageConfig;
}): Promise<{ inlinePath?: string; reportPath?: string }> {
const nowMs = resolveMemoryCoreNowMs(params.nowMs);
const nowMs = Number.isFinite(params.nowMs) ? (params.nowMs as number) : Date.now();
const body = params.bodyLines.length > 0 ? params.bodyLines.join("\n") : "- No notable updates.";
let inlinePath: string | undefined;
let reportPath: string | undefined;
@@ -108,7 +107,7 @@ export async function writeDailyDreamingPhaseBlock(params: {
await appendMemoryHostEvent(params.workspaceDir, {
type: "memory.dream.completed",
timestamp: resolveMemoryCoreTimestamp(nowMs),
timestamp: new Date(nowMs).toISOString(),
phase: params.phase,
...(inlinePath ? { inlinePath } : {}),
...(reportPath ? { reportPath } : {}),
@@ -132,14 +131,14 @@ export async function writeDeepDreamingReport(params: {
if (!shouldWriteSeparate(params.storage)) {
return undefined;
}
const nowMs = resolveMemoryCoreNowMs(params.nowMs);
const nowMs = Number.isFinite(params.nowMs) ? (params.nowMs as number) : Date.now();
const reportPath = resolveSeparateReportPath(params.workspaceDir, "deep", nowMs, params.timezone);
await fs.mkdir(path.dirname(reportPath), { recursive: true });
const body = params.bodyLines.length > 0 ? params.bodyLines.join("\n") : "- No durable changes.";
await fs.writeFile(reportPath, `# Deep Sleep\n\n${body}\n`, "utf-8");
await appendMemoryHostEvent(params.workspaceDir, {
type: "memory.dream.completed",
timestamp: resolveMemoryCoreTimestamp(nowMs),
timestamp: new Date(nowMs).toISOString(),
phase: "deep",
reportPath,
lineCount: params.bodyLines.length,

View File

@@ -1,18 +0,0 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { buildMemoryFlushPlan } from "./flush-plan.js";
describe("buildMemoryFlushPlan", () => {
afterEach(() => {
vi.restoreAllMocks();
});
it("falls back when the injected timestamp is outside Date range", () => {
vi.spyOn(Date, "now").mockReturnValue(Date.UTC(2026, 4, 30, 12, 0, 0));
const plan = buildMemoryFlushPlan({
nowMs: 8_640_000_000_000_001,
});
expect(plan?.relativePath).toBe("memory/2026-05-30.md");
});
});

View File

@@ -6,7 +6,6 @@ import {
type MemoryFlushPlan,
type OpenClawConfig,
} from "openclaw/plugin-sdk/memory-core-host-runtime-core";
import { resolveMemoryCoreNowMs } from "./time.js";
export const DEFAULT_MEMORY_FLUSH_SOFT_TOKENS = 4000;
export const DEFAULT_MEMORY_FLUSH_FORCE_TRANSCRIPT_BYTES = 2 * 1024 * 1024;
@@ -54,7 +53,7 @@ function formatDateStampInTimezone(nowMs: number, timezone: string): string {
if (year && month && day) {
return `${year}-${month}-${day}`;
}
return new Date(resolveMemoryCoreNowMs(nowMs)).toISOString().slice(0, 10);
return new Date(nowMs).toISOString().slice(0, 10);
}
function normalizeNonNegativeInt(value: unknown): number | null {
@@ -100,7 +99,7 @@ export function buildMemoryFlushPlan(
} = {},
): MemoryFlushPlan | null {
const resolved = params;
const nowMs = resolveMemoryCoreNowMs(resolved.nowMs);
const nowMs = Number.isFinite(resolved.nowMs) ? (resolved.nowMs as number) : Date.now();
const cfg = resolved.cfg;
const defaults = cfg?.agents?.defaults?.compaction?.memoryFlush;
if (defaults?.enabled === false) {

View File

@@ -1,7 +1,7 @@
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from "vitest";
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
vi.mock("openclaw/plugin-sdk/memory-host-events", () => ({
appendMemoryHostEvent: vi.fn(async () => {}),
@@ -40,10 +40,6 @@ describe("short-term promotion", () => {
await fs.rm(fixtureRoot, { recursive: true, force: true });
});
afterEach(() => {
vi.restoreAllMocks();
});
async function withTempWorkspace(run: (workspaceDir: string) => Promise<void>) {
const workspaceDir = path.join(fixtureRoot, `case-${caseId++}`);
await fs.mkdir(path.join(workspaceDir, "memory", ".dreams"), { recursive: true });
@@ -93,31 +89,19 @@ describe("short-term promotion", () => {
return candidate.promotedAt;
}
async function readRecallStoreEntries(workspaceDir: string): Promise<
async function readRecallStoreEntries(
workspaceDir: string,
): Promise<
Record<
string,
{
claimHash?: unknown;
firstRecalledAt?: unknown;
lastRecalledAt?: unknown;
recallCount?: unknown;
snippet?: unknown;
totalScore?: unknown;
}
{ claimHash?: unknown; recallCount?: unknown; snippet?: unknown; totalScore?: unknown }
>
> {
const raw = await fs.readFile(resolveShortTermRecallStorePath(workspaceDir), "utf-8");
const store = JSON.parse(raw) as {
entries?: Record<
string,
{
claimHash?: unknown;
firstRecalledAt?: unknown;
lastRecalledAt?: unknown;
recallCount?: unknown;
snippet?: unknown;
totalScore?: unknown;
}
{ claimHash?: unknown; recallCount?: unknown; snippet?: unknown; totalScore?: unknown }
>;
};
return store.entries ?? {};
@@ -176,35 +160,6 @@ describe("short-term promotion", () => {
});
});
it("falls back when the injected recall timestamp is outside Date range", async () => {
vi.spyOn(Date, "now").mockReturnValue(Date.UTC(2026, 4, 30, 12, 0, 0));
await withTempWorkspace(async (workspaceDir) => {
const notePath = await writeDailyMemoryNote(workspaceDir, "2026-05-30", [
"Bounded recall timestamp note.",
]);
await recordShortTermRecalls({
workspaceDir,
query: "bounded recall",
nowMs: 8_640_000_000_000_001,
results: [
{
path: path.relative(workspaceDir, notePath).replaceAll("\\", "/"),
source: "memory",
startLine: 1,
endLine: 1,
score: 0.9,
snippet: "Bounded recall timestamp note.",
},
],
});
const [entry] = Object.values(await readRecallStoreEntries(workspaceDir));
expect(entry?.firstRecalledAt).toBe("2026-05-30T12:00:00.000Z");
expect(entry?.lastRecalledAt).toBe("2026-05-30T12:00:00.000Z");
});
});
it("records short-term recall for notes stored in spaced and Unicode memory subdirectories", async () => {
await withTempWorkspace(async (workspaceDir) => {
const spacedPath = await writeDailyMemoryNoteInSubdir(

View File

@@ -21,7 +21,6 @@ import {
} from "./concept-vocabulary.js";
import { asRecord } from "./dreaming-shared.js";
import { compactMemoryForBudget, DEFAULT_MEMORY_FILE_MAX_CHARS } from "./memory-budget.js";
import { resolveMemoryCoreNowMs, resolveMemoryCoreTimestamp } from "./time.js";
const SHORT_TERM_PATH_RE = /(?:^|\/)memory\/(?:[^/]+\/)*(\d{4})-(\d{2})-(\d{2})(?:-[^/]+)?\.md$/;
const DREAMING_MEMORY_PATH_RE = /(?:^|\/)memory\/dreaming\//;
@@ -1053,8 +1052,8 @@ export async function recordShortTermRecalls(params: {
return;
}
const nowMs = resolveMemoryCoreNowMs(params.nowMs);
const nowIso = resolveMemoryCoreTimestamp(nowMs);
const nowMs = Number.isFinite(params.nowMs) ? (params.nowMs as number) : Date.now();
const nowIso = new Date(nowMs).toISOString();
const signalType = params.signalType ?? "recall";
const queryHash = hashQuery(query);
const todayBucket =
@@ -1200,8 +1199,8 @@ export async function recordGroundedShortTermCandidates(params: {
return;
}
const nowMs = resolveMemoryCoreNowMs(params.nowMs);
const nowIso = resolveMemoryCoreTimestamp(nowMs);
const nowMs = Number.isFinite(params.nowMs) ? (params.nowMs as number) : Date.now();
const nowIso = new Date(nowMs).toISOString();
const fallbackDayBucket = formatMemoryDreamingDay(nowMs, params.timezone);
await withShortTermLock(workspaceDir, async () => {
const store = await readStore(workspaceDir, nowIso);
@@ -1282,8 +1281,8 @@ export async function recordDreamingPhaseSignals(params: {
if (keys.length === 0) {
return;
}
const nowMs = resolveMemoryCoreNowMs(params.nowMs);
const nowIso = resolveMemoryCoreTimestamp(nowMs);
const nowMs = Number.isFinite(params.nowMs) ? (params.nowMs as number) : Date.now();
const nowIso = new Date(nowMs).toISOString();
await withShortTermLock(workspaceDir, async () => {
const [store, phaseSignals] = await Promise.all([
@@ -1335,8 +1334,8 @@ export async function recordRemConsideredPhaseSignals(params: {
if (keys.length === 0) {
return;
}
const nowMs = resolveMemoryCoreNowMs(params.nowMs);
const nowIso = resolveMemoryCoreTimestamp(nowMs);
const nowMs = Number.isFinite(params.nowMs) ? (params.nowMs as number) : Date.now();
const nowIso = new Date(nowMs).toISOString();
await withShortTermLock(workspaceDir, async () => {
const [store, phaseSignals] = await Promise.all([
@@ -1377,8 +1376,8 @@ export async function readLightStagedKeys(params: {
if (!workspaceDir) {
return new Set();
}
const nowMs = resolveMemoryCoreNowMs(params.nowMs);
const nowIso = resolveMemoryCoreTimestamp(nowMs);
const nowMs = Number.isFinite(params.nowMs) ? (params.nowMs as number) : Date.now();
const nowIso = new Date(nowMs).toISOString();
const store = await readPhaseSignalStore(workspaceDir, nowIso);
const keys = new Set<string>();
for (const [key, entry] of Object.entries(store.entries)) {
@@ -1410,8 +1409,8 @@ export async function rankShortTermPromotionCandidates(
return [];
}
const nowMs = resolveMemoryCoreNowMs(options.nowMs);
const nowIso = resolveMemoryCoreTimestamp(nowMs);
const nowMs = Number.isFinite(options.nowMs) ? (options.nowMs as number) : Date.now();
const nowIso = new Date(nowMs).toISOString();
const minScore = toFiniteScore(options.minScore, DEFAULT_PROMOTION_MIN_SCORE);
const minRecallCount = toFiniteNonNegativeInt(
options.minRecallCount,
@@ -1551,8 +1550,8 @@ export async function readShortTermRecallEntries(params: {
if (!workspaceDir) {
return [];
}
const nowMs = resolveMemoryCoreNowMs(params.nowMs);
const nowIso = resolveMemoryCoreTimestamp(nowMs);
const nowMs = Number.isFinite(params.nowMs) ? (params.nowMs as number) : Date.now();
const nowIso = new Date(nowMs).toISOString();
const store = await readStore(workspaceDir, nowIso);
return Object.values(store.entries).filter(
(entry): entry is ShortTermRecallEntry =>
@@ -1839,8 +1838,8 @@ export async function applyShortTermPromotions(
options: ApplyShortTermPromotionsOptions,
): Promise<ApplyShortTermPromotionsResult> {
const workspaceDir = options.workspaceDir.trim();
const nowMs = resolveMemoryCoreNowMs(options.nowMs);
const nowIso = resolveMemoryCoreTimestamp(nowMs);
const nowMs = Number.isFinite(options.nowMs) ? (options.nowMs as number) : Date.now();
const nowIso = new Date(nowMs).toISOString();
const limit = Number.isFinite(options.limit)
? Math.max(0, Math.floor(options.limit as number))
: options.candidates.length;

View File

@@ -1,10 +0,0 @@
import { timestampMsToIsoString } from "openclaw/plugin-sdk/number-runtime";
export function resolveMemoryCoreNowMs(nowMs: unknown): number {
return timestampMsToIsoString(nowMs) === undefined ? Date.now() : (nowMs as number);
}
export function resolveMemoryCoreTimestamp(nowMs: unknown): string {
const timestampMs = resolveMemoryCoreNowMs(nowMs);
return timestampMsToIsoString(timestampMs) ?? new Date().toISOString();
}

View File

@@ -6,7 +6,6 @@ import {
replaceManagedMarkdownBlock,
withTrailingNewline,
} from "openclaw/plugin-sdk/memory-host-markdown";
import { timestampMsToIsoString } from "openclaw/plugin-sdk/number-runtime";
import { uniqueStrings } from "openclaw/plugin-sdk/string-coerce-runtime";
import { compileMemoryWikiVault } from "./compile.js";
import type { ResolvedMemoryWikiConfig } from "./config.js";
@@ -17,7 +16,6 @@ import {
WIKI_RELATED_END_MARKER,
WIKI_RELATED_START_MARKER,
} from "./markdown.js";
import { resolveMemoryWikiTimestamp } from "./time.js";
import { initializeMemoryWikiVault } from "./vault.js";
const CHATGPT_PREFERENCE_SIGNAL_RE =
@@ -205,7 +203,7 @@ function isoFromUnix(raw: unknown): string | undefined {
if (!Number.isFinite(numeric)) {
return undefined;
}
return timestampMsToIsoString(numeric * 1000);
return new Date(numeric * 1000).toISOString();
}
function cleanMessageText(value: string): string {
@@ -747,7 +745,7 @@ export async function importChatGptConversations(params: {
let updatedCount = 0;
let skippedCount = 0;
let runId: string | undefined;
const nowIso = resolveMemoryWikiTimestamp(params.nowMs);
const nowIso = new Date(params.nowMs ?? Date.now()).toISOString();
let importRunRecord: ChatGptImportRunRecord | undefined;
let importRunDir = "";

View File

@@ -605,30 +605,4 @@ cli note
.then((entries) => entries.filter((entry) => entry !== "index.md")),
).resolves.toStrictEqual([]);
});
it("imports ChatGPT exports with out-of-range Unix timestamps", async () => {
const { rootDir, config } = await createCliVault({ initialize: true });
const exportDir = await createChatGptExport(rootDir);
const conversationsPath = path.join(exportDir, "conversations.json");
const conversations = JSON.parse(await fs.readFile(conversationsPath, "utf8")) as Array<
Record<string, unknown>
>;
conversations[0].update_time = 9_000_000_000_000;
await fs.writeFile(conversationsPath, `${JSON.stringify(conversations, null, 2)}\n`, "utf8");
const result = await runWikiChatGptImport({
config,
exportPath: exportDir,
json: true,
});
expect(result.createdCount).toBe(1);
const sourceFile = (await fs.readdir(path.join(rootDir, "sources"))).find(
(entry) => entry !== "index.md",
);
expect(sourceFile).toBeDefined();
const pageContent = await fs.readFile(path.join(rootDir, "sources", sourceFile ?? ""), "utf8");
expect(pageContent).toContain("- Created: 2024-04-06T00:26:40.000Z");
expect(pageContent).toContain("- Updated: 2024-04-06T00:26:40.000Z");
});
});

View File

@@ -5,7 +5,6 @@ import { compileMemoryWikiVault } from "./compile.js";
import type { ResolvedMemoryWikiConfig } from "./config.js";
import { appendMemoryWikiLog } from "./log.js";
import { renderMarkdownFence, renderWikiMarkdown, slugifyWikiSegment } from "./markdown.js";
import { resolveMemoryWikiTimestamp } from "./time.js";
import { initializeMemoryWikiVault } from "./vault.js";
type IngestMemoryWikiSourceResult = {
@@ -49,7 +48,7 @@ export async function ingestMemoryWikiSource(params: {
const pageRelativePath = path.join("sources", `${slug}.md`);
const pagePath = path.join(params.config.vault.path, pageRelativePath);
const created = !(await pathExists(pagePath));
const timestamp = resolveMemoryWikiTimestamp(params.nowMs);
const timestamp = new Date(params.nowMs ?? Date.now()).toISOString();
const markdown = renderWikiMarkdown({
frontmatter: {

View File

@@ -1,48 +0,0 @@
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { writeImportedSourcePage } from "./source-page-shared.js";
describe("writeImportedSourcePage", () => {
let suiteRoot: string;
beforeEach(async () => {
suiteRoot = await fs.mkdtemp(path.join(os.tmpdir(), "memory-wiki-source-page-"));
});
afterEach(async () => {
vi.useRealTimers();
await fs.rm(suiteRoot, { recursive: true, force: true });
});
it("falls back when the source mtime is outside the Date range", async () => {
vi.useFakeTimers();
vi.setSystemTime(new Date("2026-05-01T12:00:00.000Z"));
const sourcePath = path.join(suiteRoot, "source.txt");
await fs.writeFile(sourcePath, "source body", "utf8");
const state: Parameters<typeof writeImportedSourcePage>[0]["state"] = {
entries: {},
version: 1,
};
const result = await writeImportedSourcePage({
vaultRoot: suiteRoot,
syncKey: "unsafe:source",
sourcePath,
sourceUpdatedAtMs: 8_700_000_000_000_000,
sourceSize: 11,
renderFingerprint: "fingerprint",
pagePath: "pages/source.md",
group: "unsafe-local",
state,
buildRendered: (raw, updatedAt) => `updatedAt: ${updatedAt}\n${raw}`,
});
await expect(fs.readFile(path.join(suiteRoot, "pages/source.md"), "utf8")).resolves.toBe(
"updatedAt: 2026-05-01T12:00:00.000Z\nsource body",
);
expect(result).toEqual({ pagePath: "pages/source.md", changed: true, created: true });
expect(state.entries["unsafe:source"]?.sourceUpdatedAtMs).toBe(8_700_000_000_000_000);
});
});

View File

@@ -1,5 +1,4 @@
import fs from "node:fs/promises";
import { timestampMsToIsoString } from "openclaw/plugin-sdk/number-runtime";
import { FsSafeError, root as fsRoot } from "openclaw/plugin-sdk/security-runtime";
import {
setImportedSourceEntry,
@@ -49,7 +48,7 @@ export async function writeImportedSourcePage(params: {
throw error;
});
const created = !pageStat;
const updatedAt = timestampMsToIsoString(params.sourceUpdatedAtMs) ?? new Date().toISOString();
const updatedAt = new Date(params.sourceUpdatedAtMs).toISOString();
const shouldSkip = await shouldSkipImportedSourceWrite({
vaultRoot: params.vaultRoot,
syncKey: params.syncKey,

View File

@@ -1,20 +0,0 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { resolveMemoryWikiTimestamp } from "./time.js";
describe("resolveMemoryWikiTimestamp", () => {
afterEach(() => {
vi.restoreAllMocks();
});
it("uses valid injected timestamps", () => {
expect(resolveMemoryWikiTimestamp(Date.UTC(2026, 3, 5, 12, 0, 0))).toBe(
"2026-04-05T12:00:00.000Z",
);
});
it("falls back when injected timestamps are outside Date range", () => {
vi.spyOn(Date, "now").mockReturnValue(Date.UTC(2026, 4, 30, 12, 0, 0));
expect(resolveMemoryWikiTimestamp(8_640_000_000_000_001)).toBe("2026-05-30T12:00:00.000Z");
});
});

View File

@@ -1,7 +0,0 @@
import { timestampMsToIsoString } from "openclaw/plugin-sdk/number-runtime";
export function resolveMemoryWikiTimestamp(nowMs?: number): string {
return (
timestampMsToIsoString(nowMs) ?? timestampMsToIsoString(Date.now()) ?? new Date().toISOString()
);
}

View File

@@ -7,7 +7,6 @@ import {
import { FsSafeError, pathExists, root as fsRoot } from "openclaw/plugin-sdk/security-runtime";
import type { ResolvedMemoryWikiConfig } from "./config.js";
import { appendMemoryWikiLog } from "./log.js";
import { resolveMemoryWikiTimestamp } from "./time.js";
export const WIKI_VAULT_DIRECTORIES = [
"entities",
@@ -129,7 +128,7 @@ export async function initializeMemoryWikiVault(
JSON.stringify(
{
version: 1,
createdAt: resolveMemoryWikiTimestamp(options?.nowMs),
createdAt: new Date(options?.nowMs ?? Date.now()).toISOString(),
renderMode: config.vault.renderMode,
},
null,
@@ -143,7 +142,7 @@ export async function initializeMemoryWikiVault(
if (createdDirectories.length > 0 || createdFiles.length > 0) {
await appendMemoryWikiLog(rootDir, {
type: "init",
timestamp: resolveMemoryWikiTimestamp(options?.nowMs),
timestamp: new Date(options?.nowMs ?? Date.now()).toISOString(),
details: {
createdDirectories: createdDirectories.map((dir) => path.relative(rootDir, dir) || "."),
createdFiles: createdFiles.map((file) => path.relative(rootDir, file)),

View File

@@ -22,13 +22,6 @@ import {
} from "../policy-state.js";
import { POLICY_TOOL_GROUPS } from "../tool-policy-conformance.js";
let fsPromisesModulePromise: Promise<typeof import("node:fs/promises")> | null = null;
const loadFsPromisesModule = async () => {
fsPromisesModulePromise ??= import("node:fs/promises");
return await fsPromisesModulePromise;
};
const CHECK_IDS = {
policyAttestationMismatch: "policy/attestation-hash-mismatch",
policyDeniedChannelProvider: "policy/channels-denied-provider",
@@ -5275,7 +5268,7 @@ async function readPolicyFile(
const displayName = policyDisplayName(ctx);
const path = resolveWorkspacePath(ctx, policyPathSetting(ctx));
try {
const fs = await loadFsPromisesModule();
const fs = await import("node:fs/promises");
return {
raw: await fs.readFile(path, "utf-8"),
path,
@@ -5296,7 +5289,7 @@ async function readWorkspaceFile(
): Promise<{ raw: string; path: string } | null> {
const path = resolveWorkspacePath(ctx, fileName);
try {
const fs = await loadFsPromisesModule();
const fs = await import("node:fs/promises");
return { raw: await fs.readFile(path, "utf-8"), path };
} catch (err) {
if (isNotFound(err)) {

View File

@@ -38,14 +38,6 @@ import {
import type { FetchMediaOptions, FetchMediaResult } from "../engine/adapter/types.js";
import { getBridgeLogger } from "./logger.js";
let mediaRuntimeModulePromise: Promise<typeof import("openclaw/plugin-sdk/media-runtime")> | null =
null;
const loadMediaRuntimeModule = async () => {
mediaRuntimeModulePromise ??= import("openclaw/plugin-sdk/media-runtime");
return await mediaRuntimeModulePromise;
};
function createBuiltinAdapter(): PlatformAdapter {
return {
async validateRemoteUrl(_url: string, _options?: { allowPrivate?: boolean }): Promise<void> {
@@ -60,7 +52,7 @@ function createBuiltinAdapter(): PlatformAdapter {
},
async downloadFile(url: string, destDir: string, filename?: string): Promise<string> {
const { readRemoteMediaBuffer } = await loadMediaRuntimeModule();
const { readRemoteMediaBuffer } = await import("openclaw/plugin-sdk/media-runtime");
const result = await readRemoteMediaBuffer({ url, filePathHint: filename });
const fs = await import("node:fs");
const path = await import("node:path");
@@ -73,7 +65,7 @@ function createBuiltinAdapter(): PlatformAdapter {
},
async fetchMedia(options: FetchMediaOptions): Promise<FetchMediaResult> {
const { readRemoteMediaBuffer } = await loadMediaRuntimeModule();
const { readRemoteMediaBuffer } = await import("openclaw/plugin-sdk/media-runtime");
const result = await readRemoteMediaBuffer({
url: options.url,
filePathHint: options.filePathHint,

View File

@@ -37,14 +37,6 @@ function loadGatewayModule(): Promise<typeof import("./bridge/gateway.js")> {
return gatewayModulePromise;
}
let outboundMessagingModulePromise:
| Promise<typeof import("./engine/messaging/outbound.js")>
| undefined;
function loadOutboundMessagingModule(): Promise<typeof import("./engine/messaging/outbound.js")> {
outboundMessagingModulePromise ??= import("./engine/messaging/outbound.js");
return outboundMessagingModulePromise;
}
function createQQBotSendReceipt(params: {
messageId?: string;
target: string;
@@ -77,7 +69,7 @@ async function sendQQBotText(params: {
// platform adapter, etc.) have executed before engine code runs.
await loadGatewayModule();
const account = resolveQQBotAccount(params.cfg, params.accountId);
const { sendText } = await loadOutboundMessagingModule();
const { sendText } = await import("./engine/messaging/outbound.js");
const result = await sendText({
to: params.to,
text: params.text,
@@ -108,7 +100,7 @@ async function sendQQBotMedia(params: {
// Same guard as sendText — ensure adapters are registered.
await loadGatewayModule();
const account = resolveQQBotAccount(params.cfg, params.accountId);
const { sendMedia } = await loadOutboundMessagingModule();
const { sendMedia } = await import("./engine/messaging/outbound.js");
const result = await sendMedia({
to: params.to,
text: params.text ?? "",

View File

@@ -86,25 +86,4 @@ describe("QQBot token manager", () => {
expiresAt: Date.now(),
});
});
it("does not throw while logging fetched tokens when the process clock is outside the Date range", async () => {
const logger = { debug: vi.fn(), info: vi.fn(), error: vi.fn() };
const dateNowSpy = vi.spyOn(Date, "now").mockReturnValue(8_640_000_000_000_001);
mockGuardedTokenResponse('{"access_token":"token-1","expires_in":7200}', {
status: 200,
headers: { "content-type": "application/json" },
});
const manager = new TokenManager({ logger });
try {
await expect(manager.getAccessToken("app-id", "secret")).resolves.toBe("token-1");
} finally {
dateNowSpy.mockRestore();
}
expect(manager.getStatus("app-id").expiresAt).toBe(7_200_000);
expect(logger.debug).toHaveBeenCalledWith(
"[qqbot:token:app-id] Cached, expires at: 1970-01-01T02:00:00.000Z",
);
});
});

View File

@@ -6,12 +6,7 @@
* globals, fully supporting multi-account concurrent operation.
*/
import {
parseStrictPositiveInteger,
resolveDateTimestampMs,
resolveExpiresAtMsFromDurationSeconds,
resolveTimestampMsToIsoString,
} from "openclaw/plugin-sdk/number-runtime";
import { parseStrictPositiveInteger } from "openclaw/plugin-sdk/number-runtime";
import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/ssrf-runtime";
import type { EngineLogger } from "../types.js";
import { formatErrorMessage } from "../utils/format.js";
@@ -278,14 +273,10 @@ export class TokenManager {
throw new Error(`Failed to get access_token: ${JSON.stringify(data)}`);
}
const nowMs = resolveDateTimestampMs(Date.now());
const expiresAt =
resolveExpiresAtMsFromDurationSeconds(resolveTokenExpiresInSeconds(data.expires_in), {
nowMs,
}) ?? nowMs;
const expiresAt = Date.now() + resolveTokenExpiresInSeconds(data.expires_in) * 1000;
this.cache.set(appId, { token: data.access_token, expiresAt, appId });
this.logger?.debug?.(
`[qqbot:token:${appId}] Cached, expires at: ${resolveTimestampMsToIsoString(expiresAt)}`,
`[qqbot:token:${appId}] Cached, expires at: ${new Date(expiresAt).toISOString()}`,
);
return data.access_token;

View File

@@ -4,10 +4,7 @@ import { resolveApprovalOverGateway } from "openclaw/plugin-sdk/approval-gateway
import { parseExecApprovalCommandText } from "openclaw/plugin-sdk/approval-reply-runtime";
import { resolveCommandAuthorization } from "openclaw/plugin-sdk/command-auth-native";
import { requestHeartbeat } from "openclaw/plugin-sdk/heartbeat-runtime";
import {
parseStrictFiniteNumber,
timestampMsToIsoString,
} from "openclaw/plugin-sdk/number-runtime";
import { parseStrictFiniteNumber } from "openclaw/plugin-sdk/number-runtime";
import {
normalizeOptionalString,
normalizeUniqueTrimmedStringList,
@@ -322,10 +319,7 @@ function formatInteractionSelectionLabel(params: {
return params.summary.selectedTime;
}
if (typeof params.summary.selectedDateTime === "number") {
const selectedDateTime = timestampMsToIsoString(params.summary.selectedDateTime * 1000);
if (selectedDateTime) {
return selectedDateTime;
}
return new Date(params.summary.selectedDateTime * 1000).toISOString();
}
if (params.summary.richTextPreview) {
return params.summary.richTextPreview;

View File

@@ -2075,54 +2075,6 @@ describe("registerSlackInteractionEvents", () => {
expect(payload.selectedDateTime).toBe(1_771_700_200);
});
it("falls back when Slack datetime selection is outside Date range", async () => {
const { ctx, app, getHandler } = createContext();
registerSlackInteractionEvents({ ctx: ctx as never });
const handler = getHandler();
const ack = vi.fn().mockResolvedValue(undefined);
await handler({
ack,
body: {
user: { id: "U333" },
channel: { id: "C3" },
message: {
ts: "555.669",
text: "fallback",
blocks: [
{
type: "actions",
block_id: "datetime_block",
elements: [{ type: "datetimepicker", action_id: "openclaw:datetime" }],
},
],
},
},
action: {
type: "datetimepicker",
action_id: "openclaw:datetime",
block_id: "datetime_block",
selected_date_time: 9_000_000_000_000,
},
});
expectRecordFields(chatUpdateCall(app), {
channel: "C3",
ts: "555.669",
blocks: [
{
type: "context",
elements: [
{
type: "mrkdwn",
text: ":white_check_mark: *openclaw:datetime* selected by <@U333>",
},
],
},
],
});
});
it("captures workflow button trigger metadata", async () => {
enqueueSystemEventMock.mockClear();
const { ctx, getHandler } = createContext();

View File

@@ -583,37 +583,6 @@ describe("voice-call plugin", () => {
expect(runtimeStub.manager.speak).not.toHaveBeenCalled();
});
it("reports stale call history with invalid ended timestamps", async () => {
runtimeStub.manager.getCall = vi.fn(() => undefined);
runtimeStub.manager.getCallByProviderCallId = vi.fn(() => undefined);
runtimeStub.manager.getCallHistory = vi.fn(async () => [
createCallRecord({
callId: "call-1",
providerCallId: "CA123",
state: "completed",
endReason: "completed",
endedAt: Number.POSITIVE_INFINITY,
}),
]);
const { methods } = setup({ provider: "mock" });
const handler = methods.get("voicecall.speak") as
| ((ctx: {
params: Record<string, unknown>;
respond: ReturnType<typeof vi.fn>;
}) => Promise<void>)
| undefined;
const respond = vi.fn();
await handler?.({ params: { callId: "CA123", message: "hello" }, respond });
const [ok, , error] = firstRespondCall(respond);
expect(ok).toBe(false);
expect(error?.message).toContain("call is not active");
expect(error?.message).toContain("last state=completed");
expect(error?.message).toContain("endReason=completed");
expect(error?.message).not.toContain("endedAt=");
});
it("normalizes legacy config through runtime creation and warns to run doctor", async () => {
const { methods } = setup({
enabled: true,

View File

@@ -1,6 +1,5 @@
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
import { ErrorCodes, errorShape } from "openclaw/plugin-sdk/gateway-runtime";
import { timestampMsToIsoString } from "openclaw/plugin-sdk/number-runtime";
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
import { Type } from "typebox";
import {
@@ -348,11 +347,10 @@ export default definePluginEntry({
if (!call) {
return undefined;
}
const endedAt = timestampMsToIsoString(call.endedAt);
const details = [
`last state=${call.state}`,
call.endReason ? `endReason=${call.endReason}` : undefined,
endedAt ? `endedAt=${endedAt}` : undefined,
call.endedAt ? `endedAt=${new Date(call.endedAt).toISOString()}` : undefined,
].filter(Boolean);
return `call is not active (${details.join(", ")})`;
};

545
npm-shrinkwrap.json generated
View File

@@ -74,6 +74,7 @@
"node": ">=22.19.0"
},
"optionalDependencies": {
"sharp": "0.34.5",
"sqlite-vec": "0.1.9"
}
},
@@ -167,6 +168,16 @@
"node": ">=22.19.0"
}
},
"node_modules/@emnapi/runtime": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz",
"integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==",
"license": "MIT",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@google/genai": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/@google/genai/-/genai-2.6.0.tgz",
@@ -254,6 +265,472 @@
"hono": "^4"
}
},
"node_modules/@img/colour": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz",
"integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==",
"license": "MIT",
"optional": true,
"engines": {
"node": ">=18"
}
},
"node_modules/@img/sharp-darwin-arm64": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz",
"integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==",
"cpu": [
"arm64"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-darwin-arm64": "1.2.4"
}
},
"node_modules/@img/sharp-darwin-x64": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz",
"integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==",
"cpu": [
"x64"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-darwin-x64": "1.2.4"
}
},
"node_modules/@img/sharp-libvips-darwin-arm64": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz",
"integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==",
"cpu": [
"arm64"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"darwin"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-darwin-x64": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz",
"integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==",
"cpu": [
"x64"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"darwin"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-arm": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz",
"integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==",
"cpu": [
"arm"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-arm64": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz",
"integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==",
"cpu": [
"arm64"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-ppc64": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz",
"integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==",
"cpu": [
"ppc64"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-riscv64": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz",
"integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==",
"cpu": [
"riscv64"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-s390x": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz",
"integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==",
"cpu": [
"s390x"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-x64": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz",
"integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==",
"cpu": [
"x64"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linuxmusl-arm64": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz",
"integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==",
"cpu": [
"arm64"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linuxmusl-x64": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz",
"integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==",
"cpu": [
"x64"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-linux-arm": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz",
"integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==",
"cpu": [
"arm"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-arm": "1.2.4"
}
},
"node_modules/@img/sharp-linux-arm64": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz",
"integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==",
"cpu": [
"arm64"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-arm64": "1.2.4"
}
},
"node_modules/@img/sharp-linux-ppc64": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz",
"integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==",
"cpu": [
"ppc64"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-ppc64": "1.2.4"
}
},
"node_modules/@img/sharp-linux-riscv64": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz",
"integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==",
"cpu": [
"riscv64"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-riscv64": "1.2.4"
}
},
"node_modules/@img/sharp-linux-s390x": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz",
"integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==",
"cpu": [
"s390x"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-s390x": "1.2.4"
}
},
"node_modules/@img/sharp-linux-x64": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz",
"integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==",
"cpu": [
"x64"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-x64": "1.2.4"
}
},
"node_modules/@img/sharp-linuxmusl-arm64": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz",
"integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==",
"cpu": [
"arm64"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linuxmusl-arm64": "1.2.4"
}
},
"node_modules/@img/sharp-linuxmusl-x64": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz",
"integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==",
"cpu": [
"x64"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linuxmusl-x64": "1.2.4"
}
},
"node_modules/@img/sharp-wasm32": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz",
"integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==",
"cpu": [
"wasm32"
],
"license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
"optional": true,
"dependencies": {
"@emnapi/runtime": "^1.7.0"
},
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-win32-arm64": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz",
"integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==",
"cpu": [
"arm64"
],
"license": "Apache-2.0 AND LGPL-3.0-or-later",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-win32-ia32": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz",
"integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==",
"cpu": [
"ia32"
],
"license": "Apache-2.0 AND LGPL-3.0-or-later",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-win32-x64": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz",
"integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==",
"cpu": [
"x64"
],
"license": "Apache-2.0 AND LGPL-3.0-or-later",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@isaacs/fs-minipass": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz",
@@ -998,6 +1475,16 @@
"node": ">= 0.8"
}
},
"node_modules/detect-libc": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
"license": "Apache-2.0",
"optional": true,
"engines": {
"node": ">=8"
}
},
"node_modules/diff": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/diff/-/diff-9.0.0.tgz",
@@ -2751,6 +3238,19 @@
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"license": "MIT"
},
"node_modules/semver": {
"version": "7.8.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.1.tgz",
"integrity": "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==",
"license": "ISC",
"optional": true,
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/send": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz",
@@ -2814,6 +3314,51 @@
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
"license": "ISC"
},
"node_modules/sharp": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz",
"integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==",
"hasInstallScript": true,
"license": "Apache-2.0",
"optional": true,
"dependencies": {
"@img/colour": "^1.0.0",
"detect-libc": "^2.1.2",
"semver": "^7.7.3"
},
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-darwin-arm64": "0.34.5",
"@img/sharp-darwin-x64": "0.34.5",
"@img/sharp-libvips-darwin-arm64": "1.2.4",
"@img/sharp-libvips-darwin-x64": "1.2.4",
"@img/sharp-libvips-linux-arm": "1.2.4",
"@img/sharp-libvips-linux-arm64": "1.2.4",
"@img/sharp-libvips-linux-ppc64": "1.2.4",
"@img/sharp-libvips-linux-riscv64": "1.2.4",
"@img/sharp-libvips-linux-s390x": "1.2.4",
"@img/sharp-libvips-linux-x64": "1.2.4",
"@img/sharp-libvips-linuxmusl-arm64": "1.2.4",
"@img/sharp-libvips-linuxmusl-x64": "1.2.4",
"@img/sharp-linux-arm": "0.34.5",
"@img/sharp-linux-arm64": "0.34.5",
"@img/sharp-linux-ppc64": "0.34.5",
"@img/sharp-linux-riscv64": "0.34.5",
"@img/sharp-linux-s390x": "0.34.5",
"@img/sharp-linux-x64": "0.34.5",
"@img/sharp-linuxmusl-arm64": "0.34.5",
"@img/sharp-linuxmusl-x64": "0.34.5",
"@img/sharp-wasm32": "0.34.5",
"@img/sharp-win32-arm64": "0.34.5",
"@img/sharp-win32-ia32": "0.34.5",
"@img/sharp-win32-x64": "0.34.5"
}
},
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",

View File

@@ -1941,6 +1941,7 @@
"vitest": "4.1.7"
},
"optionalDependencies": {
"sharp": "0.34.5",
"sqlite-vec": "0.1.9"
},
"overrides": {

View File

@@ -1,34 +0,0 @@
{
"name": "@openclaw/code-mode-runtime",
"version": "0.0.0-private",
"private": true,
"files": [
"dist"
],
"type": "module",
"main": "./dist/index.mjs",
"types": "./dist/index.d.mts",
"exports": {
".": {
"types": "./dist/index.d.mts",
"import": "./dist/index.mjs",
"default": "./dist/index.mjs"
},
"./types": {
"types": "./dist/types.d.mts",
"import": "./dist/types.mjs",
"default": "./dist/types.mjs"
},
"./worker": {
"types": "./dist/worker.d.mts",
"import": "./dist/worker.mjs",
"default": "./dist/worker.mjs"
}
},
"scripts": {
"build": "tsdown src/index.ts src/types.ts src/worker.ts --no-config --platform node --format esm --dts --out-dir dist --clean"
},
"dependencies": {
"quickjs-wasi": "3.0.0"
}
}

View File

@@ -1,18 +0,0 @@
import path from "node:path";
import { fileURLToPath } from "node:url";
export type {
CodeModeBridgeMethod,
CodeModeFailureCode,
CodeModePendingBridgeRequest,
CodeModeRuntimeConfig,
CodeModeSettledBridgeRequest,
CodeModeWorkerInput,
CodeModeWorkerResult,
} from "./types.js";
export function resolveCodeModeRuntimeWorkerUrl(currentModuleUrl = import.meta.url): URL {
const currentPath = fileURLToPath(currentModuleUrl);
const extension = path.extname(currentPath) || ".js";
return new URL(`./worker${extension}`, currentModuleUrl);
}

View File

@@ -1,62 +0,0 @@
export type CodeModeBridgeMethod = "search" | "describe" | "call" | "yield";
export type CodeModeRuntimeConfig = {
timeoutMs: number;
memoryLimitBytes: number;
maxPendingToolCalls: number;
maxSnapshotBytes: number;
};
export type CodeModePendingBridgeRequest = {
id: string;
method: CodeModeBridgeMethod;
args: unknown[];
};
export type CodeModeSettledBridgeRequest = {
id: string;
ok: boolean;
value?: unknown;
error?: string;
};
export type CodeModeWorkerInput =
| {
kind: "exec";
source: string;
config: CodeModeRuntimeConfig;
catalog: unknown[];
}
| {
kind: "resume";
snapshotBytes: Uint8Array;
config: CodeModeRuntimeConfig;
settledRequests: CodeModeSettledBridgeRequest[];
};
export type CodeModeFailureCode =
| "invalid_input"
| "runtime_unavailable"
| "timeout"
| "output_limit_exceeded"
| "snapshot_limit_exceeded"
| "internal_error";
export type CodeModeWorkerResult =
| {
status: "completed";
value: unknown;
output: unknown[];
}
| {
status: "waiting";
snapshotBytes: Uint8Array;
pendingRequests: CodeModePendingBridgeRequest[];
output: unknown[];
}
| {
status: "failed";
error: string;
code: CodeModeFailureCode;
output: unknown[];
};

View File

@@ -1,555 +0,0 @@
import { randomUUID } from "node:crypto";
import { readFile } from "node:fs/promises";
import { createRequire } from "node:module";
import { parentPort, workerData } from "node:worker_threads";
import { EvalFlags, Intrinsics, JSException, QuickJS, type JSValueHandle } from "quickjs-wasi";
import type {
CodeModePendingBridgeRequest,
CodeModeRuntimeConfig,
CodeModeSettledBridgeRequest,
CodeModeWorkerInput,
CodeModeWorkerResult,
} from "./types.js";
const require = createRequire(import.meta.url);
const QUICKJS_WASM_PATH = require.resolve("quickjs-wasi/quickjs.wasm");
let quickJsWasmModulePromise: Promise<WebAssembly.Module> | undefined;
class CodeModeWorkerFailure extends Error {
readonly code: Extract<CodeModeWorkerResult, { status: "failed" }>["code"];
constructor(
code: Extract<CodeModeWorkerResult, { status: "failed" }>["code"],
message: string,
options?: ErrorOptions,
) {
super(message, options);
this.name = "CodeModeWorkerFailure";
this.code = code;
}
}
class CodeModeWorkerFailureWithOutput extends CodeModeWorkerFailure {
readonly output: unknown[];
constructor(
code: Extract<CodeModeWorkerResult, { status: "failed" }>["code"],
message: string,
output: unknown[],
options?: ErrorOptions,
) {
super(code, message, options);
this.name = "CodeModeWorkerFailureWithOutput";
this.output = output;
}
}
class CodeModeGuestError extends Error {
constructor(message: string) {
super(message);
this.name = "CodeModeGuestError";
}
}
function isQuickJsInterruptedError(error: unknown): boolean {
if (error instanceof CodeModeGuestError) {
return false;
}
return errorMessage(error) === "interrupted";
}
type VmRun = {
vm: QuickJS;
didTimeout: () => boolean;
};
function getQuickJsWasmModule(): Promise<WebAssembly.Module> {
quickJsWasmModulePromise ??= readFile(QUICKJS_WASM_PATH).then((bytes) =>
WebAssembly.compile(bytes),
);
return quickJsWasmModulePromise;
}
function isRecord(value: unknown): value is Record<string, unknown> {
return Boolean(value && typeof value === "object" && !Array.isArray(value));
}
function toJsonSafe(value: unknown): unknown {
if (value === undefined) {
return null;
}
try {
return JSON.parse(JSON.stringify(value)) as unknown;
} catch {
if (value instanceof Error) {
return { name: value.name, message: value.message };
}
if (value === null) {
return null;
}
switch (typeof value) {
case "string":
case "number":
case "boolean":
return value;
case "bigint":
case "symbol":
case "function":
return String(value);
default:
return Object.prototype.toString.call(value);
}
}
}
function errorMessage(error: unknown): string {
if (error instanceof JSException) {
return error.stack || error.message || String(error);
}
if (error instanceof Error) {
return error.message || String(error);
}
return String(error);
}
const CONTROLLER_SOURCE = String.raw`
(() => {
const output = [];
const pending = new Map();
const catalog = Array.isArray(globalThis.__openclawCatalog) ? globalThis.__openclawCatalog : [];
function safe(value) {
if (value === undefined) return null;
try {
return JSON.parse(JSON.stringify(value));
} catch {
if (value instanceof Error) {
return { name: value.name, message: value.message };
}
if (value === null) return null;
const type = typeof value;
if (type === "string" || type === "number" || type === "boolean") return value;
return String(value);
}
}
function asText(value) {
if (typeof value === "string") return value;
const encoded = JSON.stringify(safe(value));
return typeof encoded === "string" ? encoded : String(value);
}
function request(method, args) {
const id = String(globalThis.__openclawHostRequest(String(method), JSON.stringify(safe(args ?? []))));
return new Promise((resolve, reject) => {
pending.set(id, { resolve, reject });
});
}
function settle(id, ok, payload) {
const entry = pending.get(String(id));
if (!entry) return false;
pending.delete(String(id));
let parsed = null;
try {
parsed = JSON.parse(String(payload));
} catch {
parsed = String(payload);
}
if (ok) {
entry.resolve(parsed);
} else {
const error = new Error(typeof parsed === "string" ? parsed : parsed?.message ?? "nested tool failed");
entry.reject(error);
}
return true;
}
const baseTools = Object.create(null);
Object.defineProperties(baseTools, {
search: { value: (query, options) => request("search", [query, options]), enumerable: true },
describe: { value: (id) => request("describe", [id]), enumerable: true },
call: { value: (id, input) => request("call", [id, input]), enumerable: true },
});
const safeNameCounts = new Map();
for (const tool of catalog) {
const name = typeof tool?.name === "string" ? tool.name : "";
if (!/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name)) continue;
safeNameCounts.set(name, (safeNameCounts.get(name) ?? 0) + 1);
}
for (const tool of catalog) {
const name = typeof tool?.name === "string" ? tool.name : "";
const id = typeof tool?.id === "string" ? tool.id : "";
if (!id || safeNameCounts.get(name) !== 1 || Object.prototype.hasOwnProperty.call(baseTools, name)) {
continue;
}
Object.defineProperty(baseTools, name, {
value: (input) => request("call", [id, input]),
enumerable: true,
});
}
Object.defineProperties(globalThis, {
ALL_TOOLS: { value: Object.freeze(catalog.slice()), enumerable: true },
tools: { value: Object.freeze(baseTools), enumerable: true },
text: { value: (value) => output.push({ type: "text", text: asText(value) }), enumerable: true },
json: { value: (value) => output.push({ type: "json", value: safe(value) }), enumerable: true },
yield_control: { value: (reason) => request("yield", [reason]), enumerable: true },
__openclawSettleBridge: { value: settle },
__openclawTakeOutput: { value: () => output.splice(0) },
});
})();
`;
function buildUserSource(code: string): string {
return `globalThis.__openclawResult = (async () => {\n${code}\n})()`;
}
function createHostRequestHandler(params: {
vm: QuickJS;
pendingRequests: CodeModePendingBridgeRequest[];
config: CodeModeRuntimeConfig;
}): (this: JSValueHandle, method: JSValueHandle, argsJson: JSValueHandle) => JSValueHandle {
return (methodHandle, argsHandle) => {
if (params.pendingRequests.length >= params.config.maxPendingToolCalls) {
throw new Error("too many pending code mode tool calls");
}
const method = methodHandle.toString();
if (method !== "search" && method !== "describe" && method !== "call" && method !== "yield") {
throw new Error("unsupported code mode bridge method");
}
let args: unknown = [];
try {
args = JSON.parse(argsHandle.toString()) as unknown;
} catch {
args = [];
}
const id = `bridge:${params.pendingRequests.length + 1}:${randomUUID()}`;
params.pendingRequests.push({
id,
method,
args: Array.isArray(args) ? args : [],
});
return params.vm.newString(id);
};
}
async function createVm(params: {
catalog: unknown[];
config: CodeModeRuntimeConfig;
pendingRequests: CodeModePendingBridgeRequest[];
}): Promise<VmRun> {
const startedAt = Date.now();
let timedOut = false;
const vm = await QuickJS.create({
wasm: await getQuickJsWasmModule(),
memoryLimit: params.config.memoryLimitBytes,
intrinsics: Intrinsics.ALL,
timezoneOffset: 0,
interruptHandler: () => {
timedOut = Date.now() - startedAt > params.config.timeoutMs;
return timedOut;
},
});
const catalogHandle = vm.hostToHandle(params.catalog);
try {
vm.setProp(vm.global, "__openclawCatalog", catalogHandle);
} finally {
catalogHandle.dispose();
}
const hostRequest = vm.newFunction(
"__openclawHostRequest",
createHostRequestHandler({
vm,
pendingRequests: params.pendingRequests,
config: params.config,
}),
);
try {
vm.setProp(vm.global, "__openclawHostRequest", hostRequest);
} finally {
hostRequest.dispose();
}
vm.evalCode(CONTROLLER_SOURCE, "openclaw-code-mode:controller.js").dispose();
return { vm, didTimeout: () => timedOut };
}
async function restoreVm(params: {
snapshotBytes: Uint8Array;
config: CodeModeRuntimeConfig;
pendingRequests: CodeModePendingBridgeRequest[];
}): Promise<VmRun> {
const startedAt = Date.now();
let timedOut = false;
const snapshot = QuickJS.deserializeSnapshot(params.snapshotBytes);
const vm = await QuickJS.restore(snapshot, {
wasm: await getQuickJsWasmModule(),
memoryLimit: params.config.memoryLimitBytes,
intrinsics: Intrinsics.ALL,
timezoneOffset: 0,
interruptHandler: () => {
timedOut = Date.now() - startedAt > params.config.timeoutMs;
return timedOut;
},
});
vm.registerHostCallback(
"__openclawHostRequest",
createHostRequestHandler({
vm,
pendingRequests: params.pendingRequests,
config: params.config,
}),
);
return { vm, didTimeout: () => timedOut };
}
function takeOutput(vm: QuickJS): unknown[] {
const take = vm.global.getProp("__openclawTakeOutput");
try {
const output = vm.callFunction(take, vm.undefined);
try {
const dumped = vm.dump(output);
return Array.isArray(dumped) ? (dumped as unknown[]) : [];
} finally {
output.dispose();
}
} finally {
take.dispose();
}
}
function takeOutputSafely(vm: QuickJS): unknown[] {
try {
return takeOutput(vm);
} catch {
return [];
}
}
function throwWorkerFailureWithOutput(params: {
error: unknown;
didTimeout: () => boolean;
output: unknown[];
vm: QuickJS;
}): never {
const timedOut = params.didTimeout() || isQuickJsInterruptedError(params.error);
const failureOutput = params.output.length > 0 ? params.output : takeOutputSafely(params.vm);
if (timedOut) {
throw new CodeModeWorkerFailureWithOutput(
"timeout",
"code mode timeout exceeded",
failureOutput,
{ cause: params.error },
);
}
if (params.error instanceof CodeModeWorkerFailure) {
throw new CodeModeWorkerFailureWithOutput(
params.error.code,
params.error.message,
failureOutput,
{ cause: params.error },
);
}
if (failureOutput.length > 0) {
throw new CodeModeWorkerFailureWithOutput(
"internal_error",
errorMessage(params.error),
failureOutput,
{ cause: params.error },
);
}
throw params.error;
}
function drainPendingJobs(vm: QuickJS): void {
for (let index = 0; index < 1000; index += 1) {
if (vm.executePendingJobs() === 0) {
return;
}
}
throw new Error("code mode pending job limit exceeded");
}
function getResultHandle(vm: QuickJS): JSValueHandle {
return vm.global.getProp("__openclawResult");
}
async function readCompletedResult(vm: QuickJS, resultHandle: JSValueHandle): Promise<unknown> {
if (!resultHandle.isPromise) {
return toJsonSafe(vm.dump(resultHandle));
}
const settled = await vm.resolvePromise(resultHandle);
if ("error" in settled) {
try {
throw new CodeModeGuestError(errorMessage(vm.dump(settled.error)));
} finally {
settled.error.dispose();
}
}
try {
return toJsonSafe(vm.dump(settled.value));
} finally {
settled.value.dispose();
}
}
function waitingResult(params: {
vm: QuickJS;
pendingRequests: CodeModePendingBridgeRequest[];
output: unknown[];
config: CodeModeRuntimeConfig;
}): CodeModeWorkerResult {
const snapshotBytes = QuickJS.serializeSnapshot(params.vm.snapshot());
if (snapshotBytes.byteLength > params.config.maxSnapshotBytes) {
throw new CodeModeWorkerFailure("snapshot_limit_exceeded", "code mode snapshot limit exceeded");
}
return {
status: "waiting",
snapshotBytes,
pendingRequests: params.pendingRequests,
output: params.output,
};
}
async function runExec(input: Extract<CodeModeWorkerInput, { kind: "exec" }>) {
const pendingRequests: CodeModePendingBridgeRequest[] = [];
const { vm, didTimeout } = await createVm({
catalog: input.catalog,
config: input.config,
pendingRequests,
});
let output: unknown[] = [];
try {
vm.evalCode(
buildUserSource(input.source),
"openclaw-code-mode:user.js",
EvalFlags.ASYNC,
).dispose();
drainPendingJobs(vm);
output = takeOutput(vm);
const resultHandle = getResultHandle(vm);
try {
if (pendingRequests.length > 0) {
return waitingResult({ vm, pendingRequests, output, config: input.config });
}
if (resultHandle.isPromise && resultHandle.promiseState === 0) {
throw new Error("code mode promise is pending without host work");
}
return {
status: "completed" as const,
value: await readCompletedResult(vm, resultHandle),
output,
};
} finally {
resultHandle.dispose();
}
} catch (error) {
return throwWorkerFailureWithOutput({ error, didTimeout, output, vm });
} finally {
vm.dispose();
}
}
async function runResume(input: Extract<CodeModeWorkerInput, { kind: "resume" }>) {
const pendingRequests: CodeModePendingBridgeRequest[] = [];
const { vm, didTimeout } = await restoreVm({
snapshotBytes: input.snapshotBytes,
config: input.config,
pendingRequests,
});
let output: unknown[] = [];
try {
const settle = vm.global.getProp("__openclawSettleBridge");
try {
for (const request of input.settledRequests) {
const id = vm.newString(request.id);
const payload = vm.newString(JSON.stringify(request.ok ? request.value : request.error));
try {
vm.callFunction(
settle,
vm.undefined,
id,
request.ok ? vm.true : vm.false,
payload,
).dispose();
} finally {
id.dispose();
payload.dispose();
}
}
} finally {
settle.dispose();
}
drainPendingJobs(vm);
output = takeOutput(vm);
const resultHandle = getResultHandle(vm);
try {
if (pendingRequests.length > 0) {
return waitingResult({ vm, pendingRequests, output, config: input.config });
}
if (resultHandle.isPromise && resultHandle.promiseState === 0) {
throw new Error("code mode promise is pending without host work");
}
return {
status: "completed" as const,
value: await readCompletedResult(vm, resultHandle),
output,
};
} finally {
resultHandle.dispose();
}
} catch (error) {
return throwWorkerFailureWithOutput({ error, didTimeout, output, vm });
} finally {
vm.dispose();
}
}
async function main(): Promise<CodeModeWorkerResult> {
const input = workerData as unknown;
if (!isRecord(input) || !isRecord(input.config)) {
return {
status: "failed",
error: "invalid code mode worker input",
code: "invalid_input",
output: [],
};
}
try {
if (input.kind === "exec" && typeof input.source === "string") {
return await runExec({
kind: "exec",
source: input.source,
config: input.config as CodeModeRuntimeConfig,
catalog: Array.isArray(input.catalog) ? input.catalog : [],
});
}
if (input.kind === "resume" && input.snapshotBytes instanceof Uint8Array) {
return await runResume({
kind: "resume",
snapshotBytes: input.snapshotBytes,
config: input.config as CodeModeRuntimeConfig,
settledRequests: Array.isArray(input.settledRequests)
? (input.settledRequests as CodeModeSettledBridgeRequest[])
: [],
});
}
return {
status: "failed",
error: "invalid code mode worker input",
code: "invalid_input",
output: [],
};
} catch (error) {
return {
status: "failed",
error: errorMessage(error),
code: error instanceof CodeModeWorkerFailure ? error.code : "internal_error",
output: error instanceof CodeModeWorkerFailureWithOutput ? error.output : [],
};
}
}
// oxlint-disable-next-line unicorn/require-post-message-target-origin -- Node worker_threads MessagePort, not window.postMessage.
parentPort?.postMessage(await main());

View File

@@ -1,8 +0,0 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "dist"
},
"include": ["src/**/*"]
}

View File

@@ -1,7 +0,0 @@
//#region packages/media-understanding-common/src/active-model.d.ts
type ActiveMediaModel = {
provider: string;
model?: string;
};
//#endregion
export { ActiveMediaModel };

View File

@@ -1 +0,0 @@
export {};

View File

@@ -1,14 +0,0 @@
import { MediaUnderstandingCapability } from "./types.mjs";
//#region packages/media-understanding-common/src/defaults.d.ts
declare const DEFAULT_MAX_CHARS = 500;
declare const DEFAULT_MAX_CHARS_BY_CAPABILITY: Record<MediaUnderstandingCapability, number | undefined>;
declare const DEFAULT_MAX_BYTES: Record<MediaUnderstandingCapability, number>;
declare const DEFAULT_TIMEOUT_SECONDS: Record<MediaUnderstandingCapability, number>;
declare const DEFAULT_PROMPT: Record<MediaUnderstandingCapability, string>;
declare const DEFAULT_VIDEO_MAX_BASE64_BYTES: number;
declare const CLI_OUTPUT_MAX_BUFFER: number;
declare const DEFAULT_MEDIA_CONCURRENCY = 2;
declare const MIN_AUDIO_FILE_BYTES = 1024;
//#endregion
export { CLI_OUTPUT_MAX_BUFFER, DEFAULT_MAX_BYTES, DEFAULT_MAX_CHARS, DEFAULT_MAX_CHARS_BY_CAPABILITY, DEFAULT_MEDIA_CONCURRENCY, DEFAULT_PROMPT, DEFAULT_TIMEOUT_SECONDS, DEFAULT_VIDEO_MAX_BASE64_BYTES, MIN_AUDIO_FILE_BYTES };

View File

@@ -1,29 +0,0 @@
//#region packages/media-understanding-common/src/defaults.ts
const MB = 1024 * 1024;
const DEFAULT_MAX_CHARS = 500;
const DEFAULT_MAX_CHARS_BY_CAPABILITY = {
image: 500,
audio: void 0,
video: 500
};
const DEFAULT_MAX_BYTES = {
image: 10 * MB,
audio: 20 * MB,
video: 50 * MB
};
const DEFAULT_TIMEOUT_SECONDS = {
image: 60,
audio: 60,
video: 120
};
const DEFAULT_PROMPT = {
image: "Describe the image.",
audio: "Transcribe the audio.",
video: "Describe the video."
};
const DEFAULT_VIDEO_MAX_BASE64_BYTES = 70 * MB;
const CLI_OUTPUT_MAX_BUFFER = 5 * MB;
const DEFAULT_MEDIA_CONCURRENCY = 2;
const MIN_AUDIO_FILE_BYTES = 1024;
//#endregion
export { CLI_OUTPUT_MAX_BUFFER, DEFAULT_MAX_BYTES, DEFAULT_MAX_CHARS, DEFAULT_MAX_CHARS_BY_CAPABILITY, DEFAULT_MEDIA_CONCURRENCY, DEFAULT_PROMPT, DEFAULT_TIMEOUT_SECONDS, DEFAULT_VIDEO_MAX_BASE64_BYTES, MIN_AUDIO_FILE_BYTES };

View File

@@ -1,9 +0,0 @@
//#region packages/media-understanding-common/src/errors.d.ts
type MediaUnderstandingSkipReason = "maxBytes" | "timeout" | "unsupported" | "empty" | "blocked" | "tooSmall";
declare class MediaUnderstandingSkipError extends Error {
readonly reason: MediaUnderstandingSkipReason;
constructor(reason: MediaUnderstandingSkipReason, message: string);
}
declare function isMediaUnderstandingSkipError(err: unknown): err is MediaUnderstandingSkipError;
//#endregion
export { MediaUnderstandingSkipError, isMediaUnderstandingSkipError };

View File

@@ -1,13 +0,0 @@
//#region packages/media-understanding-common/src/errors.ts
var MediaUnderstandingSkipError = class extends Error {
constructor(reason, message) {
super(message);
this.reason = reason;
this.name = "MediaUnderstandingSkipError";
}
};
function isMediaUnderstandingSkipError(err) {
return err instanceof MediaUnderstandingSkipError;
}
//#endregion
export { MediaUnderstandingSkipError, isMediaUnderstandingSkipError };

View File

@@ -1,11 +0,0 @@
import { MediaUnderstandingOutput } from "./types.mjs";
//#region packages/media-understanding-common/src/format.d.ts
declare function extractMediaUserText(body?: string): string | undefined;
declare function formatMediaUnderstandingBody(params: {
body?: string;
outputs: MediaUnderstandingOutput[];
}): string;
declare function formatAudioTranscripts(outputs: MediaUnderstandingOutput[]): string;
//#endregion
export { extractMediaUserText, formatAudioTranscripts, formatMediaUnderstandingBody };

View File

@@ -1,47 +0,0 @@
//#region packages/media-understanding-common/src/format.ts
const MEDIA_PLACEHOLDER_RE = /^<media:[^>]+>(\s*\([^)]*\))?$/i;
const MEDIA_PLACEHOLDER_TOKEN_RE = /^<media:[^>]+>(\s*\([^)]*\))?\s*/i;
function extractMediaUserText(body) {
const trimmed = body?.trim() ?? "";
if (!trimmed) return;
if (MEDIA_PLACEHOLDER_RE.test(trimmed)) return;
return trimmed.replace(MEDIA_PLACEHOLDER_TOKEN_RE, "").trim() || void 0;
}
function formatSection(title, kind, text, userText) {
const lines = [`[${title}]`];
if (userText) lines.push(`User text:\n${userText}`);
lines.push(`${kind}:\n${text}`);
return lines.join("\n");
}
function formatMediaUnderstandingBody(params) {
const outputs = params.outputs.filter((output) => output.text.trim());
if (outputs.length === 0) return params.body ?? "";
const userText = extractMediaUserText(params.body);
const sections = [];
if (userText && outputs.length > 1) sections.push(`User text:\n${userText}`);
const counts = /* @__PURE__ */ new Map();
for (const output of outputs) counts.set(output.kind, (counts.get(output.kind) ?? 0) + 1);
const seen = /* @__PURE__ */ new Map();
for (const output of outputs) {
const count = counts.get(output.kind) ?? 1;
const next = (seen.get(output.kind) ?? 0) + 1;
seen.set(output.kind, next);
const suffix = count > 1 ? ` ${next}/${count}` : "";
if (output.kind === "audio.transcription") {
sections.push(formatSection(`Audio${suffix}`, "Transcript", output.text, outputs.length === 1 ? userText : void 0));
continue;
}
if (output.kind === "image.description") {
sections.push(formatSection(`Image${suffix}`, "Description", output.text, outputs.length === 1 ? userText : void 0));
continue;
}
sections.push(formatSection(`Video${suffix}`, "Description", output.text, outputs.length === 1 ? userText : void 0));
}
return sections.join("\n\n").trim();
}
function formatAudioTranscripts(outputs) {
if (outputs.length === 1) return outputs[0].text;
return outputs.map((output, index) => `Audio ${index + 1}:\n${output.text}`).join("\n\n");
}
//#endregion
export { extractMediaUserText, formatAudioTranscripts, formatMediaUnderstandingBody };

View File

@@ -1,11 +0,0 @@
import { ActiveMediaModel } from "./active-model.mjs";
import { MediaAttachment, MediaUnderstandingCapability, MediaUnderstandingCapabilityRegistry, MediaUnderstandingKind, MediaUnderstandingOutput, MediaUnderstandingProvider } from "./types.mjs";
import { CLI_OUTPUT_MAX_BUFFER, DEFAULT_MAX_BYTES, DEFAULT_MAX_CHARS, DEFAULT_MAX_CHARS_BY_CAPABILITY, DEFAULT_MEDIA_CONCURRENCY, DEFAULT_PROMPT, DEFAULT_TIMEOUT_SECONDS, DEFAULT_VIDEO_MAX_BASE64_BYTES, MIN_AUDIO_FILE_BYTES } from "./defaults.mjs";
import { MediaUnderstandingSkipError, isMediaUnderstandingSkipError } from "./errors.mjs";
import { extractMediaUserText, formatAudioTranscripts, formatMediaUnderstandingBody } from "./format.mjs";
import { OpenAiCompatibleVideoPayload, buildOpenAiCompatibleVideoRequestBody, coerceOpenAiCompatibleVideoText, resolveMediaUnderstandingString } from "./openai-compatible-video.mjs";
import { extractGeminiResponse } from "./output-extract.mjs";
import { normalizeMediaExecutionProviderId, normalizeMediaProviderId } from "./provider-id.mjs";
import { providerSupportsCapability } from "./provider-supports.mjs";
import { estimateBase64Size, resolveVideoMaxBase64Bytes } from "./video.mjs";
export { ActiveMediaModel, CLI_OUTPUT_MAX_BUFFER, DEFAULT_MAX_BYTES, DEFAULT_MAX_CHARS, DEFAULT_MAX_CHARS_BY_CAPABILITY, DEFAULT_MEDIA_CONCURRENCY, DEFAULT_PROMPT, DEFAULT_TIMEOUT_SECONDS, DEFAULT_VIDEO_MAX_BASE64_BYTES, MIN_AUDIO_FILE_BYTES, MediaAttachment, MediaUnderstandingCapability, MediaUnderstandingCapabilityRegistry, MediaUnderstandingKind, MediaUnderstandingOutput, MediaUnderstandingProvider, MediaUnderstandingSkipError, OpenAiCompatibleVideoPayload, buildOpenAiCompatibleVideoRequestBody, coerceOpenAiCompatibleVideoText, estimateBase64Size, extractGeminiResponse, extractMediaUserText, formatAudioTranscripts, formatMediaUnderstandingBody, isMediaUnderstandingSkipError, normalizeMediaExecutionProviderId, normalizeMediaProviderId, providerSupportsCapability, resolveMediaUnderstandingString, resolveVideoMaxBase64Bytes };

View File

@@ -1,11 +0,0 @@
import "./active-model.mjs";
import { CLI_OUTPUT_MAX_BUFFER, DEFAULT_MAX_BYTES, DEFAULT_MAX_CHARS, DEFAULT_MAX_CHARS_BY_CAPABILITY, DEFAULT_MEDIA_CONCURRENCY, DEFAULT_PROMPT, DEFAULT_TIMEOUT_SECONDS, DEFAULT_VIDEO_MAX_BASE64_BYTES, MIN_AUDIO_FILE_BYTES } from "./defaults.mjs";
import { MediaUnderstandingSkipError, isMediaUnderstandingSkipError } from "./errors.mjs";
import { extractMediaUserText, formatAudioTranscripts, formatMediaUnderstandingBody } from "./format.mjs";
import { buildOpenAiCompatibleVideoRequestBody, coerceOpenAiCompatibleVideoText, resolveMediaUnderstandingString } from "./openai-compatible-video.mjs";
import { extractGeminiResponse } from "./output-extract.mjs";
import { normalizeMediaExecutionProviderId, normalizeMediaProviderId } from "./provider-id.mjs";
import { providerSupportsCapability } from "./provider-supports.mjs";
import "./types.mjs";
import { estimateBase64Size, resolveVideoMaxBase64Bytes } from "./video.mjs";
export { CLI_OUTPUT_MAX_BUFFER, DEFAULT_MAX_BYTES, DEFAULT_MAX_CHARS, DEFAULT_MAX_CHARS_BY_CAPABILITY, DEFAULT_MEDIA_CONCURRENCY, DEFAULT_PROMPT, DEFAULT_TIMEOUT_SECONDS, DEFAULT_VIDEO_MAX_BASE64_BYTES, MIN_AUDIO_FILE_BYTES, MediaUnderstandingSkipError, buildOpenAiCompatibleVideoRequestBody, coerceOpenAiCompatibleVideoText, estimateBase64Size, extractGeminiResponse, extractMediaUserText, formatAudioTranscripts, formatMediaUnderstandingBody, isMediaUnderstandingSkipError, normalizeMediaExecutionProviderId, normalizeMediaProviderId, providerSupportsCapability, resolveMediaUnderstandingString, resolveVideoMaxBase64Bytes };

View File

@@ -1,37 +0,0 @@
//#region packages/media-understanding-common/src/openai-compatible-video.d.ts
type OpenAiCompatibleVideoPayload = {
choices?: Array<{
message?: {
content?: string | Array<{
text?: string;
}>;
reasoning_content?: string;
};
}>;
};
declare function resolveMediaUnderstandingString(value: string | undefined, fallback: string): string;
declare function coerceOpenAiCompatibleVideoText(payload: OpenAiCompatibleVideoPayload): string | null;
declare function buildOpenAiCompatibleVideoRequestBody(params: {
model: string;
prompt: string;
mime: string;
buffer: Buffer;
}): {
model: string;
messages: {
role: string;
content: ({
type: string;
text: string;
video_url?: undefined;
} | {
type: string;
video_url: {
url: string;
};
text?: undefined;
})[];
}[];
};
//#endregion
export { OpenAiCompatibleVideoPayload, buildOpenAiCompatibleVideoRequestBody, coerceOpenAiCompatibleVideoText, resolveMediaUnderstandingString };

View File

@@ -1,32 +0,0 @@
//#region packages/media-understanding-common/src/openai-compatible-video.ts
function resolveMediaUnderstandingString(value, fallback) {
return value?.trim() || fallback;
}
function coerceOpenAiCompatibleVideoText(payload) {
const message = payload.choices?.[0]?.message;
if (!message) return null;
if (typeof message.content === "string" && message.content.trim()) return message.content.trim();
if (Array.isArray(message.content)) {
const text = message.content.map((part) => part.text?.trim() ?? "").filter(Boolean).join("\n");
if (text) return text;
}
if (typeof message.reasoning_content === "string" && message.reasoning_content.trim()) return message.reasoning_content.trim();
return null;
}
function buildOpenAiCompatibleVideoRequestBody(params) {
return {
model: params.model,
messages: [{
role: "user",
content: [{
type: "text",
text: params.prompt
}, {
type: "video_url",
video_url: { url: `data:${params.mime};base64,${params.buffer.toString("base64")}` }
}]
}]
};
}
//#endregion
export { buildOpenAiCompatibleVideoRequestBody, coerceOpenAiCompatibleVideoText, resolveMediaUnderstandingString };

View File

@@ -1,4 +0,0 @@
//#region packages/media-understanding-common/src/output-extract.d.ts
declare function extractGeminiResponse(raw: string): string | null;
//#endregion
export { extractGeminiResponse };

View File

@@ -1,21 +0,0 @@
//#region packages/media-understanding-common/src/output-extract.ts
function extractLastJsonObject(raw) {
const trimmed = raw.trim();
const start = trimmed.lastIndexOf("{");
if (start === -1) return null;
const slice = trimmed.slice(start);
try {
return JSON.parse(slice);
} catch {
return null;
}
}
function extractGeminiResponse(raw) {
const payload = extractLastJsonObject(raw);
if (!payload || typeof payload !== "object") return null;
const response = payload.response;
if (typeof response !== "string") return null;
return response.trim() || null;
}
//#endregion
export { extractGeminiResponse };

View File

@@ -1,5 +0,0 @@
//#region packages/media-understanding-common/src/provider-id.d.ts
declare function normalizeMediaProviderId(id: string): string;
declare function normalizeMediaExecutionProviderId(id: string): string;
//#endregion
export { normalizeMediaExecutionProviderId, normalizeMediaProviderId };

View File

@@ -1,18 +0,0 @@
//#region packages/media-understanding-common/src/provider-id.ts
function normalizeProviderId(provider) {
return provider.trim().toLowerCase();
}
function normalizeMediaProviderId(id) {
const normalized = normalizeProviderId(id);
if (normalized === "gemini") return "google";
if (normalized === "minimax-cn") return "minimax";
if (normalized === "minimax-portal-cn") return "minimax-portal";
return normalized;
}
function normalizeMediaExecutionProviderId(id) {
const normalized = normalizeProviderId(id);
if (normalized === "minimax-cn" || normalized === "minimax-portal-cn") return normalized;
return normalizeMediaProviderId(normalized);
}
//#endregion
export { normalizeMediaExecutionProviderId, normalizeMediaProviderId };

View File

@@ -1,6 +0,0 @@
import { MediaUnderstandingCapability, MediaUnderstandingProvider } from "./types.mjs";
//#region packages/media-understanding-common/src/provider-supports.d.ts
declare function providerSupportsCapability(provider: MediaUnderstandingProvider | undefined, capability: MediaUnderstandingCapability): boolean;
//#endregion
export { providerSupportsCapability };

View File

@@ -1,9 +0,0 @@
//#region packages/media-understanding-common/src/provider-supports.ts
function providerSupportsCapability(provider, capability) {
if (!provider) return false;
if (capability === "audio") return Boolean(provider.transcribeAudio);
if (capability === "image") return Boolean(provider.describeImage);
return Boolean(provider.describeVideo);
}
//#endregion
export { providerSupportsCapability };

View File

@@ -1,31 +0,0 @@
//#region packages/media-understanding-common/src/types.d.ts
type MediaUnderstandingKind = "audio.transcription" | "video.description" | "image.description";
type MediaUnderstandingCapability = "image" | "audio" | "video";
type MediaUnderstandingCapabilityRegistry = Map<string, {
capabilities?: MediaUnderstandingCapability[];
}>;
type MediaAttachment = {
path?: string;
url?: string;
mime?: string;
index: number;
alreadyTranscribed?: boolean;
};
type MediaUnderstandingOutput = {
kind: MediaUnderstandingKind;
attachmentIndex: number;
text: string;
provider: string;
model?: string;
};
type MediaUnderstandingProvider = {
id: string;
capabilities?: MediaUnderstandingCapability[];
transcribeAudio?: unknown;
describeVideo?: unknown;
describeImage?: unknown;
describeImages?: unknown;
extractStructured?: unknown;
};
//#endregion
export { MediaAttachment, MediaUnderstandingCapability, MediaUnderstandingCapabilityRegistry, MediaUnderstandingKind, MediaUnderstandingOutput, MediaUnderstandingProvider };

View File

@@ -1 +0,0 @@
export {};

View File

@@ -1,5 +0,0 @@
//#region packages/media-understanding-common/src/video.d.ts
declare function estimateBase64Size(bytes: number): number;
declare function resolveVideoMaxBase64Bytes(maxBytes: number): number;
//#endregion
export { estimateBase64Size, resolveVideoMaxBase64Bytes };

View File

@@ -1,11 +0,0 @@
import { DEFAULT_VIDEO_MAX_BASE64_BYTES } from "./defaults.mjs";
//#region packages/media-understanding-common/src/video.ts
function estimateBase64Size(bytes) {
return Math.ceil(bytes / 3) * 4;
}
function resolveVideoMaxBase64Bytes(maxBytes) {
const expanded = Math.floor(maxBytes * (4 / 3));
return Math.min(expanded, DEFAULT_VIDEO_MAX_BASE64_BYTES);
}
//#endregion
export { estimateBase64Size, resolveVideoMaxBase64Bytes };

View File

@@ -1,71 +0,0 @@
{
"name": "@openclaw/media-understanding-common",
"version": "0.0.0-private",
"private": true,
"files": [
"dist"
],
"type": "module",
"main": "./dist/index.mjs",
"types": "./dist/index.d.mts",
"exports": {
".": {
"types": "./dist/index.d.mts",
"import": "./dist/index.mjs",
"default": "./dist/index.mjs"
},
"./active-model": {
"types": "./dist/active-model.d.mts",
"import": "./dist/active-model.mjs",
"default": "./dist/active-model.mjs"
},
"./defaults": {
"types": "./dist/defaults.d.mts",
"import": "./dist/defaults.mjs",
"default": "./dist/defaults.mjs"
},
"./errors": {
"types": "./dist/errors.d.mts",
"import": "./dist/errors.mjs",
"default": "./dist/errors.mjs"
},
"./format": {
"types": "./dist/format.d.mts",
"import": "./dist/format.mjs",
"default": "./dist/format.mjs"
},
"./openai-compatible-video": {
"types": "./dist/openai-compatible-video.d.mts",
"import": "./dist/openai-compatible-video.mjs",
"default": "./dist/openai-compatible-video.mjs"
},
"./output-extract": {
"types": "./dist/output-extract.d.mts",
"import": "./dist/output-extract.mjs",
"default": "./dist/output-extract.mjs"
},
"./provider-id": {
"types": "./dist/provider-id.d.mts",
"import": "./dist/provider-id.mjs",
"default": "./dist/provider-id.mjs"
},
"./provider-supports": {
"types": "./dist/provider-supports.d.mts",
"import": "./dist/provider-supports.mjs",
"default": "./dist/provider-supports.mjs"
},
"./types": {
"types": "./dist/types.d.mts",
"import": "./dist/types.mjs",
"default": "./dist/types.mjs"
},
"./video": {
"types": "./dist/video.d.mts",
"import": "./dist/video.mjs",
"default": "./dist/video.mjs"
}
},
"scripts": {
"build": "tsdown src/index.ts src/active-model.ts src/defaults.ts src/errors.ts src/format.ts src/openai-compatible-video.ts src/output-extract.ts src/provider-id.ts src/provider-supports.ts src/types.ts src/video.ts --no-config --platform node --format esm --dts --out-dir dist --clean"
}
}

View File

@@ -1,4 +0,0 @@
export type ActiveMediaModel = {
provider: string;
model?: string;
};

View File

@@ -1,32 +0,0 @@
import type { MediaUnderstandingCapability } from "./types.js";
const MB = 1024 * 1024;
export const DEFAULT_MAX_CHARS = 500;
export const DEFAULT_MAX_CHARS_BY_CAPABILITY: Record<
MediaUnderstandingCapability,
number | undefined
> = {
image: DEFAULT_MAX_CHARS,
audio: undefined,
video: DEFAULT_MAX_CHARS,
};
export const DEFAULT_MAX_BYTES: Record<MediaUnderstandingCapability, number> = {
image: 10 * MB,
audio: 20 * MB,
video: 50 * MB,
};
export const DEFAULT_TIMEOUT_SECONDS: Record<MediaUnderstandingCapability, number> = {
image: 60,
audio: 60,
video: 120,
};
export const DEFAULT_PROMPT: Record<MediaUnderstandingCapability, string> = {
image: "Describe the image.",
audio: "Transcribe the audio.",
video: "Describe the video.",
};
export const DEFAULT_VIDEO_MAX_BASE64_BYTES = 70 * MB;
export const CLI_OUTPUT_MAX_BUFFER = 5 * MB;
export const DEFAULT_MEDIA_CONCURRENCY = 2;
export const MIN_AUDIO_FILE_BYTES = 1024;

View File

@@ -1,21 +0,0 @@
type MediaUnderstandingSkipReason =
| "maxBytes"
| "timeout"
| "unsupported"
| "empty"
| "blocked"
| "tooSmall";
export class MediaUnderstandingSkipError extends Error {
readonly reason: MediaUnderstandingSkipReason;
constructor(reason: MediaUnderstandingSkipReason, message: string) {
super(message);
this.reason = reason;
this.name = "MediaUnderstandingSkipError";
}
}
export function isMediaUnderstandingSkipError(err: unknown): err is MediaUnderstandingSkipError {
return err instanceof MediaUnderstandingSkipError;
}

View File

@@ -1,98 +0,0 @@
import type { MediaUnderstandingOutput } from "./types.js";
const MEDIA_PLACEHOLDER_RE = /^<media:[^>]+>(\s*\([^)]*\))?$/i;
const MEDIA_PLACEHOLDER_TOKEN_RE = /^<media:[^>]+>(\s*\([^)]*\))?\s*/i;
export function extractMediaUserText(body?: string): string | undefined {
const trimmed = body?.trim() ?? "";
if (!trimmed) {
return undefined;
}
if (MEDIA_PLACEHOLDER_RE.test(trimmed)) {
return undefined;
}
const cleaned = trimmed.replace(MEDIA_PLACEHOLDER_TOKEN_RE, "").trim();
return cleaned || undefined;
}
function formatSection(
title: string,
kind: "Transcript" | "Description",
text: string,
userText?: string,
): string {
const lines = [`[${title}]`];
if (userText) {
lines.push(`User text:\n${userText}`);
}
lines.push(`${kind}:\n${text}`);
return lines.join("\n");
}
export function formatMediaUnderstandingBody(params: {
body?: string;
outputs: MediaUnderstandingOutput[];
}): string {
const outputs = params.outputs.filter((output) => output.text.trim());
if (outputs.length === 0) {
return params.body ?? "";
}
const userText = extractMediaUserText(params.body);
const sections: string[] = [];
if (userText && outputs.length > 1) {
sections.push(`User text:\n${userText}`);
}
const counts = new Map<MediaUnderstandingOutput["kind"], number>();
for (const output of outputs) {
counts.set(output.kind, (counts.get(output.kind) ?? 0) + 1);
}
const seen = new Map<MediaUnderstandingOutput["kind"], number>();
for (const output of outputs) {
const count = counts.get(output.kind) ?? 1;
const next = (seen.get(output.kind) ?? 0) + 1;
seen.set(output.kind, next);
const suffix = count > 1 ? ` ${next}/${count}` : "";
if (output.kind === "audio.transcription") {
sections.push(
formatSection(
`Audio${suffix}`,
"Transcript",
output.text,
outputs.length === 1 ? userText : undefined,
),
);
continue;
}
if (output.kind === "image.description") {
sections.push(
formatSection(
`Image${suffix}`,
"Description",
output.text,
outputs.length === 1 ? userText : undefined,
),
);
continue;
}
sections.push(
formatSection(
`Video${suffix}`,
"Description",
output.text,
outputs.length === 1 ? userText : undefined,
),
);
}
return sections.join("\n\n").trim();
}
export function formatAudioTranscripts(outputs: MediaUnderstandingOutput[]): string {
if (outputs.length === 1) {
return outputs[0].text;
}
return outputs.map((output, index) => `Audio ${index + 1}:\n${output.text}`).join("\n\n");
}

View File

@@ -1,10 +0,0 @@
export * from "./active-model.js";
export * from "./defaults.js";
export * from "./errors.js";
export * from "./format.js";
export * from "./openai-compatible-video.js";
export * from "./output-extract.js";
export * from "./provider-id.js";
export * from "./provider-supports.js";
export * from "./types.js";
export * from "./video.js";

View File

@@ -1,66 +0,0 @@
export type OpenAiCompatibleVideoPayload = {
choices?: Array<{
message?: {
content?: string | Array<{ text?: string }>;
reasoning_content?: string;
};
}>;
};
export function resolveMediaUnderstandingString(
value: string | undefined,
fallback: string,
): string {
const trimmed = value?.trim();
return trimmed || fallback;
}
export function coerceOpenAiCompatibleVideoText(
payload: OpenAiCompatibleVideoPayload,
): string | null {
const message = payload.choices?.[0]?.message;
if (!message) {
return null;
}
if (typeof message.content === "string" && message.content.trim()) {
return message.content.trim();
}
if (Array.isArray(message.content)) {
const text = message.content
.map((part) => part.text?.trim() ?? "")
.filter(Boolean)
.join("\n");
if (text) {
return text;
}
}
if (typeof message.reasoning_content === "string" && message.reasoning_content.trim()) {
return message.reasoning_content.trim();
}
return null;
}
export function buildOpenAiCompatibleVideoRequestBody(params: {
model: string;
prompt: string;
mime: string;
buffer: Buffer;
}) {
return {
model: params.model,
messages: [
{
role: "user",
content: [
{ type: "text", text: params.prompt },
{
type: "video_url",
video_url: {
url: `data:${params.mime};base64,${params.buffer.toString("base64")}`,
},
},
],
},
],
};
}

View File

@@ -1,26 +0,0 @@
function extractLastJsonObject(raw: string): unknown {
const trimmed = raw.trim();
const start = trimmed.lastIndexOf("{");
if (start === -1) {
return null;
}
const slice = trimmed.slice(start);
try {
return JSON.parse(slice);
} catch {
return null;
}
}
export function extractGeminiResponse(raw: string): string | null {
const payload = extractLastJsonObject(raw);
if (!payload || typeof payload !== "object") {
return null;
}
const response = (payload as { response?: unknown }).response;
if (typeof response !== "string") {
return null;
}
const trimmed = response.trim();
return trimmed || null;
}

View File

@@ -1,25 +0,0 @@
function normalizeProviderId(provider: string): string {
return provider.trim().toLowerCase();
}
export function normalizeMediaProviderId(id: string): string {
const normalized = normalizeProviderId(id);
if (normalized === "gemini") {
return "google";
}
if (normalized === "minimax-cn") {
return "minimax";
}
if (normalized === "minimax-portal-cn") {
return "minimax-portal";
}
return normalized;
}
export function normalizeMediaExecutionProviderId(id: string): string {
const normalized = normalizeProviderId(id);
if (normalized === "minimax-cn" || normalized === "minimax-portal-cn") {
return normalized;
}
return normalizeMediaProviderId(normalized);
}

View File

@@ -1,17 +0,0 @@
import type { MediaUnderstandingCapability, MediaUnderstandingProvider } from "./types.js";
export function providerSupportsCapability(
provider: MediaUnderstandingProvider | undefined,
capability: MediaUnderstandingCapability,
): boolean {
if (!provider) {
return false;
}
if (capability === "audio") {
return Boolean(provider.transcribeAudio);
}
if (capability === "image") {
return Boolean(provider.describeImage);
}
return Boolean(provider.describeVideo);
}

View File

@@ -1,39 +0,0 @@
export type MediaUnderstandingKind =
| "audio.transcription"
| "video.description"
| "image.description";
export type MediaUnderstandingCapability = "image" | "audio" | "video";
export type MediaUnderstandingCapabilityRegistry = Map<
string,
{
capabilities?: MediaUnderstandingCapability[];
}
>;
export type MediaAttachment = {
path?: string;
url?: string;
mime?: string;
index: number;
alreadyTranscribed?: boolean;
};
export type MediaUnderstandingOutput = {
kind: MediaUnderstandingKind;
attachmentIndex: number;
text: string;
provider: string;
model?: string;
};
export type MediaUnderstandingProvider = {
id: string;
capabilities?: MediaUnderstandingCapability[];
transcribeAudio?: unknown;
describeVideo?: unknown;
describeImage?: unknown;
describeImages?: unknown;
extractStructured?: unknown;
};

View File

@@ -1,10 +0,0 @@
import { DEFAULT_VIDEO_MAX_BASE64_BYTES } from "./defaults.js";
export function estimateBase64Size(bytes: number): number {
return Math.ceil(bytes / 3) * 4;
}
export function resolveVideoMaxBase64Bytes(maxBytes: number): number {
const expanded = Math.floor(maxBytes * (4 / 3));
return Math.min(expanded, DEFAULT_VIDEO_MAX_BASE64_BYTES);
}

302
pnpm-lock.yaml generated
View File

@@ -304,6 +304,9 @@ importers:
specifier: 4.1.7
version: 4.1.7(@opentelemetry/api@1.9.1)(@types/node@25.9.1)(@vitest/browser-playwright@4.1.7)(@vitest/coverage-v8@4.1.7)(jsdom@29.1.1(@noble/hashes@2.0.1))(vite@8.0.14(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.22.3)(yaml@2.9.0))
optionalDependencies:
sharp:
specifier: 0.34.5
version: 0.34.5
sqlite-vec:
specifier: 0.1.9
version: 0.1.9
@@ -1691,7 +1694,7 @@ importers:
version: 2.2.3
baileys:
specifier: 7.0.0-rc13
version: 7.0.0-rc13(audio-decode@2.2.3)
version: 7.0.0-rc13(audio-decode@2.2.3)(sharp@0.34.5)
typebox:
specifier: 1.1.38
version: 1.1.38
@@ -1788,12 +1791,6 @@ importers:
specifier: 2.9.0
version: 2.9.0
packages/code-mode-runtime:
dependencies:
quickjs-wasi:
specifier: 3.0.0
version: 3.0.0
packages/gateway-client:
dependencies:
'@openclaw/gateway-protocol':
@@ -1835,8 +1832,6 @@ importers:
packages/media-generation-core: {}
packages/media-understanding-common: {}
packages/memory-host-sdk: {}
packages/net-policy:
@@ -2739,6 +2734,159 @@ packages:
peerDependencies:
hono: 4.12.18
'@img/colour@1.1.0':
resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==}
engines: {node: '>=18'}
'@img/sharp-darwin-arm64@0.34.5':
resolution: {integrity: sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64]
os: [darwin]
'@img/sharp-darwin-x64@0.34.5':
resolution: {integrity: sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [darwin]
'@img/sharp-libvips-darwin-arm64@1.2.4':
resolution: {integrity: sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==}
cpu: [arm64]
os: [darwin]
'@img/sharp-libvips-darwin-x64@1.2.4':
resolution: {integrity: sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==}
cpu: [x64]
os: [darwin]
'@img/sharp-libvips-linux-arm64@1.2.4':
resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-arm@1.2.4':
resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==}
cpu: [arm]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-ppc64@1.2.4':
resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-riscv64@1.2.4':
resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-s390x@1.2.4':
resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-x64@1.2.4':
resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==}
cpu: [x64]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linuxmusl-arm64@1.2.4':
resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==}
cpu: [arm64]
os: [linux]
libc: [musl]
'@img/sharp-libvips-linuxmusl-x64@1.2.4':
resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==}
cpu: [x64]
os: [linux]
libc: [musl]
'@img/sharp-linux-arm64@0.34.5':
resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@img/sharp-linux-arm@0.34.5':
resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm]
os: [linux]
libc: [glibc]
'@img/sharp-linux-ppc64@0.34.5':
resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@img/sharp-linux-riscv64@0.34.5':
resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@img/sharp-linux-s390x@0.34.5':
resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@img/sharp-linux-x64@0.34.5':
resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [linux]
libc: [glibc]
'@img/sharp-linuxmusl-arm64@0.34.5':
resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64]
os: [linux]
libc: [musl]
'@img/sharp-linuxmusl-x64@0.34.5':
resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [linux]
libc: [musl]
'@img/sharp-wasm32@0.34.5':
resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [wasm32]
'@img/sharp-win32-arm64@0.34.5':
resolution: {integrity: sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64]
os: [win32]
'@img/sharp-win32-ia32@0.34.5':
resolution: {integrity: sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [ia32]
os: [win32]
'@img/sharp-win32-x64@0.34.5':
resolution: {integrity: sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [win32]
'@isaacs/fs-minipass@4.0.1':
resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==}
engines: {node: '>=18.0.0'}
@@ -6450,6 +6598,10 @@ packages:
setprototypeof@1.2.0:
resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==}
sharp@0.34.5:
resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
shebang-command@2.0.0:
resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
engines: {node: '>=8'}
@@ -8206,6 +8358,103 @@ snapshots:
dependencies:
hono: 4.12.18
'@img/colour@1.1.0':
optional: true
'@img/sharp-darwin-arm64@0.34.5':
optionalDependencies:
'@img/sharp-libvips-darwin-arm64': 1.2.4
optional: true
'@img/sharp-darwin-x64@0.34.5':
optionalDependencies:
'@img/sharp-libvips-darwin-x64': 1.2.4
optional: true
'@img/sharp-libvips-darwin-arm64@1.2.4':
optional: true
'@img/sharp-libvips-darwin-x64@1.2.4':
optional: true
'@img/sharp-libvips-linux-arm64@1.2.4':
optional: true
'@img/sharp-libvips-linux-arm@1.2.4':
optional: true
'@img/sharp-libvips-linux-ppc64@1.2.4':
optional: true
'@img/sharp-libvips-linux-riscv64@1.2.4':
optional: true
'@img/sharp-libvips-linux-s390x@1.2.4':
optional: true
'@img/sharp-libvips-linux-x64@1.2.4':
optional: true
'@img/sharp-libvips-linuxmusl-arm64@1.2.4':
optional: true
'@img/sharp-libvips-linuxmusl-x64@1.2.4':
optional: true
'@img/sharp-linux-arm64@0.34.5':
optionalDependencies:
'@img/sharp-libvips-linux-arm64': 1.2.4
optional: true
'@img/sharp-linux-arm@0.34.5':
optionalDependencies:
'@img/sharp-libvips-linux-arm': 1.2.4
optional: true
'@img/sharp-linux-ppc64@0.34.5':
optionalDependencies:
'@img/sharp-libvips-linux-ppc64': 1.2.4
optional: true
'@img/sharp-linux-riscv64@0.34.5':
optionalDependencies:
'@img/sharp-libvips-linux-riscv64': 1.2.4
optional: true
'@img/sharp-linux-s390x@0.34.5':
optionalDependencies:
'@img/sharp-libvips-linux-s390x': 1.2.4
optional: true
'@img/sharp-linux-x64@0.34.5':
optionalDependencies:
'@img/sharp-libvips-linux-x64': 1.2.4
optional: true
'@img/sharp-linuxmusl-arm64@0.34.5':
optionalDependencies:
'@img/sharp-libvips-linuxmusl-arm64': 1.2.4
optional: true
'@img/sharp-linuxmusl-x64@0.34.5':
optionalDependencies:
'@img/sharp-libvips-linuxmusl-x64': 1.2.4
optional: true
'@img/sharp-wasm32@0.34.5':
dependencies:
'@emnapi/runtime': 1.10.0
optional: true
'@img/sharp-win32-arm64@0.34.5':
optional: true
'@img/sharp-win32-ia32@0.34.5':
optional: true
'@img/sharp-win32-x64@0.34.5':
optional: true
'@isaacs/fs-minipass@4.0.1':
dependencies:
minipass: 7.1.3
@@ -9856,7 +10105,7 @@ snapshots:
bail@2.0.2: {}
baileys@7.0.0-rc13(audio-decode@2.2.3):
baileys@7.0.0-rc13(audio-decode@2.2.3)(sharp@0.34.5):
dependencies:
'@cacheable/node-cache': 1.7.6
'@hapi/boom': 9.1.4
@@ -9871,6 +10120,7 @@ snapshots:
ws: 8.21.0
optionalDependencies:
audio-decode: 2.2.3
sharp: 0.34.5
transitivePeerDependencies:
- bufferutil
- supports-color
@@ -12306,6 +12556,38 @@ snapshots:
setprototypeof@1.2.0: {}
sharp@0.34.5:
dependencies:
'@img/colour': 1.1.0
detect-libc: 2.1.2
semver: 7.8.0
optionalDependencies:
'@img/sharp-darwin-arm64': 0.34.5
'@img/sharp-darwin-x64': 0.34.5
'@img/sharp-libvips-darwin-arm64': 1.2.4
'@img/sharp-libvips-darwin-x64': 1.2.4
'@img/sharp-libvips-linux-arm': 1.2.4
'@img/sharp-libvips-linux-arm64': 1.2.4
'@img/sharp-libvips-linux-ppc64': 1.2.4
'@img/sharp-libvips-linux-riscv64': 1.2.4
'@img/sharp-libvips-linux-s390x': 1.2.4
'@img/sharp-libvips-linux-x64': 1.2.4
'@img/sharp-libvips-linuxmusl-arm64': 1.2.4
'@img/sharp-libvips-linuxmusl-x64': 1.2.4
'@img/sharp-linux-arm': 0.34.5
'@img/sharp-linux-arm64': 0.34.5
'@img/sharp-linux-ppc64': 0.34.5
'@img/sharp-linux-riscv64': 0.34.5
'@img/sharp-linux-s390x': 0.34.5
'@img/sharp-linux-x64': 0.34.5
'@img/sharp-linuxmusl-arm64': 0.34.5
'@img/sharp-linuxmusl-x64': 0.34.5
'@img/sharp-wasm32': 0.34.5
'@img/sharp-win32-arm64': 0.34.5
'@img/sharp-win32-ia32': 0.34.5
'@img/sharp-win32-x64': 0.34.5
optional: true
shebang-command@2.0.0:
dependencies:
shebang-regex: 3.0.0

View File

@@ -106,6 +106,7 @@ allowBuilds:
koffi: false
node-llama-cpp: true
protobufjs: true
sharp: true
tree-sitter-bash: false
openclaw: true
"@openclaw/proxyline": true

View File

@@ -48,7 +48,6 @@ export const BUILD_ALL_STEPS = [
"packages/plugin-sdk/package.json",
"packages/llm-core/package.json",
"packages/markdown-core/package.json",
"packages/media-understanding-common/package.json",
"packages/terminal-core/package.json",
"packages/memory-host-sdk/package.json",
"tsconfig.json",
@@ -58,7 +57,6 @@ export const BUILD_ALL_STEPS = [
"packages/markdown-core/src",
"packages/memory-host-sdk/src",
"packages/media-generation-core/src",
"packages/media-understanding-common/src",
"packages/terminal-core/src",
"src/types",
"src/video-generation/dashscope-compatible.ts",
@@ -173,44 +171,6 @@ export const BUILD_ALL_PROFILE_STEP_ENV = {
},
};
export function buildAllUsage() {
return [
"Usage: node scripts/build-all.mjs [profile]",
"",
"Builds OpenClaw artifacts for the selected profile.",
"",
"Profiles:",
...Object.keys(BUILD_ALL_PROFILES).map((profile) => ` ${profile}`),
"",
"Options:",
" -h, --help Show this help.",
].join("\n");
}
export function parseBuildAllArgs(argv) {
const args = {
help: false,
profile: "full",
};
let sawProfile = false;
for (const arg of argv) {
if (arg === "--help" || arg === "-h") {
args.help = true;
} else if (arg.startsWith("-")) {
throw new Error(`unknown argument: ${arg}\n\n${buildAllUsage()}`);
} else if (sawProfile) {
throw new Error(`unexpected argument: ${arg}\n\n${buildAllUsage()}`);
} else {
args.profile = arg;
sawProfile = true;
}
}
if (!args.help && !BUILD_ALL_PROFILES[args.profile]) {
throw new Error(`Unknown build profile: ${args.profile}\n\n${buildAllUsage()}`);
}
return args;
}
export function resolveBuildAllSteps(profile = "full") {
const labels = BUILD_ALL_PROFILES[profile];
if (!labels) {
@@ -503,54 +463,44 @@ function isMainModule() {
}
if (isMainModule()) {
let args;
try {
args = parseBuildAllArgs(process.argv.slice(2));
} catch (error) {
console.error(error instanceof Error ? error.message : String(error));
process.exit(2);
}
if (args?.help) {
console.log(buildAllUsage());
} else {
const timings = [];
let exitCode = 0;
for (const step of resolveBuildAllSteps(args.profile)) {
const startedAt = performance.now();
const cacheState = resolveBuildAllStepCacheState(step);
if (process.env.OPENCLAW_BUILD_CACHE !== "0" && cacheState.fresh) {
restoreBuildAllStepCacheOutputs(cacheState);
const durationMs = performance.now() - startedAt;
timings.push({ label: step.label, status: "cached", durationMs });
console.error(`[build-all] ${step.label} (cached) ${formatBuildAllDuration(durationMs)}`);
continue;
}
console.error(`[build-all] ${step.label}`);
const invocation = resolveBuildAllStep(step);
const result = spawnSync(invocation.command, invocation.args, invocation.options);
const profile = process.argv[2] ?? "full";
const timings = [];
let exitCode = 0;
for (const step of resolveBuildAllSteps(profile)) {
const startedAt = performance.now();
const cacheState = resolveBuildAllStepCacheState(step);
if (process.env.OPENCLAW_BUILD_CACHE !== "0" && cacheState.fresh) {
restoreBuildAllStepCacheOutputs(cacheState);
const durationMs = performance.now() - startedAt;
if (typeof result.status === "number") {
if (result.status !== 0) {
timings.push({ label: step.label, status: "failed", durationMs });
console.error(
`[build-all] ${step.label} failed after ${formatBuildAllDuration(durationMs)}`,
);
exitCode = result.status;
break;
}
writeBuildAllStepCacheStamp(step, resolveBuildAllStepCacheState(step));
timings.push({ label: step.label, status: "ran", durationMs });
console.error(`[build-all] ${step.label} done in ${formatBuildAllDuration(durationMs)}`);
continue;
timings.push({ label: step.label, status: "cached", durationMs });
console.error(`[build-all] ${step.label} (cached) ${formatBuildAllDuration(durationMs)}`);
continue;
}
console.error(`[build-all] ${step.label}`);
const invocation = resolveBuildAllStep(step);
const result = spawnSync(invocation.command, invocation.args, invocation.options);
const durationMs = performance.now() - startedAt;
if (typeof result.status === "number") {
if (result.status !== 0) {
timings.push({ label: step.label, status: "failed", durationMs });
console.error(
`[build-all] ${step.label} failed after ${formatBuildAllDuration(durationMs)}`,
);
exitCode = result.status;
break;
}
timings.push({ label: step.label, status: "failed", durationMs });
console.error(`[build-all] ${step.label} failed after ${formatBuildAllDuration(durationMs)}`);
exitCode = 1;
break;
}
console.error(formatBuildAllTimingSummary(timings));
if (exitCode !== 0) {
process.exit(exitCode);
writeBuildAllStepCacheStamp(step, resolveBuildAllStepCacheState(step));
timings.push({ label: step.label, status: "ran", durationMs });
console.error(`[build-all] ${step.label} done in ${formatBuildAllDuration(durationMs)}`);
continue;
}
timings.push({ label: step.label, status: "failed", durationMs });
console.error(`[build-all] ${step.label} failed after ${formatBuildAllDuration(durationMs)}`);
exitCode = 1;
break;
}
console.error(formatBuildAllTimingSummary(timings));
if (exitCode !== 0) {
process.exit(exitCode);
}
}

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