mirror of
https://github.com/moltbot/moltbot.git
synced 2026-04-21 05:32:53 +00:00
fix(ci): unblock agent typing and cache startup metadata
This commit is contained in:
@@ -149,8 +149,8 @@ function normalizeMatrixCompatibilityConfig(cfg: OpenClawConfig): ChannelDoctorC
|
||||
config: {
|
||||
...cfg,
|
||||
channels: {
|
||||
...cfg.channels,
|
||||
matrix: updatedMatrix as OpenClawConfig["channels"]["matrix"],
|
||||
...(cfg.channels ?? {}),
|
||||
matrix: updatedMatrix as NonNullable<OpenClawConfig["channels"]>["matrix"],
|
||||
},
|
||||
},
|
||||
changes,
|
||||
|
||||
@@ -973,8 +973,8 @@
|
||||
"android:test:integration": "OPENCLAW_LIVE_TEST=1 OPENCLAW_LIVE_ANDROID_NODE=1 node scripts/run-vitest.mjs run --config vitest.live.config.ts src/gateway/android-node.capabilities.live.test.ts",
|
||||
"android:test:third-party": "cd apps/android && ./gradlew :app:testThirdPartyDebugUnitTest",
|
||||
"audit:seams": "node scripts/audit-seams.mjs",
|
||||
"build": "pnpm canvas:a2ui:bundle && node scripts/tsdown-build.mjs && node scripts/runtime-postbuild.mjs && node scripts/build-stamp.mjs && pnpm build:plugin-sdk:dts && node --import tsx scripts/write-plugin-sdk-entry-dts.ts && node scripts/check-plugin-sdk-exports.mjs && node --import tsx scripts/canvas-a2ui-copy.ts && node --import tsx scripts/copy-hook-metadata.ts && node --import tsx scripts/copy-export-html-templates.ts && node --import tsx scripts/write-build-info.ts && node --import tsx scripts/write-cli-startup-metadata.ts && node --import tsx scripts/write-cli-compat.ts",
|
||||
"build:docker": "node scripts/tsdown-build.mjs && node scripts/runtime-postbuild.mjs && node scripts/build-stamp.mjs && node --import tsx scripts/canvas-a2ui-copy.ts && node --import tsx scripts/copy-hook-metadata.ts && node --import tsx scripts/copy-export-html-templates.ts && node --import tsx scripts/write-build-info.ts && node --import tsx scripts/write-cli-startup-metadata.ts && node --import tsx scripts/write-cli-compat.ts",
|
||||
"build": "pnpm canvas:a2ui:bundle && node scripts/tsdown-build.mjs && node scripts/runtime-postbuild.mjs && node scripts/build-stamp.mjs && pnpm build:plugin-sdk:dts && node --import tsx scripts/write-plugin-sdk-entry-dts.ts && node scripts/check-plugin-sdk-exports.mjs && node --import tsx scripts/canvas-a2ui-copy.ts && node --import tsx scripts/copy-hook-metadata.ts && node --import tsx scripts/copy-export-html-templates.ts && node --import tsx scripts/write-build-info.ts && node --experimental-strip-types scripts/write-cli-startup-metadata.ts && node --import tsx scripts/write-cli-compat.ts",
|
||||
"build:docker": "node scripts/tsdown-build.mjs && node scripts/runtime-postbuild.mjs && node scripts/build-stamp.mjs && node --import tsx scripts/canvas-a2ui-copy.ts && node --import tsx scripts/copy-hook-metadata.ts && node --import tsx scripts/copy-export-html-templates.ts && node --import tsx scripts/write-build-info.ts && node --experimental-strip-types scripts/write-cli-startup-metadata.ts && node --import tsx scripts/write-cli-compat.ts",
|
||||
"build:plugin-sdk:dts": "tsc -p tsconfig.plugin-sdk.dts.json",
|
||||
"build:strict-smoke": "pnpm canvas:a2ui:bundle && node scripts/tsdown-build.mjs && node scripts/runtime-postbuild.mjs && node scripts/build-stamp.mjs && pnpm build:plugin-sdk:dts && node --import tsx scripts/write-plugin-sdk-entry-dts.ts && node scripts/check-plugin-sdk-exports.mjs",
|
||||
"canon:check": "node scripts/canon.mjs check",
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { createHash } from "node:crypto";
|
||||
import { mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||
import { renderRootHelpText as renderSourceRootHelpText } from "../src/cli/program/root-help.ts";
|
||||
|
||||
function dedupe(values: string[]): string[] {
|
||||
const seen = new Set<string>();
|
||||
@@ -40,10 +40,33 @@ type ExtensionChannelEntry = {
|
||||
label: string;
|
||||
};
|
||||
|
||||
export function readBundledChannelCatalogIds(
|
||||
type BundledChannelCatalog = {
|
||||
ids: string[];
|
||||
signature: string;
|
||||
};
|
||||
|
||||
function resolveRootHelpBundleIdentity(
|
||||
distDirOverride: string = distDir,
|
||||
): { bundleName: string; signature: string } | null {
|
||||
const bundleName = readdirSync(distDirOverride).find(
|
||||
(entry) => entry.startsWith("root-help-") && entry.endsWith(".js"),
|
||||
);
|
||||
if (!bundleName) {
|
||||
return null;
|
||||
}
|
||||
const bundlePath = path.join(distDirOverride, bundleName);
|
||||
const raw = readFileSync(bundlePath, "utf8");
|
||||
return {
|
||||
bundleName,
|
||||
signature: createHash("sha1").update(raw).digest("hex"),
|
||||
};
|
||||
}
|
||||
|
||||
export function readBundledChannelCatalog(
|
||||
extensionsDirOverride: string = extensionsDir,
|
||||
): string[] {
|
||||
): BundledChannelCatalog {
|
||||
const entries: ExtensionChannelEntry[] = [];
|
||||
const signature = createHash("sha1");
|
||||
for (const dirEntry of readdirSync(extensionsDirOverride, { withFileTypes: true })) {
|
||||
if (!dirEntry.isDirectory()) {
|
||||
continue;
|
||||
@@ -51,6 +74,7 @@ export function readBundledChannelCatalogIds(
|
||||
const packageJsonPath = path.join(extensionsDirOverride, dirEntry.name, "package.json");
|
||||
try {
|
||||
const raw = readFileSync(packageJsonPath, "utf8");
|
||||
signature.update(`${dirEntry.name}\0${raw}\0`);
|
||||
const parsed = JSON.parse(raw) as {
|
||||
openclaw?: {
|
||||
channel?: {
|
||||
@@ -75,31 +99,40 @@ export function readBundledChannelCatalogIds(
|
||||
// Ignore malformed or missing extension package manifests.
|
||||
}
|
||||
}
|
||||
return entries
|
||||
.toSorted((a, b) => (a.order === b.order ? a.label.localeCompare(b.label) : a.order - b.order))
|
||||
.map((entry) => entry.id);
|
||||
return {
|
||||
ids: entries
|
||||
.toSorted((a, b) =>
|
||||
a.order === b.order ? a.label.localeCompare(b.label) : a.order - b.order,
|
||||
)
|
||||
.map((entry) => entry.id),
|
||||
signature: signature.digest("hex"),
|
||||
};
|
||||
}
|
||||
|
||||
export function readBundledChannelCatalogIds(
|
||||
extensionsDirOverride: string = extensionsDir,
|
||||
): string[] {
|
||||
return readBundledChannelCatalog(extensionsDirOverride).ids;
|
||||
}
|
||||
|
||||
export async function renderBundledRootHelpText(
|
||||
_distDirOverride: string = distDir,
|
||||
): Promise<string> {
|
||||
const bundleName = readdirSync(distDirOverride).find(
|
||||
(entry) => entry.startsWith("root-help-") && entry.endsWith(".js"),
|
||||
);
|
||||
if (!bundleName) {
|
||||
const bundleIdentity = resolveRootHelpBundleIdentity(_distDirOverride);
|
||||
if (!bundleIdentity) {
|
||||
throw new Error("No root-help bundle found in dist; cannot write CLI startup metadata.");
|
||||
}
|
||||
const moduleUrl = pathToFileURL(path.join(distDirOverride, bundleName)).href;
|
||||
const moduleUrl = pathToFileURL(path.join(_distDirOverride, bundleIdentity.bundleName)).href;
|
||||
const inlineModule = [
|
||||
`const mod = await import(${JSON.stringify(moduleUrl)});`,
|
||||
"if (typeof mod.outputRootHelp !== 'function') {",
|
||||
` throw new Error(${JSON.stringify(`Bundle ${bundleName} does not export outputRootHelp.`)});`,
|
||||
` throw new Error(${JSON.stringify(`Bundle ${bundleIdentity.bundleName} does not export outputRootHelp.`)});`,
|
||||
"}",
|
||||
"await mod.outputRootHelp();",
|
||||
"process.exit(0);",
|
||||
].join("\n");
|
||||
const result = spawnSync(process.execPath, ["--input-type=module", "--eval", inlineModule], {
|
||||
cwd: distDirOverride,
|
||||
cwd: _distDirOverride,
|
||||
encoding: "utf8",
|
||||
timeout: 30_000,
|
||||
});
|
||||
@@ -109,13 +142,18 @@ export async function renderBundledRootHelpText(
|
||||
if (result.status !== 0) {
|
||||
const stderr = result.stderr?.trim();
|
||||
throw new Error(
|
||||
`Failed to render bundled root help from ${bundleName}` +
|
||||
`Failed to render bundled root help from ${bundleIdentity.bundleName}` +
|
||||
(stderr ? `: ${stderr}` : result.signal ? `: terminated by ${result.signal}` : ""),
|
||||
);
|
||||
}
|
||||
return result.stdout ?? "";
|
||||
}
|
||||
|
||||
async function renderSourceRootHelpText(): Promise<string> {
|
||||
const module = await import("../src/cli/program/root-help.ts");
|
||||
return module.renderRootHelpText({ pluginSdkResolution: "src" });
|
||||
}
|
||||
|
||||
export async function writeCliStartupMetadata(options?: {
|
||||
distDir?: string;
|
||||
outputPath?: string;
|
||||
@@ -124,15 +162,32 @@ export async function writeCliStartupMetadata(options?: {
|
||||
const resolvedDistDir = options?.distDir ?? distDir;
|
||||
const resolvedOutputPath = options?.outputPath ?? outputPath;
|
||||
const resolvedExtensionsDir = options?.extensionsDir ?? extensionsDir;
|
||||
const catalog = readBundledChannelCatalogIds(resolvedExtensionsDir);
|
||||
const channelOptions = dedupe([...CORE_CHANNEL_ORDER, ...catalog]);
|
||||
const useSourceRootHelp =
|
||||
resolvedDistDir === distDir &&
|
||||
resolvedOutputPath === outputPath &&
|
||||
resolvedExtensionsDir === extensionsDir;
|
||||
const rootHelpText = useSourceRootHelp
|
||||
? await renderSourceRootHelpText({ pluginSdkResolution: "src" })
|
||||
: await renderBundledRootHelpText(resolvedDistDir);
|
||||
const channelCatalog = readBundledChannelCatalog(resolvedExtensionsDir);
|
||||
const bundleIdentity = resolveRootHelpBundleIdentity(resolvedDistDir);
|
||||
const channelOptions = dedupe([...CORE_CHANNEL_ORDER, ...channelCatalog.ids]);
|
||||
|
||||
try {
|
||||
const existing = JSON.parse(readFileSync(resolvedOutputPath, "utf8")) as {
|
||||
rootHelpBundleSignature?: unknown;
|
||||
channelCatalogSignature?: unknown;
|
||||
};
|
||||
if (
|
||||
bundleIdentity &&
|
||||
existing.rootHelpBundleSignature === bundleIdentity.signature &&
|
||||
existing.channelCatalogSignature === channelCatalog.signature
|
||||
) {
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
// Missing or malformed existing metadata means we should regenerate it.
|
||||
}
|
||||
|
||||
let rootHelpText: string;
|
||||
try {
|
||||
rootHelpText = await renderBundledRootHelpText(resolvedDistDir);
|
||||
} catch {
|
||||
rootHelpText = await renderSourceRootHelpText();
|
||||
}
|
||||
|
||||
mkdirSync(resolvedDistDir, { recursive: true });
|
||||
writeFileSync(
|
||||
@@ -141,6 +196,8 @@ export async function writeCliStartupMetadata(options?: {
|
||||
{
|
||||
generatedBy: "scripts/write-cli-startup-metadata.ts",
|
||||
channelOptions,
|
||||
channelCatalogSignature: channelCatalog.signature,
|
||||
rootHelpBundleSignature: bundleIdentity?.signature ?? null,
|
||||
rootHelpText,
|
||||
},
|
||||
null,
|
||||
|
||||
@@ -8,6 +8,7 @@ import { getApiKeyForModel, requireApiKey } from "./model-auth.js";
|
||||
import { normalizeProviderId, parseModelRef } from "./model-selection.js";
|
||||
import { ensureOpenClawModelsJson } from "./models-config.js";
|
||||
import { discoverAuthStorage, discoverModels } from "./pi-model-discovery.js";
|
||||
import { buildAssistantMessageWithZeroUsage } from "./stream-message-shared.js";
|
||||
|
||||
export const LIVE_CACHE_TEST_ENABLED =
|
||||
isLiveTestEnabled() && isTruthyEnvValue(process.env.OPENCLAW_LIVE_CACHE_TEST);
|
||||
@@ -123,6 +124,22 @@ export function extractAssistantText(message: AssistantMessage): string {
|
||||
.join(" ");
|
||||
}
|
||||
|
||||
export function buildAssistantHistoryTurn(
|
||||
text: string,
|
||||
model?: Pick<Model<Api>, "api" | "provider" | "id">,
|
||||
): AssistantMessage {
|
||||
return buildAssistantMessageWithZeroUsage({
|
||||
model: {
|
||||
api: model?.api ?? "openai-responses",
|
||||
provider: model?.provider ?? "openai",
|
||||
id: model?.id ?? "test-model",
|
||||
},
|
||||
content: [{ type: "text", text }],
|
||||
stopReason: "stop",
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
}
|
||||
|
||||
export function computeCacheHitRate(usage: {
|
||||
input?: number;
|
||||
cacheRead?: number;
|
||||
|
||||
@@ -48,14 +48,14 @@ import {
|
||||
planTurnInput,
|
||||
} from "./openai-ws-message-conversion.js";
|
||||
import { buildOpenAIWebSocketResponseCreatePayload } from "./openai-ws-request.js";
|
||||
import { createBoundaryAwareStreamFnForModel } from "./provider-transport-stream.js";
|
||||
import { log } from "./pi-embedded-runner/logger.js";
|
||||
import { createBoundaryAwareStreamFnForModel } from "./provider-transport-stream.js";
|
||||
import {
|
||||
buildAssistantMessageWithZeroUsage,
|
||||
buildStreamErrorAssistantMessage,
|
||||
} from "./stream-message-shared.js";
|
||||
import { mergeTransportMetadata } from "./transport-stream-shared.js";
|
||||
import { stripSystemPromptCacheBoundary } from "./system-prompt-cache-boundary.js";
|
||||
import { mergeTransportMetadata } from "./transport-stream-shared.js";
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Per-session state
|
||||
@@ -963,7 +963,7 @@ async function fallbackToHttp(
|
||||
const httpStreamFn =
|
||||
openAIWsStreamDeps.createHttpFallbackStreamFn(model as ProviderRuntimeModel) ??
|
||||
openAIWsStreamDeps.streamSimple;
|
||||
const httpStream = httpStreamFn(model, context, mergedOptions);
|
||||
const httpStream = await httpStreamFn(model, context, mergedOptions);
|
||||
for await (const event of httpStream) {
|
||||
if (fallbackOptions?.suppressStart && event.type === "start") {
|
||||
continue;
|
||||
|
||||
@@ -6,6 +6,7 @@ import { Type } from "@sinclair/typebox";
|
||||
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import {
|
||||
buildAssistantHistoryTurn as buildTypedAssistantHistoryTurn,
|
||||
buildStableCachePrefix,
|
||||
completeSimpleWithLiveTimeout,
|
||||
computeCacheHitRate,
|
||||
@@ -49,12 +50,11 @@ let liveRunnerRootDir: string | undefined;
|
||||
|
||||
type UserContent = Extract<Message, { role: "user" }>["content"];
|
||||
|
||||
function makeAssistantHistoryTurn(text: string): Message {
|
||||
return {
|
||||
role: "assistant",
|
||||
content: [{ type: "text", text }],
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
function makeAssistantHistoryTurn(
|
||||
text: string,
|
||||
model?: Awaited<ReturnType<typeof resolveLiveDirectModel>>["model"],
|
||||
): Message {
|
||||
return buildTypedAssistantHistoryTurn(text, model);
|
||||
}
|
||||
|
||||
function makeUserHistoryTurn(content: UserContent): Message {
|
||||
@@ -348,9 +348,9 @@ async function runOpenAiToolCacheProbe(params: {
|
||||
},
|
||||
toolTurn.response,
|
||||
buildToolResultMessage(toolTurn.toolCall.id, NOOP_TOOL.name, "ok"),
|
||||
makeAssistantHistoryTurn("TOOL HISTORY ACKNOWLEDGED"),
|
||||
makeAssistantHistoryTurn("TOOL HISTORY ACKNOWLEDGED", params.model),
|
||||
makeUserHistoryTurn("Keep the tool output stable in history."),
|
||||
makeAssistantHistoryTurn("TOOL HISTORY PRESERVED"),
|
||||
makeAssistantHistoryTurn("TOOL HISTORY PRESERVED", params.model),
|
||||
{
|
||||
role: "user",
|
||||
content: `Reply with exactly CACHE-OK ${params.suffix}.`,
|
||||
@@ -432,9 +432,9 @@ async function runOpenAiImageCacheProbe(params: {
|
||||
makeImageUserTurn(
|
||||
"An image is attached. Ignore image semantics but keep the bytes in history.",
|
||||
),
|
||||
makeAssistantHistoryTurn("IMAGE HISTORY ACKNOWLEDGED"),
|
||||
makeAssistantHistoryTurn("IMAGE HISTORY ACKNOWLEDGED", params.model),
|
||||
makeUserHistoryTurn("Keep the earlier image turn stable in context."),
|
||||
makeAssistantHistoryTurn("IMAGE HISTORY PRESERVED"),
|
||||
makeAssistantHistoryTurn("IMAGE HISTORY PRESERVED", params.model),
|
||||
makeUserHistoryTurn(`Reply with exactly CACHE-OK ${params.suffix}.`),
|
||||
],
|
||||
},
|
||||
@@ -526,9 +526,9 @@ async function runAnthropicToolCacheProbe(params: {
|
||||
},
|
||||
toolTurn.response,
|
||||
buildToolResultMessage(toolTurn.toolCall.id, NOOP_TOOL.name, "ok"),
|
||||
makeAssistantHistoryTurn("TOOL HISTORY ACKNOWLEDGED"),
|
||||
makeAssistantHistoryTurn("TOOL HISTORY ACKNOWLEDGED", params.model),
|
||||
makeUserHistoryTurn("Keep the tool output stable in history."),
|
||||
makeAssistantHistoryTurn("TOOL HISTORY PRESERVED"),
|
||||
makeAssistantHistoryTurn("TOOL HISTORY PRESERVED", params.model),
|
||||
{
|
||||
role: "user",
|
||||
content: `Reply with exactly CACHE-OK ${params.suffix}.`,
|
||||
@@ -572,9 +572,9 @@ async function runAnthropicImageCacheProbe(params: {
|
||||
makeImageUserTurn(
|
||||
"An image is attached. Ignore image semantics but keep the bytes in history.",
|
||||
),
|
||||
makeAssistantHistoryTurn("IMAGE HISTORY ACKNOWLEDGED"),
|
||||
makeAssistantHistoryTurn("IMAGE HISTORY ACKNOWLEDGED", params.model),
|
||||
makeUserHistoryTurn("Keep the earlier image turn stable in context."),
|
||||
makeAssistantHistoryTurn("IMAGE HISTORY PRESERVED"),
|
||||
makeAssistantHistoryTurn("IMAGE HISTORY PRESERVED", params.model),
|
||||
makeUserHistoryTurn(`Reply with exactly CACHE-OK ${params.suffix}.`),
|
||||
],
|
||||
},
|
||||
|
||||
@@ -83,7 +83,8 @@ export const sessionAbortCompactionMock: Mock<(reason?: unknown) => void> = vi.f
|
||||
export const createOpenClawCodingToolsMock = vi.fn(() => []);
|
||||
export const resolveEmbeddedAgentStreamFnMock = vi.fn((_params?: unknown) => vi.fn());
|
||||
export const applyExtraParamsToAgentMock = vi.fn(() => ({ effectiveExtraParams: {} }));
|
||||
export const resolveAgentTransportOverrideMock = vi.fn(() => undefined);
|
||||
export const resolveAgentTransportOverrideMock: Mock<(params?: unknown) => string | undefined> =
|
||||
vi.fn(() => undefined);
|
||||
|
||||
export function resetCompactSessionStateMocks(): void {
|
||||
sanitizeSessionHistoryMock.mockReset();
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { AssistantMessage, Tool } from "@mariozechner/pi-ai";
|
||||
import { Type } from "@sinclair/typebox";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
buildAssistantHistoryTurn,
|
||||
buildStableCachePrefix,
|
||||
completeSimpleWithLiveTimeout,
|
||||
computeCacheHitRate,
|
||||
@@ -122,21 +123,13 @@ async function runOpenAiMcpStyleCacheProbe(params: {
|
||||
{ role: "user", content: toolTurn.prompt, timestamp: Date.now() },
|
||||
toolTurn.response,
|
||||
buildToolResultMessage(toolTurn.toolCall.id),
|
||||
{
|
||||
role: "assistant",
|
||||
content: [{ type: "text", text: "MCP TOOL HISTORY ACKNOWLEDGED" }],
|
||||
timestamp: Date.now(),
|
||||
},
|
||||
buildAssistantHistoryTurn("MCP TOOL HISTORY ACKNOWLEDGED", params.model),
|
||||
{
|
||||
role: "user",
|
||||
content: "Keep the MCP tool output stable in history.",
|
||||
timestamp: Date.now(),
|
||||
},
|
||||
{
|
||||
role: "assistant",
|
||||
content: [{ type: "text", text: "MCP TOOL HISTORY PRESERVED" }],
|
||||
timestamp: Date.now(),
|
||||
},
|
||||
buildAssistantHistoryTurn("MCP TOOL HISTORY PRESERVED", params.model),
|
||||
{
|
||||
role: "user",
|
||||
content: `Reply with exactly CACHE-OK ${params.suffix}.`,
|
||||
|
||||
Reference in New Issue
Block a user