Context engine/plugins: accept third-party engines whose info.id differs from registered slot id (#66601)

This commit is contained in:
GodsBoy
2026-04-14 18:39:36 +02:00
committed by Josh Lehman
parent 40c9d0affc
commit 988229cd8f
3 changed files with 23 additions and 10 deletions

View File

@@ -15,6 +15,7 @@ Docs: https://docs.openclaw.ai
- Cron/delivery: treat explicit `delivery.mode: "none"` runs as not requested even if the runner reports `delivered: false`, so no-delivery cron jobs no longer persist false delivery failures or errors. (#69285) Thanks @matsuri1987.
- Plugins/install: repair active and default-enabled bundled plugin runtime dependencies before import in packaged installs, so bundled Discord, WhatsApp, Slack, Telegram, and provider plugins work without putting their dependency trees in core.
- BlueBubbles: raise the outbound `/api/v1/message/text` send timeout default from 10s to 30s, and add a configurable `channels.bluebubbles.sendTimeoutMs` (also per-account) so macOS 26 setups where Private API iMessage sends stall for 60+ seconds no longer silently lose messages at the 10s abort. Probes, chat lookups, and health checks keep the shorter 10s default. Fixes #67486. (#69193) Thanks @omarshahine.
- Context engine/plugins: stop rejecting third-party context engines whose `info.id` differs from the registered plugin slot id. The strict-match contract added in 2026.4.14 broke `lossless-claw` and other plugins whose internal engine id does not equal the slot id they are registered under, producing repeated `info.id must match registered id` lane failures on every turn. Fixes #66601. (#66678) Thanks @GodsBoy.
## 2026.4.20

View File

@@ -796,15 +796,20 @@ describe("Invalid engine fallback", () => {
expect(console.error).toHaveBeenCalledWith(expect.stringContaining("missing info"));
});
it("falls back to default engine when info.id mismatches the registered id", async () => {
const engineId = `mismatched-info-id-${Date.now().toString(36)}`;
it("accepts resolved engines whose info.id differs from the registered slot id (#66601)", async () => {
// Regression for openclaw/openclaw#66601: third-party plugins like
// lossless-claw register under an external slot id ("lossless-claw") but
// the ContextEngine they return uses the plugin's own internal id
// (e.g. "lcm"). That id is metadata, not the lookup key.
const engineId = `plugin-slot-${Date.now().toString(36)}`;
const internalInfoId = "lcm";
registerContextEngine(
engineId,
() =>
({
info: { id: "legacy", name: "Broken Engine" },
info: { id: internalInfoId, name: "Lossless Context Manager", version: "0.5.2" },
async ingest() {
return { ingested: false };
return { ingested: true };
},
async assemble({ messages }: { messages: AgentMessage[] }) {
return { messages, estimatedTokens: 0 };
@@ -816,10 +821,15 @@ describe("Invalid engine fallback", () => {
);
const engine = await resolveContextEngine(configWithSlot(engineId));
expect(engine.info.id).toBe("legacy");
expect(console.error).toHaveBeenCalledWith(
expect.stringContaining(`info.id must match registered id "${engineId}"`),
);
// The engine's own info.id is preserved; resolution does not overwrite it.
expect(engine.info.id).toBe(internalInfoId);
expect(engine.info.name).toBe("Lossless Context Manager");
// And the engine is usable through the wrapper.
const result = await engine.assemble({
sessionId: "s1",
messages: [makeMockMessage("user", "hello")],
});
expect(result.estimatedTokens).toBe(0);
});
it("falls back to default engine when resolved engine omits lifecycle methods", async () => {

View File

@@ -421,11 +421,13 @@ function describeResolvedContextEngineContractError(
issues.push("missing info");
} else {
const infoRecord = info as Record<string, unknown>;
// Engines own their internal info.id; it is metadata, not a handle into the
// registry. The registered id (plugin slot id) and the engine's own id are
// allowed to differ, so we only require that info.id is a non-empty string
// for display/logging purposes and do not enforce equality with engineId.
const infoId = typeof infoRecord.id === "string" ? infoRecord.id.trim() : "";
if (!infoId) {
issues.push("missing info.id");
} else if (infoId !== engineId) {
issues.push(`info.id must match registered id "${engineId}"`);
}
if (typeof infoRecord.name !== "string" || !infoRecord.name.trim()) {
issues.push("missing info.name");