mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 19:20:43 +00:00
merge main into PR 77502
This commit is contained in:
@@ -11,7 +11,9 @@ Docs: https://docs.openclaw.ai
|
||||
### Changes
|
||||
|
||||
- Plugins/migration: emit catalog-backed install hints when `plugins.entries` or `plugins.allow` references an official external plugin that is not installed, so upgraded configs point operators to `openclaw plugins install <spec>` instead of telling them to remove valid plugin config. (#77483) Thanks @hclsys.
|
||||
- Dependencies: refresh runtime and provider packages including Pi 0.73.0, ACPX adapters, OpenAI, Anthropic, Slack, and TypeScript native preview, while keeping the Bedrock runtime installer override pinned below the Windows ARM Node 24 npm resolver failure.
|
||||
- Plugins/active-memory: skip session-store channel entries that contain `:` when resolving the recall subagent's channel, so QQ c2c agent IDs (e.g. `c2c:10D4F7C2…`) and other scoped conversation IDs do not reach bundled-plugin `dirName` validation and crash the recall run. The same guard already applied to explicit `channelId` params (#76704); this extends it to store-derived channels. (#77396) Thanks @hclsys.
|
||||
- Secrets/external channel contracts: also look in `<rootDir>/dist/` when resolving the `secret-contract-api` sidecar, so npm-published externalized channel plugins (e.g. `@openclaw/discord` since 2026.5.2) whose compiled artifacts live under `dist/` actually contribute their channel SecretRef contracts to the runtime snapshot. Without this, env-backed `channels.discord.token` SecretRefs silently failed to resolve at gateway start on 2026.5.3, leaving the channel `not configured` even though #76449 had landed the generic external-contract loader. Thanks @mogglemoss.
|
||||
- Models/auth: add `openclaw models auth list [--provider <id>] [--json]` so users can inspect saved per-agent auth profiles without dumping secrets or hitting the old “too many arguments” path. Thanks @vincentkoc.
|
||||
- Control UI/header: show the active agent name in dashboard breadcrumbs without adding the current session key, keeping non-chat views oriented without crowding the topbar.
|
||||
- Control UI/cron: make the New Job sidebar collapsible so the jobs list can reclaim space while keeping the form one click away. Thanks @BunsDev.
|
||||
@@ -54,6 +56,7 @@ Docs: https://docs.openclaw.ai
|
||||
### Fixes
|
||||
|
||||
- Agents/OpenAI: default direct OpenAI Responses models to the SSE transport instead of WebSocket auto-selection, preventing pi runtime chat turns from hanging on servers where the WebSocket path stalls while the OpenAI HTTP stream works. Thanks @vincentkoc.
|
||||
- Docker: prune package-excluded plugin dist directories from runtime images unless the build explicitly opts that plugin in, so official external plugins such as Feishu stay install-on-demand instead of shipping partial metadata without compiled runtime output. Fixes #77424. Thanks @vincentkoc.
|
||||
- CLI/update: disable and skip plugins that fail package-update plugin sync, so a broken npm/ClawHub/git/marketplace plugin cannot turn a successful OpenClaw package update into a failed update result. Thanks @vincentkoc.
|
||||
- CLI/update: use an absolute POSIX npm script shell during package-manager updates, so restricted PATH environments can still run dependency lifecycle scripts while updating from `--tag main`. Fixes #77530. Thanks @PeterTremonti.
|
||||
- Diagnostics: grant the internal diagnostics event bus to official installed diagnostics exporter plugins, so npm-installed `@openclaw/diagnostics-prometheus` can emit metrics without broadening the capability to arbitrary global plugins. Fixes #76628. Thanks @RayWoo.
|
||||
@@ -65,6 +68,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Doctor/config: restore legacy group chat config migrations for `routing.allowFrom`, `routing.groupChat.*`, and `channels.telegram.requireMention` so upgrades keep WhatsApp, Telegram, and iMessage group mention gates and history settings instead of leaving configs invalid or silently blocked. Thanks @scoootscooob.
|
||||
- CLI/update: make package-update follow-up processes write completion results and exit explicitly, so Windows packaged upgrades do not hang after the new package finishes post-core plugin work. Thanks @vincentkoc.
|
||||
- Release validation: skip Slack live QA unless Slack credentials are explicitly configured, so release gates can keep proving non-Slack surfaces while Slack is still local and credential-gated. Thanks @vincentkoc.
|
||||
- Plugins/update: treat OpenClaw CalVer correction versions like `2026.5.3-1` as satisfying base plugin API ranges, so correction builds can install plugins that require the base runtime API. Fixes #77293. (#77450) Thanks @p3nchan.
|
||||
- fix(gateway): clamp unbound websocket auth scopes [AI]. (#77413) Thanks @pgondhi987.
|
||||
- Gate zalouser startup name matching [AI]. (#77411) Thanks @pgondhi987.
|
||||
- Active Memory: send a bounded latest-message search query to the recall worker so channel/runtime metadata does not become the memory search string. Fixes #65309. Thanks @joeykrug, @westley3601, @pimenov, and @tasi333.
|
||||
@@ -73,8 +77,10 @@ Docs: https://docs.openclaw.ai
|
||||
- fix(qqbot): keep private commands off framework surface [AI]. (#77212) Thanks @pgondhi987.
|
||||
- Claude CLI: honor non-off `/think` levels by passing Claude Code's session-scoped `--effort` flag through the CLI backend seam, so chat bridges no longer show an inert thinking control. Fixes #77303. Thanks @Petr1t.
|
||||
- Agents/subagents: refresh deferred final-delivery payloads when same-session completion output changes, so retried parent notifications use the final child summary instead of stale progress text. Thanks @vincentkoc.
|
||||
- active-memory: skip the memory sub-agent gracefully instead of logging a confusing allowlist error when no memory plugin (`memory-core` or `memory-lancedb`) is loaded, so active-memory with no memory backend no longer produces misleading "No callable tools remain" warnings in the gateway log. Fixes #77506. Thanks @hclsys.
|
||||
- Memory/wiki: preserve representation from both corpora in `corpus=all` searches while backfilling unused result capacity, so memory hits are not starved by numerically higher wiki integer scores. Fixes #77337. Thanks @hclsys.
|
||||
- Telegram: clean up tool-only draft previews after assistant message boundaries so transient `Surfacing...` tool-status bubbles do not linger when no matching final preview arrives. Thanks @BunsDev.
|
||||
- Telegram: let explicit forum-topic `requireMention` settings override persisted `/activate` and `/deactivate` state, so per-topic mention gates work consistently. Fixes #49864. Thanks @Panniantong.
|
||||
- Cron: surface failed isolated-run diagnostics in `cron show`, status, and run history when requested tools are unavailable, so blocked cron runs report the actual tool-policy failure instead of a misleading green result. Fixes #75763. Thanks @RyanSandoval.
|
||||
- TUI/escape abort: track the in-flight runId after `chat.send` resolves so pressing Esc during the gap before the first gateway event aborts the run instead of repeatedly printing `no active run`. Fixes #1296. Thanks @Lukavyi and @romneyda.
|
||||
- TUI/render: stop the long-token sanitizer from injecting literal spaces inside inline code spans, fenced code blocks, table borders, and bare hyphenated/dotted identifiers, so copied package names, entity IDs, and shell line-continuations stay byte-for-byte intact while narrow-terminal protection still chunks unidentifiable long prose tokens. Fixes #48432, #39505. Thanks @DocOellerson, @xeusoc, @CCcassiusdjs, @akramcodez, @brokemac79, @romneyda.
|
||||
|
||||
@@ -124,6 +124,7 @@ RUN printf 'packages:\n - .\n - ui\n' > /tmp/pnpm-workspace.runtime.yaml && \
|
||||
cp /tmp/pnpm-workspace.runtime.yaml pnpm-workspace.yaml && \
|
||||
CI=true NPM_CONFIG_FROZEN_LOCKFILE=false pnpm prune --prod && \
|
||||
node scripts/postinstall-bundled-plugins.mjs && \
|
||||
OPENCLAW_EXTENSIONS="$OPENCLAW_EXTENSIONS" node scripts/prune-docker-plugin-dist.mjs && \
|
||||
find dist -type f \( -name '*.d.ts' -o -name '*.d.mts' -o -name '*.d.cts' -o -name '*.map' \) -delete && \
|
||||
node scripts/check-package-dist-imports.mjs /app
|
||||
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
},
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@agentclientprotocol/claude-agent-acp": "0.31.4",
|
||||
"@zed-industries/codex-acp": "0.12.0",
|
||||
"@agentclientprotocol/claude-agent-acp": "0.32.0",
|
||||
"@zed-industries/codex-acp": "0.13.0",
|
||||
"acpx": "0.6.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -211,8 +211,8 @@ ${ACPX_CMD} codex sessions close oc-codex-<conversationId>
|
||||
Defaults are:
|
||||
|
||||
- `openclaw -> openclaw acp`
|
||||
- `claude -> npx -y @agentclientprotocol/claude-agent-acp@^0.31.0`
|
||||
- `codex -> bundled @zed-industries/codex-acp@0.12.0 through OpenClaw's isolated CODEX_HOME wrapper`
|
||||
- `claude -> bundled @agentclientprotocol/claude-agent-acp@0.32.0`
|
||||
- `codex -> bundled @zed-industries/codex-acp@0.13.0 through OpenClaw's isolated CODEX_HOME wrapper`
|
||||
- `copilot -> copilot --acp --stdio`
|
||||
- `cursor -> cursor-agent acp`
|
||||
- `droid -> droid exec --output-format acp`
|
||||
|
||||
@@ -163,7 +163,7 @@ describe("prepareAcpxCodexAuthConfig", () => {
|
||||
});
|
||||
|
||||
const wrapper = await fs.readFile(generated.wrapperPath, "utf8");
|
||||
expect(wrapper).toContain('"@zed-industries/codex-acp@^0.12.0"');
|
||||
expect(wrapper).toContain('"@zed-industries/codex-acp@0.13.0"');
|
||||
expect(wrapper).toContain('"--", "codex-acp"');
|
||||
expect(wrapper).not.toContain("@zed-industries/codex-acp@^0.11.1");
|
||||
});
|
||||
@@ -184,7 +184,7 @@ describe("prepareAcpxCodexAuthConfig", () => {
|
||||
});
|
||||
|
||||
const wrapper = await fs.readFile(generated.wrapperPath, "utf8");
|
||||
expect(wrapper).toContain('"@agentclientprotocol/claude-agent-acp@0.31.4"');
|
||||
expect(wrapper).toContain('"@agentclientprotocol/claude-agent-acp@0.32.0"');
|
||||
expect(wrapper).toContain('"--", "claude-agent-acp"');
|
||||
expect(wrapper).not.toContain("@agentclientprotocol/claude-agent-acp@^0.31.0");
|
||||
expect(wrapper).not.toContain("@agentclientprotocol/claude-agent-acp@0.31.0");
|
||||
|
||||
@@ -4,10 +4,8 @@ import path from "node:path";
|
||||
import type { ResolvedAcpxPluginConfig } from "./config.js";
|
||||
|
||||
const CODEX_ACP_PACKAGE = "@zed-industries/codex-acp";
|
||||
const CODEX_ACP_PACKAGE_RANGE = "^0.12.0";
|
||||
const CODEX_ACP_BIN = "codex-acp";
|
||||
const CLAUDE_ACP_PACKAGE = "@agentclientprotocol/claude-agent-acp";
|
||||
const CLAUDE_ACP_PACKAGE_VERSION = "0.31.4";
|
||||
const CLAUDE_ACP_BIN = "claude-agent-acp";
|
||||
const RUN_CONFIGURED_COMMAND_SENTINEL = "--openclaw-run-configured";
|
||||
const requireFromHere = createRequire(import.meta.url);
|
||||
@@ -15,8 +13,22 @@ const requireFromHere = createRequire(import.meta.url);
|
||||
type PackageManifest = {
|
||||
name?: unknown;
|
||||
bin?: unknown;
|
||||
dependencies?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
const selfManifest = requireFromHere("../package.json") as PackageManifest;
|
||||
|
||||
function readManifestDependencyVersion(packageName: string): string {
|
||||
const version = selfManifest.dependencies?.[packageName];
|
||||
if (typeof version !== "string" || version.trim() === "") {
|
||||
throw new Error(`Missing ${packageName} dependency version in @openclaw/acpx manifest`);
|
||||
}
|
||||
return version;
|
||||
}
|
||||
|
||||
const CODEX_ACP_PACKAGE_VERSION = readManifestDependencyVersion(CODEX_ACP_PACKAGE);
|
||||
const CLAUDE_ACP_PACKAGE_VERSION = readManifestDependencyVersion(CLAUDE_ACP_PACKAGE);
|
||||
|
||||
function quoteCommandPart(value: string): string {
|
||||
return JSON.stringify(value);
|
||||
}
|
||||
@@ -205,7 +217,7 @@ child.on("exit", (code, signal) => {
|
||||
function buildCodexAcpWrapperScript(installedBinPath?: string): string {
|
||||
return buildAdapterWrapperScript({
|
||||
displayName: "Codex",
|
||||
packageSpec: `${CODEX_ACP_PACKAGE}@${CODEX_ACP_PACKAGE_RANGE}`,
|
||||
packageSpec: `${CODEX_ACP_PACKAGE}@${CODEX_ACP_PACKAGE_VERSION}`,
|
||||
binName: CODEX_ACP_BIN,
|
||||
installedBinPath,
|
||||
envSetup: `const codexHome = fileURLToPath(new URL("./codex-home/", import.meta.url));
|
||||
|
||||
@@ -13,8 +13,8 @@ describe("acpx package manifest", () => {
|
||||
) as AcpxPackageManifest;
|
||||
|
||||
expect(packageJson.dependencies?.acpx).toBeDefined();
|
||||
expect(packageJson.dependencies?.["@zed-industries/codex-acp"]).toBe("0.12.0");
|
||||
expect(packageJson.dependencies?.["@agentclientprotocol/claude-agent-acp"]).toBe("0.31.4");
|
||||
expect(packageJson.dependencies?.["@zed-industries/codex-acp"]).toBe("0.13.0");
|
||||
expect(packageJson.dependencies?.["@agentclientprotocol/claude-agent-acp"]).toBe("0.32.0");
|
||||
expect(packageJson.devDependencies?.["@agentclientprotocol/claude-agent-acp"]).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,7 +9,7 @@ type TestSessionStore = {
|
||||
|
||||
const DOCUMENTED_OPENCLAW_BRIDGE_COMMAND =
|
||||
"env OPENCLAW_HIDE_BANNER=1 OPENCLAW_SUPPRESS_NOTES=1 openclaw acp --url ws://127.0.0.1:18789 --token-file ~/.openclaw/gateway.token --session agent:main:main";
|
||||
const CODEX_ACP_COMMAND = "npx @zed-industries/codex-acp@^0.12.0";
|
||||
const CODEX_ACP_COMMAND = "npx @zed-industries/codex-acp@0.13.0";
|
||||
const CODEX_ACP_WRAPPER_COMMAND = `node "/tmp/openclaw/acpx/codex-acp-wrapper.mjs"`;
|
||||
|
||||
function makeRuntime(
|
||||
@@ -226,7 +226,7 @@ describe("AcpxRuntime fresh reset wrapper", () => {
|
||||
reasoningEffort: "medium",
|
||||
}),
|
||||
).toBe(
|
||||
"npx @zed-industries/codex-acp@^0.12.0 -c model=gpt-5.4 -c model_reasoning_effort=medium",
|
||||
"npx @zed-industries/codex-acp@0.13.0 -c model=gpt-5.4 -c model_reasoning_effort=medium",
|
||||
);
|
||||
expect(__testing.isCodexAcpCommand("openclaw acp")).toBe(false);
|
||||
});
|
||||
|
||||
@@ -125,6 +125,23 @@ describe("active-memory plugin", () => {
|
||||
"utf8",
|
||||
);
|
||||
};
|
||||
const makeMemoryToolAllowlistError = (
|
||||
reason: string,
|
||||
sources = "runtime toolsAllow: memory_recall, memory_search, memory_get",
|
||||
) =>
|
||||
new Error(
|
||||
`No callable tools remain after resolving explicit tool allowlist ` +
|
||||
`(${sources}); ${reason}. ` +
|
||||
`Fix the allowlist or enable the plugin that registers the requested tool.`,
|
||||
);
|
||||
const hasDebugLine = (needle: string) =>
|
||||
vi
|
||||
.mocked(api.logger.debug)
|
||||
.mock.calls.some((call: unknown[]) => String(call[0]).includes(needle));
|
||||
const hasWarnLine = (needle: string) =>
|
||||
vi
|
||||
.mocked(api.logger.warn)
|
||||
.mock.calls.some((call: unknown[]) => String(call[0]).includes(needle));
|
||||
|
||||
beforeEach(async () => {
|
||||
vi.clearAllMocks();
|
||||
@@ -1646,6 +1663,133 @@ describe("active-memory plugin", () => {
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it("skips the recall subagent when no registered memory tools match", async () => {
|
||||
const sessionKey = "agent:main:missing-memory-tools";
|
||||
hoisted.sessionStore[sessionKey] = {
|
||||
sessionId: "s-missing-memory-tools",
|
||||
updatedAt: 0,
|
||||
};
|
||||
const error = makeMemoryToolAllowlistError("no registered tools matched");
|
||||
expect(__testing.isMissingRegisteredMemoryToolsError(error)).toBe(true);
|
||||
runEmbeddedPiAgent.mockRejectedValueOnce(error);
|
||||
|
||||
const result = await hooks.before_prompt_build(
|
||||
{ prompt: "what wings should i order? missing memory tools", messages: [] },
|
||||
{ agentId: "main", trigger: "user", sessionKey, messageProvider: "webchat" },
|
||||
);
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
expect(hasDebugLine("no memory tools registered")).toBe(true);
|
||||
expect(hasWarnLine("No callable tools remain")).toBe(false);
|
||||
const lines = getActiveMemoryLines(sessionKey);
|
||||
expect(lines).toEqual([expect.stringContaining("🧩 Active Memory: status=empty")]);
|
||||
expect(lines.join("\n")).not.toContain("status=unavailable");
|
||||
});
|
||||
|
||||
it("skips missing memory tools when the allowlist error includes inherited sources", async () => {
|
||||
const sessionKey = "agent:main:missing-memory-tools-with-policy-source";
|
||||
hoisted.sessionStore[sessionKey] = {
|
||||
sessionId: "s-missing-memory-tools-with-policy-source",
|
||||
updatedAt: 0,
|
||||
};
|
||||
const error = makeMemoryToolAllowlistError(
|
||||
"no registered tools matched",
|
||||
"tools.allow: *, lobster; runtime toolsAllow: memory_recall, memory_search, memory_get",
|
||||
);
|
||||
expect(__testing.isMissingRegisteredMemoryToolsError(error)).toBe(true);
|
||||
runEmbeddedPiAgent.mockRejectedValueOnce(error);
|
||||
|
||||
const result = await hooks.before_prompt_build(
|
||||
{ prompt: "what wings should i order? missing memory tools with policy", messages: [] },
|
||||
{ agentId: "main", trigger: "user", sessionKey, messageProvider: "webchat" },
|
||||
);
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
expect(hasDebugLine("no memory tools registered")).toBe(true);
|
||||
expect(hasWarnLine("No callable tools remain")).toBe(false);
|
||||
expect(getActiveMemoryLines(sessionKey)).toEqual([
|
||||
expect.stringContaining("🧩 Active Memory: status=empty"),
|
||||
]);
|
||||
});
|
||||
|
||||
it("keeps memory-tool allowlist errors visible when upstream policy can filter memory tools", async () => {
|
||||
const sessionKey = "agent:main:memory-tools-filtered-by-policy";
|
||||
hoisted.sessionStore[sessionKey] = {
|
||||
sessionId: "s-memory-tools-filtered-by-policy",
|
||||
updatedAt: 0,
|
||||
};
|
||||
const error = makeMemoryToolAllowlistError(
|
||||
"no registered tools matched",
|
||||
"tools.allow: read, exec; runtime toolsAllow: memory_recall, memory_search, memory_get",
|
||||
);
|
||||
expect(__testing.isMissingRegisteredMemoryToolsError(error)).toBe(false);
|
||||
runEmbeddedPiAgent.mockRejectedValueOnce(error);
|
||||
|
||||
const result = await hooks.before_prompt_build(
|
||||
{ prompt: "what wings should i order? memory tools filtered by policy", messages: [] },
|
||||
{ agentId: "main", trigger: "user", sessionKey, messageProvider: "webchat" },
|
||||
);
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
expect(hasDebugLine("no memory tools registered")).toBe(false);
|
||||
expect(hasWarnLine("No callable tools remain")).toBe(true);
|
||||
expect(getActiveMemoryLines(sessionKey)).toEqual([
|
||||
expect.stringContaining("🧩 Active Memory: status=unavailable"),
|
||||
]);
|
||||
});
|
||||
|
||||
it.each([
|
||||
["disabled tools", "tools are disabled for this run"],
|
||||
["models without tool support", "the selected model does not support tools"],
|
||||
])("keeps allowlist errors for %s visible", async (_label, reason) => {
|
||||
const sessionKey = `agent:main:${reason.replace(/\W+/g, "-")}`;
|
||||
hoisted.sessionStore[sessionKey] = {
|
||||
sessionId: `s-${reason.replace(/\W+/g, "-")}`,
|
||||
updatedAt: 0,
|
||||
};
|
||||
const error = makeMemoryToolAllowlistError(reason);
|
||||
expect(__testing.isMissingRegisteredMemoryToolsError(error)).toBe(false);
|
||||
runEmbeddedPiAgent.mockRejectedValueOnce(error);
|
||||
|
||||
const result = await hooks.before_prompt_build(
|
||||
{ prompt: `what wings should i order? ${reason}`, messages: [] },
|
||||
{ agentId: "main", trigger: "user", sessionKey, messageProvider: "webchat" },
|
||||
);
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
expect(hasDebugLine("no memory tools registered")).toBe(false);
|
||||
expect(hasWarnLine(reason)).toBe(true);
|
||||
expect(getActiveMemoryLines(sessionKey)).toEqual([
|
||||
expect.stringContaining("🧩 Active Memory: status=unavailable"),
|
||||
]);
|
||||
});
|
||||
|
||||
it("does not skip missing memory-tool allowlist errors after abort", async () => {
|
||||
const sessionKey = "agent:main:missing-memory-tools-after-abort";
|
||||
hoisted.sessionStore[sessionKey] = {
|
||||
sessionId: "s-missing-memory-tools-after-abort",
|
||||
updatedAt: 0,
|
||||
};
|
||||
runEmbeddedPiAgent.mockImplementationOnce(async (params: { abortSignal?: AbortSignal }) => {
|
||||
Object.defineProperty(params.abortSignal as AbortSignal, "aborted", {
|
||||
configurable: true,
|
||||
value: true,
|
||||
});
|
||||
throw makeMemoryToolAllowlistError("no registered tools matched");
|
||||
});
|
||||
|
||||
const result = await hooks.before_prompt_build(
|
||||
{ prompt: "what wings should i order? missing memory tools after abort", messages: [] },
|
||||
{ agentId: "main", trigger: "user", sessionKey, messageProvider: "webchat" },
|
||||
);
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
expect(hasDebugLine("no memory tools registered")).toBe(false);
|
||||
expect(getActiveMemoryLines(sessionKey)).toEqual([
|
||||
expect.stringContaining("🧩 Active Memory: status=timeout"),
|
||||
]);
|
||||
});
|
||||
|
||||
it("returns partial transcript text on timeout when the subagent has already written assistant output", async () => {
|
||||
__testing.setMinimumTimeoutMsForTests(1);
|
||||
__testing.setSetupGraceTimeoutMsForTests(0);
|
||||
|
||||
@@ -41,6 +41,7 @@ const DEFAULT_QMD_SEARCH_MODE = "search" as const;
|
||||
const DEFAULT_TRANSCRIPT_DIR = "active-memory";
|
||||
const DEFAULT_CIRCUIT_BREAKER_MAX_TIMEOUTS = 3;
|
||||
const DEFAULT_CIRCUIT_BREAKER_COOLDOWN_MS = 60_000;
|
||||
const ACTIVE_MEMORY_TOOL_ALLOWLIST = ["memory_recall", "memory_search", "memory_get"] as const;
|
||||
const TOGGLE_STATE_FILE = "session-toggles.json";
|
||||
const DEFAULT_PARTIAL_TRANSCRIPT_MAX_CHARS = 32_000;
|
||||
const DEFAULT_TRANSCRIPT_READ_MAX_LINES = 2_000;
|
||||
@@ -494,6 +495,38 @@ function normalizeOptionalString(value: unknown): string | undefined {
|
||||
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
||||
}
|
||||
|
||||
function isMissingRegisteredMemoryToolsError(error: unknown): boolean {
|
||||
if (!(error instanceof Error)) {
|
||||
return false;
|
||||
}
|
||||
const message = error.message.trim();
|
||||
const prefix = "No callable tools remain after resolving explicit tool allowlist (";
|
||||
const suffix =
|
||||
"); no registered tools matched. Fix the allowlist or enable the plugin that registers the requested tool.";
|
||||
if (!message.startsWith(prefix) || !message.endsWith(suffix)) {
|
||||
return false;
|
||||
}
|
||||
const sources = message.slice(prefix.length, -suffix.length);
|
||||
const runtimeSource = `runtime toolsAllow: ${ACTIVE_MEMORY_TOOL_ALLOWLIST.join(", ")}`;
|
||||
const sourceParts = sources
|
||||
.split(";")
|
||||
.map((source) => source.trim())
|
||||
.filter(Boolean);
|
||||
if (!sourceParts.includes(runtimeSource)) {
|
||||
return false;
|
||||
}
|
||||
return sourceParts.every((source) => {
|
||||
if (source === runtimeSource) {
|
||||
return true;
|
||||
}
|
||||
const entries = source
|
||||
.slice(source.indexOf(":") + 1)
|
||||
.split(",")
|
||||
.map((entry) => entry.trim());
|
||||
return entries.includes("*");
|
||||
});
|
||||
}
|
||||
|
||||
function resolveRecallRunChannelContext(params: {
|
||||
api: OpenClawPluginApi;
|
||||
agentId: string;
|
||||
@@ -2394,7 +2427,7 @@ async function runRecallSubagent(params: {
|
||||
timeoutMs: embeddedTimeoutMs,
|
||||
runId: subagentSessionId,
|
||||
trigger: "manual",
|
||||
toolsAllow: ["memory_recall", "memory_search", "memory_get"],
|
||||
toolsAllow: [...ACTIVE_MEMORY_TOOL_ALLOWLIST],
|
||||
disableMessageTool: true,
|
||||
allowGatewaySubagentBinding: true,
|
||||
bootstrapContextMode: "lightweight",
|
||||
@@ -2437,6 +2470,12 @@ async function runRecallSubagent(params: {
|
||||
const searchDebug = partialReply ? await readActiveMemorySearchDebug(sessionFile) : undefined;
|
||||
attachPartialTimeoutData(error, partialReply, searchDebug);
|
||||
}
|
||||
if (!params.abortSignal?.aborted && isMissingRegisteredMemoryToolsError(error)) {
|
||||
params.api.logger.debug?.(
|
||||
`active-memory: no memory tools registered (memory-core or memory-lancedb required); skipping sub-agent`,
|
||||
);
|
||||
return { rawReply: "NONE" };
|
||||
}
|
||||
throw error;
|
||||
} finally {
|
||||
if (tempDir) {
|
||||
@@ -2959,6 +2998,7 @@ const testing = {
|
||||
buildPromptPrefix,
|
||||
getCachedResult,
|
||||
isCircuitBreakerOpen,
|
||||
isMissingRegisteredMemoryToolsError,
|
||||
normalizePluginConfig,
|
||||
readActiveMemorySearchDebug,
|
||||
readPartialAssistantText,
|
||||
|
||||
@@ -5,9 +5,9 @@
|
||||
"description": "OpenClaw Amazon Bedrock Mantle (OpenAI-compatible) provider plugin",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "0.92.0",
|
||||
"@anthropic-ai/sdk": "0.93.0",
|
||||
"@aws/bedrock-token-generator": "^1.1.0",
|
||||
"@mariozechner/pi-ai": "0.71.1"
|
||||
"@mariozechner/pi-ai": "0.73.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@openclaw/plugin-sdk": "workspace:*"
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
"description": "OpenClaw Amazon Bedrock provider plugin",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-bedrock": "3.1041.0",
|
||||
"@aws-sdk/client-bedrock-runtime": "3.1041.0",
|
||||
"@aws-sdk/client-bedrock": "3.1042.0",
|
||||
"@aws-sdk/client-bedrock-runtime": "3.1042.0",
|
||||
"@aws-sdk/credential-provider-node": "3.972.39"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/vertex-sdk": "^0.16.0",
|
||||
"@mariozechner/pi-agent-core": "0.71.1",
|
||||
"@mariozechner/pi-ai": "0.71.1"
|
||||
"@mariozechner/pi-agent-core": "0.73.0",
|
||||
"@mariozechner/pi-ai": "0.73.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@openclaw/plugin-sdk": "workspace:*"
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"description": "OpenClaw Anthropic provider plugin",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@mariozechner/pi-ai": "0.71.1"
|
||||
"@mariozechner/pi-ai": "0.73.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@openclaw/plugin-sdk": "workspace:*"
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"description": "OpenClaw Bonjour/mDNS gateway discovery",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@homebridge/ciao": "^1.3.7"
|
||||
"@homebridge/ciao": "^1.3.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@openclaw/plugin-sdk": "workspace:*"
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@openclaw/plugin-sdk": "workspace:*",
|
||||
"undici": "8.1.0"
|
||||
"undici": "8.2.0"
|
||||
},
|
||||
"openclaw": {
|
||||
"extensions": [
|
||||
|
||||
@@ -8,11 +8,11 @@
|
||||
},
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@mariozechner/pi-coding-agent": "0.71.1",
|
||||
"@mariozechner/pi-coding-agent": "0.73.0",
|
||||
"@openai/codex": "0.128.0",
|
||||
"ajv": "^8.20.0",
|
||||
"ws": "^8.20.0",
|
||||
"zod": "^4.4.1"
|
||||
"zod": "^4.4.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@openclaw/plugin-sdk": "workspace:*"
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
"https-proxy-agent": "^9.0.0",
|
||||
"opusscript": "^0.1.1",
|
||||
"typebox": "1.1.37",
|
||||
"undici": "8.1.0",
|
||||
"undici": "8.2.0",
|
||||
"ws": "^8.20.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"description": "OpenClaw Fireworks provider plugin",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@mariozechner/pi-ai": "0.71.1"
|
||||
"@mariozechner/pi-ai": "0.73.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@openclaw/plugin-sdk": "workspace:*"
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"@clack/prompts": "^1.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@mariozechner/pi-ai": "0.71.1",
|
||||
"@mariozechner/pi-ai": "0.73.0",
|
||||
"@openclaw/plugin-sdk": "workspace:*"
|
||||
},
|
||||
"openclaw": {
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@google/genai": "^1.51.0",
|
||||
"@mariozechner/pi-ai": "0.71.1"
|
||||
"@mariozechner/pi-ai": "0.73.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@openclaw/plugin-sdk": "workspace:*"
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
"dependencies": {
|
||||
"gaxios": "7.1.4",
|
||||
"google-auth-library": "10.6.2",
|
||||
"zod": "^4.4.1"
|
||||
"zod": "^4.4.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@openclaw/plugin-sdk": "workspace:*",
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"description": "OpenClaw Kimi provider plugin",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@mariozechner/pi-ai": "0.71.1"
|
||||
"@mariozechner/pi-ai": "0.73.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@openclaw/plugin-sdk": "workspace:*"
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"description": "OpenClaw LM Studio provider plugin",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@mariozechner/pi-ai": "0.71.1"
|
||||
"@mariozechner/pi-ai": "0.73.0"
|
||||
},
|
||||
"openclaw": {
|
||||
"extensions": [
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
"dependencies": {
|
||||
"@lancedb/lancedb": "^0.27.2",
|
||||
"apache-arrow": "18.1.0",
|
||||
"openai": "^6.35.0",
|
||||
"openai": "^6.36.0",
|
||||
"typebox": "1.1.37"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"typebox": "1.1.37",
|
||||
"yaml": "^2.8.3"
|
||||
"yaml": "^2.8.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@openclaw/plugin-sdk": "workspace:*",
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"description": "Hermes to OpenClaw migration provider",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"yaml": "^2.8.3"
|
||||
"yaml": "^2.8.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@openclaw/plugin-sdk": "workspace:*",
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
},
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"zod": "^4.4.1"
|
||||
"zod": "^4.4.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@openclaw/plugin-sdk": "workspace:*",
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"nostr-tools": "^2.23.3",
|
||||
"zod": "^4.4.1"
|
||||
"zod": "^4.4.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@openclaw/plugin-sdk": "workspace:*",
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"description": "OpenClaw Ollama provider plugin",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@mariozechner/pi-ai": "0.71.1",
|
||||
"@mariozechner/pi-ai": "0.73.0",
|
||||
"typebox": "1.1.37"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -60,7 +60,7 @@ function providerWizardByKey() {
|
||||
|
||||
describe("OpenAI plugin manifest", () => {
|
||||
it("keeps runtime dependencies in the package manifest", () => {
|
||||
expect(packageJson.dependencies?.["@mariozechner/pi-ai"]).toBe("0.71.1");
|
||||
expect(packageJson.dependencies?.["@mariozechner/pi-ai"]).toBe("0.73.0");
|
||||
expect(packageJson.dependencies?.ws).toBe("^8.20.0");
|
||||
});
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"description": "OpenClaw OpenAI provider plugins",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@mariozechner/pi-ai": "0.71.1",
|
||||
"@mariozechner/pi-ai": "0.73.0",
|
||||
"ws": "^8.20.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -5,11 +5,11 @@
|
||||
"description": "OpenClaw QA lab plugin with private debugger UI and scenario runner",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@copilotkit/aimock": "1.16.4",
|
||||
"@copilotkit/aimock": "1.17.0",
|
||||
"@modelcontextprotocol/sdk": "1.29.0",
|
||||
"playwright-core": "1.59.1",
|
||||
"yaml": "^2.8.3",
|
||||
"zod": "^4.4.1"
|
||||
"yaml": "^2.8.4",
|
||||
"zod": "^4.4.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@openclaw/discord": "workspace:*",
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"description": "OpenClaw Matrix QA runner plugin",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"undici": "8.1.0"
|
||||
"undici": "8.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@openclaw/matrix": "workspace:*",
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
"mpg123-decoder": "^1.0.3",
|
||||
"silk-wasm": "^3.7.1",
|
||||
"ws": "^8.20.0",
|
||||
"zod": "^4.4.1"
|
||||
"zod": "^4.4.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@openclaw/plugin-sdk": "workspace:*",
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@slack/bolt": "^4.7.2",
|
||||
"@slack/types": "^2.20.1",
|
||||
"@slack/web-api": "^7.15.1",
|
||||
"@slack/types": "^2.21.0",
|
||||
"@slack/web-api": "^7.15.2",
|
||||
"https-proxy-agent": "^9.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
},
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"zod": "^4.4.1"
|
||||
"zod": "^4.4.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@openclaw/plugin-sdk": "workspace:*"
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"@grammyjs/transformer-throttler": "^1.2.1",
|
||||
"grammy": "^1.42.0",
|
||||
"typebox": "1.1.37",
|
||||
"undici": "8.1.0"
|
||||
"undici": "8.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@openclaw/plugin-sdk": "workspace:*"
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
import { getRuntimeConfig } from "openclaw/plugin-sdk/runtime-config-snapshot";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const { defaultRouteConfig } = vi.hoisted(() => ({
|
||||
defaultRouteConfig: {
|
||||
agents: {
|
||||
list: [{ id: "main", default: true }],
|
||||
},
|
||||
channels: { telegram: {} },
|
||||
messages: { groupChat: { mentionPatterns: [] } },
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("openclaw/plugin-sdk/runtime-config-snapshot", async () => {
|
||||
const actual = await vi.importActual<
|
||||
typeof import("openclaw/plugin-sdk/runtime-config-snapshot")
|
||||
>("openclaw/plugin-sdk/runtime-config-snapshot");
|
||||
return {
|
||||
...actual,
|
||||
getRuntimeConfig: vi.fn(() => defaultRouteConfig),
|
||||
};
|
||||
});
|
||||
|
||||
const { buildTelegramMessageContextForTest } =
|
||||
await import("./bot-message-context.test-harness.js");
|
||||
|
||||
describe("buildTelegramMessageContext requireMention precedence", () => {
|
||||
function buildForumMessage(threadId = 99) {
|
||||
return {
|
||||
message_id: 1,
|
||||
chat: {
|
||||
id: -1001234567890,
|
||||
type: "supergroup" as const,
|
||||
title: "Forum",
|
||||
is_forum: true,
|
||||
},
|
||||
date: 1_700_000_000,
|
||||
text: "hello everyone",
|
||||
message_thread_id: threadId,
|
||||
from: { id: 42, first_name: "Alice" },
|
||||
};
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.mocked(getRuntimeConfig).mockReturnValue(defaultRouteConfig as never);
|
||||
});
|
||||
|
||||
it("lets explicit topic requireMention=false override group requireMention=true", async () => {
|
||||
const ctx = await buildTelegramMessageContextForTest({
|
||||
message: buildForumMessage(),
|
||||
resolveGroupActivation: () => undefined,
|
||||
resolveGroupRequireMention: () => true,
|
||||
resolveTelegramGroupConfig: () => ({
|
||||
groupConfig: { requireMention: true },
|
||||
topicConfig: { requireMention: false },
|
||||
}),
|
||||
});
|
||||
|
||||
expect(ctx).not.toBeNull();
|
||||
});
|
||||
|
||||
it("lets explicit topic requireMention=false override mention activation", async () => {
|
||||
const resolveGroupActivation = vi.fn(() => true);
|
||||
|
||||
const ctx = await buildTelegramMessageContextForTest({
|
||||
message: buildForumMessage(),
|
||||
resolveGroupActivation,
|
||||
resolveGroupRequireMention: () => true,
|
||||
resolveTelegramGroupConfig: () => ({
|
||||
groupConfig: { requireMention: true },
|
||||
topicConfig: { requireMention: false },
|
||||
}),
|
||||
});
|
||||
|
||||
expect(ctx).not.toBeNull();
|
||||
expect(resolveGroupActivation).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
chatId: -1001234567890,
|
||||
messageThreadId: 99,
|
||||
sessionKey: "agent:main:telegram:group:-1001234567890:topic:99",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("lets explicit topic requireMention=true override always activation", async () => {
|
||||
const ctx = await buildTelegramMessageContextForTest({
|
||||
message: buildForumMessage(),
|
||||
resolveGroupActivation: () => false,
|
||||
resolveGroupRequireMention: () => false,
|
||||
resolveTelegramGroupConfig: () => ({
|
||||
groupConfig: { requireMention: false },
|
||||
topicConfig: { requireMention: true },
|
||||
}),
|
||||
});
|
||||
|
||||
expect(ctx).toBeNull();
|
||||
});
|
||||
|
||||
it("keeps activation fallback when no topic requireMention is configured", async () => {
|
||||
const ctx = await buildTelegramMessageContextForTest({
|
||||
message: buildForumMessage(),
|
||||
resolveGroupActivation: () => false,
|
||||
resolveGroupRequireMention: () => true,
|
||||
resolveTelegramGroupConfig: () => ({
|
||||
groupConfig: { requireMention: true },
|
||||
topicConfig: { agentId: "main" },
|
||||
}),
|
||||
});
|
||||
|
||||
expect(ctx).not.toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -411,8 +411,8 @@ export const buildTelegramMessageContext = async ({
|
||||
});
|
||||
const baseRequireMention = resolveGroupRequireMention(chatId);
|
||||
const requireMention = firstDefined(
|
||||
activationOverride,
|
||||
topicConfig?.requireMention,
|
||||
activationOverride,
|
||||
telegramGroupConfig?.requireMention,
|
||||
baseRequireMention,
|
||||
);
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
},
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "3.1041.0",
|
||||
"@aws-sdk/s3-request-presigner": "3.1041.0",
|
||||
"@aws-sdk/client-s3": "3.1042.0",
|
||||
"@aws-sdk/s3-request-presigner": "3.1042.0",
|
||||
"@tloncorp/tlon-skill": "0.3.5",
|
||||
"@urbit/aura": "^3.0.0"
|
||||
},
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"description": "OpenClaw webhook bridge plugin",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"zod": "^4.4.1"
|
||||
"zod": "^4.4.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@openclaw/plugin-sdk": "workspace:*"
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"https-proxy-agent": "^9.0.0",
|
||||
"jimp": "^1.6.1",
|
||||
"typebox": "1.1.37",
|
||||
"undici": "8.1.0"
|
||||
"undici": "8.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@openclaw/plugin-sdk": "workspace:*",
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"description": "OpenClaw xAI plugin",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@mariozechner/pi-ai": "0.71.1",
|
||||
"@mariozechner/pi-ai": "0.73.0",
|
||||
"typebox": "1.1.37"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
},
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"undici": "8.1.0"
|
||||
"undici": "8.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@openclaw/plugin-sdk": "workspace:*",
|
||||
|
||||
38
package.json
38
package.json
@@ -1663,27 +1663,27 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@agentclientprotocol/sdk": "0.21.0",
|
||||
"@anthropic-ai/sdk": "0.92.0",
|
||||
"@anthropic-ai/sdk": "0.93.0",
|
||||
"@anthropic-ai/vertex-sdk": "^0.16.0",
|
||||
"@aws-sdk/client-bedrock": "3.1041.0",
|
||||
"@aws-sdk/client-bedrock-runtime": "3.1041.0",
|
||||
"@aws-sdk/client-bedrock": "3.1042.0",
|
||||
"@aws-sdk/client-bedrock-runtime": "3.1042.0",
|
||||
"@aws-sdk/credential-provider-node": "3.972.39",
|
||||
"@aws/bedrock-token-generator": "^1.1.0",
|
||||
"@clack/prompts": "^1.3.0",
|
||||
"@google/genai": "^1.51.0",
|
||||
"@grammyjs/runner": "^2.0.3",
|
||||
"@grammyjs/transformer-throttler": "^1.2.1",
|
||||
"@homebridge/ciao": "^1.3.7",
|
||||
"@homebridge/ciao": "^1.3.8",
|
||||
"@lydell/node-pty": "1.2.0-beta.12",
|
||||
"@mariozechner/pi-agent-core": "0.71.1",
|
||||
"@mariozechner/pi-ai": "0.71.1",
|
||||
"@mariozechner/pi-coding-agent": "0.71.1",
|
||||
"@mariozechner/pi-tui": "0.71.1",
|
||||
"@mariozechner/pi-agent-core": "0.73.0",
|
||||
"@mariozechner/pi-ai": "0.73.0",
|
||||
"@mariozechner/pi-coding-agent": "0.73.0",
|
||||
"@mariozechner/pi-tui": "0.73.0",
|
||||
"@modelcontextprotocol/sdk": "1.29.0",
|
||||
"@mozilla/readability": "^0.6.0",
|
||||
"@slack/bolt": "^4.7.2",
|
||||
"@slack/types": "^2.20.1",
|
||||
"@slack/web-api": "^7.15.1",
|
||||
"@slack/types": "^2.21.0",
|
||||
"@slack/web-api": "^7.15.2",
|
||||
"ajv": "^8.20.0",
|
||||
"chalk": "^5.6.2",
|
||||
"chokidar": "^5.0.0",
|
||||
@@ -1695,7 +1695,7 @@
|
||||
"global-agent": "^4.1.3",
|
||||
"grammy": "^1.42.0",
|
||||
"https-proxy-agent": "^9.0.0",
|
||||
"ipaddr.js": "^2.3.0",
|
||||
"ipaddr.js": "^2.4.0",
|
||||
"jiti": "^2.6.1",
|
||||
"json5": "^2.2.3",
|
||||
"jszip": "^3.10.1",
|
||||
@@ -1703,7 +1703,7 @@
|
||||
"markdown-it": "14.1.1",
|
||||
"minimatch": "10.2.5",
|
||||
"node-edge-tts": "^1.2.10",
|
||||
"openai": "^6.35.0",
|
||||
"openai": "^6.36.0",
|
||||
"openshell": "0.1.0",
|
||||
"pdfjs-dist": "^5.7.284",
|
||||
"playwright-core": "1.59.1",
|
||||
@@ -1714,15 +1714,15 @@
|
||||
"tree-sitter-bash": "^0.25.1",
|
||||
"tslog": "^4.10.2",
|
||||
"typebox": "1.1.37",
|
||||
"undici": "8.1.0",
|
||||
"undici": "8.2.0",
|
||||
"web-push": "^3.6.7",
|
||||
"web-tree-sitter": "^0.26.8",
|
||||
"ws": "^8.20.0",
|
||||
"yaml": "^2.8.3",
|
||||
"zod": "^4.4.1"
|
||||
"yaml": "^2.8.4",
|
||||
"zod": "^4.4.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@copilotkit/aimock": "1.16.4",
|
||||
"@copilotkit/aimock": "1.17.0",
|
||||
"@grammyjs/types": "^3.26.0",
|
||||
"@lit-labs/signals": "^0.2.0",
|
||||
"@lit/context": "^1.1.6",
|
||||
@@ -1731,7 +1731,7 @@
|
||||
"@types/markdown-it": "^14.1.2",
|
||||
"@types/node": "25.6.0",
|
||||
"@types/ws": "^8.18.1",
|
||||
"@typescript/native-preview": "7.0.0-dev.20260501.1",
|
||||
"@typescript/native-preview": "7.0.0-dev.20260504.1",
|
||||
"@vitest/coverage-v8": "^4.1.5",
|
||||
"jscpd": "4.0.9",
|
||||
"jsdom": "^29.1.1",
|
||||
@@ -1761,7 +1761,7 @@
|
||||
"packageManager": "pnpm@10.33.2+sha512.a90faf6feeab71ad6c6e57f94e0fe1a12f5dcc22cd754db40ae9593eb6a3e0b6b12e3540218bb37ae083404b1f2ce6db2a4121e979829b4aff94b99f49da1cf8",
|
||||
"pnpm": {
|
||||
"overrides": {
|
||||
"@anthropic-ai/sdk": "0.92.0",
|
||||
"@anthropic-ai/sdk": "0.93.0",
|
||||
"hono": "4.12.14",
|
||||
"@hono/node-server": "1.19.14",
|
||||
"@aws-sdk/client-bedrock-runtime": "3.1024.0",
|
||||
@@ -1818,7 +1818,7 @@
|
||||
},
|
||||
"patchedDependencies": {
|
||||
"@whiskeysockets/baileys@7.0.0-rc.9": "patches/@whiskeysockets__baileys@7.0.0-rc.9.patch",
|
||||
"@agentclientprotocol/claude-agent-acp@0.31.4": "patches/@agentclientprotocol__claude-agent-acp@0.31.4.patch"
|
||||
"@agentclientprotocol/claude-agent-acp@0.32.0": "patches/@agentclientprotocol__claude-agent-acp@0.32.0.patch"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
diff --git a/dist/acp-agent.js b/dist/acp-agent.js
|
||||
index 0a8f5e3c57ed05189cba546bd65fc18143744d09..a8522d86a5a2f1bbcdd446d222cb9b7b5acb79ca 100644
|
||||
index e1d9aa9f0815f57ea2fd299a7f2b8ef0917ca191..875fdfb25fbfa905ca80728355d25a17e6d89148 100644
|
||||
--- a/dist/acp-agent.js
|
||||
+++ b/dist/acp-agent.js
|
||||
@@ -421,6 +421,7 @@ export class ClaudeAcpAgent {
|
||||
@@ -436,6 +436,7 @@ export class ClaudeAcpAgent {
|
||||
session.promptRunning = true;
|
||||
let handedOff = false;
|
||||
let stopReason = "end_turn";
|
||||
@@ -10,7 +10,7 @@ index 0a8f5e3c57ed05189cba546bd65fc18143744d09..a8522d86a5a2f1bbcdd446d222cb9b7b
|
||||
try {
|
||||
while (true) {
|
||||
const { value: message, done } = await session.query.next();
|
||||
@@ -428,6 +429,9 @@ export class ClaudeAcpAgent {
|
||||
@@ -443,6 +444,9 @@ export class ClaudeAcpAgent {
|
||||
if (session.cancelled) {
|
||||
return { stopReason: "cancelled" };
|
||||
}
|
||||
@@ -20,7 +20,7 @@ index 0a8f5e3c57ed05189cba546bd65fc18143744d09..a8522d86a5a2f1bbcdd446d222cb9b7b
|
||||
break;
|
||||
}
|
||||
if (session.emitRawSDKMessages &&
|
||||
@@ -496,7 +500,7 @@ export class ClaudeAcpAgent {
|
||||
@@ -499,7 +503,7 @@ export class ClaudeAcpAgent {
|
||||
break;
|
||||
}
|
||||
case "session_state_changed": {
|
||||
@@ -29,7 +29,7 @@ index 0a8f5e3c57ed05189cba546bd65fc18143744d09..a8522d86a5a2f1bbcdd446d222cb9b7b
|
||||
return { stopReason, usage: sessionUsage(session) };
|
||||
}
|
||||
break;
|
||||
@@ -601,6 +605,7 @@ export class ClaudeAcpAgent {
|
||||
@@ -621,6 +625,7 @@ export class ClaudeAcpAgent {
|
||||
unreachable(message, this.logger);
|
||||
break;
|
||||
}
|
||||
811
pnpm-lock.yaml
generated
811
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
6
scripts/prune-docker-plugin-dist.d.mts
Normal file
6
scripts/prune-docker-plugin-dist.d.mts
Normal file
@@ -0,0 +1,6 @@
|
||||
export function parseDockerPluginKeepList(value: unknown): Set<string>;
|
||||
export function pruneDockerPluginDist(params?: {
|
||||
cwd?: string;
|
||||
repoRoot?: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
}): string[];
|
||||
52
scripts/prune-docker-plugin-dist.mjs
Normal file
52
scripts/prune-docker-plugin-dist.mjs
Normal file
@@ -0,0 +1,52 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { collectRootPackageExcludedExtensionDirs } from "./lib/bundled-plugin-build-entries.mjs";
|
||||
import { removePathIfExists } from "./runtime-postbuild-shared.mjs";
|
||||
|
||||
function parsePluginList(value) {
|
||||
if (typeof value !== "string") {
|
||||
return new Set();
|
||||
}
|
||||
return new Set(
|
||||
value
|
||||
.split(/[\s,]+/u)
|
||||
.map((entry) => entry.trim())
|
||||
.filter(Boolean),
|
||||
);
|
||||
}
|
||||
|
||||
export function parseDockerPluginKeepList(value) {
|
||||
return parsePluginList(value);
|
||||
}
|
||||
|
||||
export function pruneDockerPluginDist(params = {}) {
|
||||
const repoRoot = params.cwd ?? params.repoRoot ?? process.cwd();
|
||||
const env = params.env ?? process.env;
|
||||
const keepPluginIds = parseDockerPluginKeepList(env.OPENCLAW_EXTENSIONS);
|
||||
const excludedPluginIds = collectRootPackageExcludedExtensionDirs({ cwd: repoRoot });
|
||||
const removed = [];
|
||||
|
||||
for (const pluginId of [...excludedPluginIds].toSorted((left, right) =>
|
||||
left.localeCompare(right),
|
||||
)) {
|
||||
if (keepPluginIds.has(pluginId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const root of ["dist", "dist-runtime"]) {
|
||||
const pluginDistDir = path.join(repoRoot, root, "extensions", pluginId);
|
||||
if (!fs.existsSync(pluginDistDir)) {
|
||||
continue;
|
||||
}
|
||||
removePathIfExists(pluginDistDir);
|
||||
removed.push(path.relative(repoRoot, pluginDistDir).replaceAll("\\", "/"));
|
||||
}
|
||||
}
|
||||
|
||||
return removed;
|
||||
}
|
||||
|
||||
if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) {
|
||||
pruneDockerPluginDist();
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import type { MessagingToolSend } from "./pi-embedded-messaging.types.js";
|
||||
import {
|
||||
handleToolExecutionEnd,
|
||||
handleToolExecutionStart,
|
||||
handleToolExecutionUpdate,
|
||||
} from "./pi-embedded-subscribe.handlers.tools.js";
|
||||
import type {
|
||||
ToolCallSummary,
|
||||
@@ -713,6 +714,47 @@ describe("handleToolExecutionEnd exec approval prompts", () => {
|
||||
});
|
||||
|
||||
describe("handleToolExecutionEnd derived tool events", () => {
|
||||
it("emits command output deltas for exec update results", async () => {
|
||||
const { ctx, onAgentEvent } = createTestContext();
|
||||
|
||||
await handleToolExecutionStart(
|
||||
ctx as never,
|
||||
{
|
||||
type: "tool_execution_start",
|
||||
toolName: "exec",
|
||||
toolCallId: "tool-exec-update-output",
|
||||
args: { command: "npm test" },
|
||||
} as never,
|
||||
);
|
||||
|
||||
handleToolExecutionUpdate(
|
||||
ctx as never,
|
||||
{
|
||||
type: "tool_execution_update",
|
||||
toolName: "exec",
|
||||
toolCallId: "tool-exec-update-output",
|
||||
partialResult: {
|
||||
details: {
|
||||
status: "running",
|
||||
aggregated: "RUN src/example.test.ts",
|
||||
},
|
||||
},
|
||||
} as never,
|
||||
);
|
||||
|
||||
expect(onAgentEvent).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
stream: "command_output",
|
||||
data: expect.objectContaining({
|
||||
itemId: "command:tool-exec-update-output",
|
||||
phase: "delta",
|
||||
output: "RUN src/example.test.ts",
|
||||
status: "running",
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("emits command output events for exec results", async () => {
|
||||
const { ctx, onAgentEvent } = createTestContext();
|
||||
|
||||
|
||||
@@ -772,7 +772,11 @@ export function handleToolExecutionUpdate(
|
||||
},
|
||||
});
|
||||
if (isExecToolName(toolName)) {
|
||||
const output = extractToolResultText(sanitized);
|
||||
const execDetails = readExecToolDetails(sanitized);
|
||||
const output =
|
||||
execDetails && "aggregated" in execDetails
|
||||
? execDetails.aggregated
|
||||
: extractToolResultText(sanitized);
|
||||
const commandData: AgentItemEventData = {
|
||||
itemId: buildCommandItemId(toolCallId),
|
||||
phase: "update",
|
||||
|
||||
@@ -111,6 +111,9 @@ describe("Dockerfile", () => {
|
||||
expect(dockerfile).toContain("pnpm-workspace.runtime.yaml");
|
||||
expect(dockerfile).toContain(" - ui\\n");
|
||||
expect(dockerfile).toContain("CI=true NPM_CONFIG_FROZEN_LOCKFILE=false pnpm prune --prod");
|
||||
expect(dockerfile).toContain(
|
||||
'OPENCLAW_EXTENSIONS="$OPENCLAW_EXTENSIONS" node scripts/prune-docker-plugin-dist.mjs',
|
||||
);
|
||||
expect(dockerfile).toContain("prune must not rediscover unrelated workspaces");
|
||||
expect(dockerfile).not.toContain(
|
||||
`npm install --prefix "${BUNDLED_PLUGIN_ROOT_DIR}/$ext" --omit=dev --silent`,
|
||||
|
||||
@@ -100,6 +100,12 @@ describe("clawhub helpers", () => {
|
||||
expect(satisfiesPluginApiRange("invalid", "^1.2.0")).toBe(false);
|
||||
});
|
||||
|
||||
it("treats OpenClaw CalVer correction versions as stable plugin API hosts", () => {
|
||||
expect(satisfiesPluginApiRange("2026.5.3-1", ">=2026.5.3")).toBe(true);
|
||||
expect(satisfiesPluginApiRange("2026.5.3-2", ">=2026.5.3")).toBe(true);
|
||||
expect(satisfiesPluginApiRange("2026.5.3-beta.1", ">=2026.5.3")).toBe(false);
|
||||
});
|
||||
|
||||
it("accepts legacy bare major.minor plugin api ranges as lower bounds", () => {
|
||||
expect(satisfiesPluginApiRange("2026.5.2", "2026.4")).toBe(true);
|
||||
expect(satisfiesPluginApiRange("2026.4.0", "2026.4")).toBe(true);
|
||||
|
||||
@@ -542,6 +542,13 @@ function satisfiesSemverRange(version: string, range: string): boolean {
|
||||
return tokens.every((token) => satisfiesComparator(version, token));
|
||||
}
|
||||
|
||||
const OPENCLAW_CALVER_STABLE_CORRECTION_PATTERN = /^[vV]?(\d{4}\.\d{1,2}\.\d{1,2})-\d+$/;
|
||||
|
||||
function normalizeCalVerCorrectionForPluginApi(pluginApiVersion: string): string {
|
||||
const match = OPENCLAW_CALVER_STABLE_CORRECTION_PATTERN.exec(pluginApiVersion.trim());
|
||||
return match?.[1] ?? pluginApiVersion;
|
||||
}
|
||||
|
||||
function buildUrl(params: Pick<ClawHubRequestParams, "baseUrl" | "path" | "search">): URL {
|
||||
const url = new URL(params.path, `${normalizeBaseUrl(params.baseUrl)}/`);
|
||||
for (const [key, value] of Object.entries(params.search ?? {})) {
|
||||
@@ -1046,7 +1053,10 @@ export function satisfiesPluginApiRange(
|
||||
if (!pluginApiRange) {
|
||||
return true;
|
||||
}
|
||||
return satisfiesSemverRange(pluginApiVersion, pluginApiRange);
|
||||
return satisfiesSemverRange(
|
||||
normalizeCalVerCorrectionForPluginApi(pluginApiVersion),
|
||||
pluginApiRange,
|
||||
);
|
||||
}
|
||||
|
||||
export function satisfiesGatewayMinimum(
|
||||
|
||||
@@ -852,6 +852,36 @@ describe("installPluginFromClawHub", () => {
|
||||
expect(archiveCleanupMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("installs when a CalVer correction runtime satisfies the base plugin API range", async () => {
|
||||
resolveCompatibilityHostVersionMock.mockReturnValueOnce("2026.5.3-1");
|
||||
fetchClawHubPackageVersionMock.mockResolvedValueOnce({
|
||||
version: {
|
||||
version: "2026.5.3",
|
||||
createdAt: 0,
|
||||
changelog: "",
|
||||
sha256hash: "a9eac48c6129bc44b6f93c9a9f48f6c700d191b7279a1e1915f28df6f59bb1af",
|
||||
compatibility: {
|
||||
pluginApiRange: ">=2026.5.3",
|
||||
minGatewayVersion: "2026.3.0",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const result = await installPluginFromClawHub({
|
||||
spec: "clawhub:demo",
|
||||
baseUrl: "https://clawhub.ai",
|
||||
});
|
||||
|
||||
expectSuccessfulClawHubInstall(result);
|
||||
expect(downloadClawHubPackageArchiveMock).toHaveBeenCalledTimes(1);
|
||||
expect(installPluginFromArchiveMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
archivePath: "/tmp/clawhub-demo/archive.zip",
|
||||
}),
|
||||
);
|
||||
expect(archiveCleanupMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("does not let a wildcard plugin API range hide an invalid runtime version", async () => {
|
||||
resolveCompatibilityHostVersionMock.mockReturnValueOnce("invalid");
|
||||
fetchClawHubPackageVersionMock.mockResolvedValueOnce({
|
||||
|
||||
56
src/plugins/prune-docker-plugin-dist.test.ts
Normal file
56
src/plugins/prune-docker-plugin-dist.test.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import {
|
||||
parseDockerPluginKeepList,
|
||||
pruneDockerPluginDist,
|
||||
} from "../../scripts/prune-docker-plugin-dist.mjs";
|
||||
import { cleanupTempDirs, makeTempRepoRoot, writeJsonFile } from "../../test/helpers/temp-repo.js";
|
||||
|
||||
const tempDirs: string[] = [];
|
||||
|
||||
function makeRepoRoot(prefix: string): string {
|
||||
return makeTempRepoRoot(tempDirs, prefix);
|
||||
}
|
||||
|
||||
function writeDistPluginFile(repoRoot: string, root: "dist" | "dist-runtime", pluginId: string) {
|
||||
const pluginDir = path.join(repoRoot, root, "extensions", pluginId);
|
||||
fs.mkdirSync(pluginDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(pluginDir, "openclaw.plugin.json"), "{}\n", "utf8");
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
cleanupTempDirs(tempDirs);
|
||||
});
|
||||
|
||||
describe("pruneDockerPluginDist", () => {
|
||||
it("parses space and comma separated Docker plugin keep lists", () => {
|
||||
expect([...parseDockerPluginKeepList("diagnostics-otel feishu,discord")]).toEqual([
|
||||
"diagnostics-otel",
|
||||
"feishu",
|
||||
"discord",
|
||||
]);
|
||||
});
|
||||
|
||||
it("removes package-excluded plugin dist unless Docker explicitly opts it in", () => {
|
||||
const repoRoot = makeRepoRoot("openclaw-docker-plugin-dist-");
|
||||
writeJsonFile(path.join(repoRoot, "package.json"), {
|
||||
files: ["dist/**", "!dist/extensions/diagnostics-otel/**", "!dist/extensions/feishu/**"],
|
||||
});
|
||||
writeDistPluginFile(repoRoot, "dist", "diagnostics-otel");
|
||||
writeDistPluginFile(repoRoot, "dist", "feishu");
|
||||
writeDistPluginFile(repoRoot, "dist-runtime", "feishu");
|
||||
writeDistPluginFile(repoRoot, "dist", "telegram");
|
||||
|
||||
const removed = pruneDockerPluginDist({
|
||||
repoRoot,
|
||||
env: { OPENCLAW_EXTENSIONS: "diagnostics-otel" } as NodeJS.ProcessEnv,
|
||||
});
|
||||
|
||||
expect(removed).toEqual(["dist/extensions/feishu", "dist-runtime/extensions/feishu"]);
|
||||
expect(fs.existsSync(path.join(repoRoot, "dist", "extensions", "diagnostics-otel"))).toBe(true);
|
||||
expect(fs.existsSync(path.join(repoRoot, "dist", "extensions", "feishu"))).toBe(false);
|
||||
expect(fs.existsSync(path.join(repoRoot, "dist-runtime", "extensions", "feishu"))).toBe(false);
|
||||
expect(fs.existsSync(path.join(repoRoot, "dist", "extensions", "telegram"))).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -98,6 +98,66 @@ describe("external channel secret contract api", () => {
|
||||
expect(api?.collectRuntimeConfigAssignments).toBeTypeOf("function");
|
||||
});
|
||||
|
||||
it("loads dist/ secret-contract-api sidecars for compiled npm-published external channel plugins", () => {
|
||||
const rootDir = makeTrackedTempDir("openclaw-channel-secret-contract-dist", tempDirs);
|
||||
fs.mkdirSync(path.join(rootDir, "dist"), { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(rootDir, "dist", "secret-contract-api.cjs"),
|
||||
`
|
||||
module.exports = {
|
||||
secretTargetRegistryEntries: [
|
||||
{
|
||||
id: "channels.discord.token",
|
||||
targetType: "channels.discord.token",
|
||||
configFile: "openclaw.json",
|
||||
pathPattern: "channels.discord.token",
|
||||
secretShape: "secret_input",
|
||||
expectedResolvedValue: "string",
|
||||
includeInPlan: true,
|
||||
includeInConfigure: true,
|
||||
includeInAudit: true
|
||||
}
|
||||
],
|
||||
collectRuntimeConfigAssignments(params) {
|
||||
params.context.assignments.push({
|
||||
path: "channels.discord.token",
|
||||
ref: { source: "env", provider: "default", id: "DISCORD_BOT_TOKEN" },
|
||||
expected: "string",
|
||||
apply() {}
|
||||
});
|
||||
}
|
||||
};
|
||||
`,
|
||||
"utf8",
|
||||
);
|
||||
const record = {
|
||||
id: "discord",
|
||||
origin: "global",
|
||||
channels: ["discord"],
|
||||
channelConfigs: {},
|
||||
rootDir,
|
||||
};
|
||||
loadPluginMetadataSnapshotMock.mockReturnValue({
|
||||
plugins: [record],
|
||||
});
|
||||
|
||||
const api = loadChannelSecretContractApi({
|
||||
channelId: "discord",
|
||||
config: { channels: { discord: {} } },
|
||||
env: {},
|
||||
loadablePluginOrigins: new Map([["discord", "global"]]),
|
||||
});
|
||||
|
||||
expect(api?.secretTargetRegistryEntries).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: "channels.discord.token",
|
||||
}),
|
||||
]),
|
||||
);
|
||||
expect(api?.collectRuntimeConfigAssignments).toBeTypeOf("function");
|
||||
});
|
||||
|
||||
it("skips external channel records outside the loadable plugin origin set", () => {
|
||||
const record = writeExternalChannelPlugin({ pluginId: "discord", channelId: "discord" });
|
||||
loadPluginMetadataSnapshotMock.mockReturnValue({
|
||||
|
||||
@@ -87,16 +87,21 @@ function orderedContractApiExtensions(): readonly string[] {
|
||||
}
|
||||
|
||||
function resolvePluginContractApiPath(rootDir: string): string | null {
|
||||
for (const extension of orderedContractApiExtensions()) {
|
||||
const candidate = path.join(rootDir, `secret-contract-api${extension}`);
|
||||
if (fs.existsSync(candidate)) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
for (const extension of orderedContractApiExtensions()) {
|
||||
const candidate = path.join(rootDir, `contract-api${extension}`);
|
||||
if (fs.existsSync(candidate)) {
|
||||
return candidate;
|
||||
// Compiled npm-published plugins place their public artifacts under <rootDir>/dist/
|
||||
// (per package.json `openclaw.runtimeExtensions`), while flat-layout plugins keep
|
||||
// them at <rootDir>/. Search both, preferring dist/ when running from built openclaw
|
||||
// artifacts and rootDir/ when running from source.
|
||||
const searchDirs = RUNNING_FROM_BUILT_ARTIFACT
|
||||
? [path.join(rootDir, "dist"), rootDir]
|
||||
: [rootDir, path.join(rootDir, "dist")];
|
||||
for (const basename of ["secret-contract-api", "contract-api"]) {
|
||||
for (const dir of searchDirs) {
|
||||
for (const extension of orderedContractApiExtensions()) {
|
||||
const candidate = path.join(dir, `${basename}${extension}`);
|
||||
if (fs.existsSync(candidate)) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
|
||||
Reference in New Issue
Block a user