Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions openclaw/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
isGenericAssistantMessage,
stripNoiseFromContent,
filterMessagesForExtraction,
isCliMetadataPass,
} from "./index.ts";

// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -610,3 +611,27 @@ describe("auto-recall threshold respects cfg.searchThreshold", () => {
expect(filtered).toHaveLength(6);
});
});

// ---------------------------------------------------------------------------
// isCliMetadataPass — guards the duplicate register() pass (issue #5371)
// ---------------------------------------------------------------------------
describe("isCliMetadataPass", () => {
it("returns true on the cli-metadata registration pass", () => {
expect(isCliMetadataPass("cli-metadata")).toBe(true);
});

it("returns false on the full registration pass", () => {
expect(isCliMetadataPass("full")).toBe(false);
});

it("returns false when registrationMode is undefined (older cores)", () => {
expect(isCliMetadataPass(undefined)).toBe(false);
});

it("returns false for unknown/other modes", () => {
expect(isCliMetadataPass("")).toBe(false);
expect(isCliMetadataPass(null)).toBe(false);
expect(isCliMetadataPass("Cli-Metadata")).toBe(false);
expect(isCliMetadataPass(0)).toBe(false);
});
});
40 changes: 40 additions & 0 deletions openclaw/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,25 @@ export { createProvider } from "./providers.ts";
// Helpers
// ============================================================================

/**
* The OpenClaw plugin loader invokes `register()` twice per gateway startup:
* once in the main pass (`registrationMode: "full"`) and once in a metadata
* pass (`registrationMode: "cli-metadata"`) used only to collect CLI command
* metadata for `openclaw mem0 help` and similar commands.
*
* On the metadata pass we must still register CLI commands, but everything
* else (provider/backend construction, memory capability, service, lifecycle
* hooks, telemetry, and the "registered" info log) should be skipped so those
* side effects only run once. Older cores that do not set `registrationMode`
* pass `undefined`, which we treat as a full registration for backwards
* compatibility.
*
* See https://github.com/mem0ai/mem0/issues/5371
*/
export function isCliMetadataPass(registrationMode: unknown): boolean {
return registrationMode === "cli-metadata";
}

// ============================================================================
// Plugin Definition
// ============================================================================
Expand All @@ -108,6 +127,27 @@ const memoryPlugin = definePluginEntry({
};
const cfg = mem0ConfigSchema.parse(api.pluginConfig, fileConfig);

// The core loader calls register() twice: a "full" pass and a
// "cli-metadata" pass. The metadata pass only needs the CLI surface so
// `openclaw mem0 help` can enumerate commands. Register CLI commands and
// return early to avoid constructing a second provider/backend, firing
// duplicate telemetry, double-registering the service/capability, and
// emitting a duplicate "registered" log line. See issue #5371.
if (isCliMetadataPass(api.registrationMode)) {
registerCliCommands(
api,
null as any,
null as any,
cfg,
() => cfg.userId,
(id: string) => `${cfg.userId}:agent:${id}`,
() => ({ user_id: cfg.userId, top_k: cfg.topK }),
() => undefined,
() => undefined,
);
return;
}

// Telemetry context bound to this plugin instance's config
const telemetryCtx = {
apiKey: cfg.apiKey,
Expand Down
9 changes: 9 additions & 0 deletions openclaw/openclaw-plugin-sdk.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,15 @@ declare module "openclaw/plugin-sdk" {

export interface OpenClawPluginApi {
pluginConfig: Record<string, unknown>;
/**
* Which registration pass the core plugin loader is running.
* The loader calls `register()` twice: once with "full" (the real
* registration) and once with "cli-metadata" (to collect CLI command
* metadata for `openclaw <plugin> help`). Plugins should skip heavy,
* non-CLI side effects on the "cli-metadata" pass. Older cores leave
* this undefined, which is treated as a full registration.
*/
registrationMode?: "full" | "cli-metadata" | string;
logger: {
info(msg: string): void;
warn(msg: string): void;
Expand Down