diff --git a/CHANGELOG.md b/CHANGELOG.md index d26a9f23559..e9a540b531b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/src/context-engine/context-engine.test.ts b/src/context-engine/context-engine.test.ts index 239ccd7b334..bd1f9d44322 100644 --- a/src/context-engine/context-engine.test.ts +++ b/src/context-engine/context-engine.test.ts @@ -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 () => { diff --git a/src/context-engine/registry.ts b/src/context-engine/registry.ts index 8d13fc0777e..4183b8a8a91 100644 --- a/src/context-engine/registry.ts +++ b/src/context-engine/registry.ts @@ -421,11 +421,13 @@ function describeResolvedContextEngineContractError( issues.push("missing info"); } else { const infoRecord = info as Record; + // 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");