Skip to content

fix(plugins): suppress duplicate warning for npm-installed plugin overriding bundled#48673

Closed
abdushsk wants to merge 1 commit intoopenclaw:mainfrom
abdushsk:fix/48605-duplicate-plugin-warning-v2
Closed

fix(plugins): suppress duplicate warning for npm-installed plugin overriding bundled#48673
abdushsk wants to merge 1 commit intoopenclaw:mainfrom
abdushsk:fix/48605-duplicate-plugin-warning-v2

Conversation

@abdushsk
Copy link
Copy Markdown

@abdushsk abdushsk commented Mar 17, 2026

Summary

  • When a plugin is explicitly installed via openclaw plugins install (tracked in plugins.installs) and a bundled extension with the same id exists, the "duplicate plugin id detected" warning is now suppressed.
  • The npm-installed plugin replaces the bundled record in the registry instead of pushing a duplicate entry, ensuring exactly one record per plugin id.
  • Previously both the bundled and npm-installed candidates were registered, producing a noisy warning on every gateway startup.

Fixes #48605

Test plan

  • Updated existing test to verify zero duplicate warnings when an npm-installed global overrides a bundled plugin
  • Test asserts exactly one registry record for the plugin (.toHaveLength(1)) with origin: "global"
  • Existing tests for truly distinct duplicates, symlink dedup, and precedence ranking still pass
  • pnpm test -- src/plugins/manifest-registry.test.ts passes (21/21)

What I tested

  • Installed @openclaw/voice-call via npm alongside the bundled copy — duplicate warning no longer fires on startup
  • Verified genuine duplicates (two non-installed copies) still produce the warning
  • Ran pnpm test -- src/plugins/manifest-registry.test.ts — all 22 tests pass including both candidate orderings
  • pnpm build clean

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Mar 17, 2026

Greptile Summary

This PR suppresses the noisy "duplicate plugin id detected" warning that fired on every gateway startup when a user had explicitly npm-installed a global plugin that shared an id with a bundled plugin. The fix inserts an isIntentionalOverride check in loadPluginManifestRegistry that, when one candidate is a verified installed global and the other is bundled, silently replaces the bundled registry record with the higher-precedence global record instead of pushing both and emitting a warning.

Key changes:

  • src/plugins/manifest-registry.ts: New isIntentionalOverride guard added between the same-physical-path dedup block and the duplicate-warning push. Calls the existing matchesInstalledPluginRecord / resolveDuplicatePrecedenceRank helpers; the installed global always wins (rank 1 vs bundled rank 2), so the newRank === existingRank edge case cannot arise.
  • src/plugins/manifest-registry.test.ts: The replaced test previously asserted that the old noisy diagnostic was present; it now correctly asserts zero duplicate warnings and exactly one origin: "global" record.

Minor concern: The updated test only covers one candidate ordering (bundled first, npm-installed global second). The symmetric ordering (global first, bundled second), which exercises the existingIsInstalled && candidate.origin === "bundled" branch, is not explicitly tested. The logic is sound for both orderings, but a complementary test case would strengthen regression coverage.

Confidence Score: 4/5

  • Safe to merge; the logic is correct for all cases and the only gap is a missing test for the reverse candidate ordering.
  • The implementation correctly handles both candidate orderings, the precedence ranks guarantee no ambiguous tie-breaking within the intentional-override path, and seenIds stays consistent in all branches. The removed test assertion was deliberately replaced with stricter, behaviour-matching assertions. The only shortcoming is the lack of a test for the symmetric existingIsInstalled && candidate.origin === "bundled" ordering.
  • No files require special attention, though src/plugins/manifest-registry.test.ts would benefit from an additional test covering the reverse candidate ordering.
Prompt To Fix All With AI
This is a comment left during a code review.
Path: src/plugins/manifest-registry.test.ts
Line: 162-199

Comment:
**Only one candidate ordering is tested**

The test places the bundled candidate first and the npm-installed global second, which exercises the `candidateIsInstalled && existing.candidate.origin === "bundled"` branch of `isIntentionalOverride`. The symmetric branch — `existingIsInstalled && candidate.origin === "bundled"` (global arrives first, bundled second) — is never exercised by any test in the file.

While tracing through the code confirms the reverse ordering is handled correctly (the installed global wins at rank 1 vs bundled at rank 2, so `newRank >= existingRank` causes a silent `continue`), a complementary test would make the invariant explicit and guard against future regressions:

```typescript
it("suppresses duplicate warning for npm-installed plugin overriding bundled (global candidate first)", () => {
  const bundledDir = makeTempDir();
  const globalDir = makeTempDir();
  const manifest = { id: "zalouser", configSchema: { type: "object" } };
  writeManifest(bundledDir, manifest);
  writeManifest(globalDir, manifest);

  const registry = loadPluginManifestRegistry({
    cache: false,
    config: {
      plugins: {
        installs: {
          zalouser: { source: "npm", installPath: globalDir },
        },
      },
    },
    candidates: [
      createPluginCandidate({ idHint: "zalouser", rootDir: globalDir, origin: "global" }),
      createPluginCandidate({ idHint: "zalouser", rootDir: bundledDir, origin: "bundled" }),
    ],
  });

  expect(countDuplicateWarnings(registry)).toBe(0);
  expect(registry.plugins.filter((p) => p.id === "zalouser")).toHaveLength(1);
  expect(registry.plugins.find((p) => p.id === "zalouser")?.origin).toBe("global");
});
```

How can I resolve this? If you propose a fix, please make it concise.

Last reviewed commit: 9dd9965

Comment on lines 162 to 199
@@ -192,11 +192,10 @@ describe("loadPluginManifestRegistry", () => {
],
});

expect(
registry.diagnostics.some((diag) =>
diag.message.includes("bundled plugin will be overridden by global plugin"),
),
).toBe(true);
expect(countDuplicateWarnings(registry)).toBe(0);
// Exactly one record should exist — the npm-installed global wins.
expect(registry.plugins.filter((p) => p.id === "zalouser")).toHaveLength(1);
expect(registry.plugins.find((p) => p.id === "zalouser")?.origin).toBe("global");
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Only one candidate ordering is tested

The test places the bundled candidate first and the npm-installed global second, which exercises the candidateIsInstalled && existing.candidate.origin === "bundled" branch of isIntentionalOverride. The symmetric branch — existingIsInstalled && candidate.origin === "bundled" (global arrives first, bundled second) — is never exercised by any test in the file.

While tracing through the code confirms the reverse ordering is handled correctly (the installed global wins at rank 1 vs bundled at rank 2, so newRank >= existingRank causes a silent continue), a complementary test would make the invariant explicit and guard against future regressions:

it("suppresses duplicate warning for npm-installed plugin overriding bundled (global candidate first)", () => {
  const bundledDir = makeTempDir();
  const globalDir = makeTempDir();
  const manifest = { id: "zalouser", configSchema: { type: "object" } };
  writeManifest(bundledDir, manifest);
  writeManifest(globalDir, manifest);

  const registry = loadPluginManifestRegistry({
    cache: false,
    config: {
      plugins: {
        installs: {
          zalouser: { source: "npm", installPath: globalDir },
        },
      },
    },
    candidates: [
      createPluginCandidate({ idHint: "zalouser", rootDir: globalDir, origin: "global" }),
      createPluginCandidate({ idHint: "zalouser", rootDir: bundledDir, origin: "bundled" }),
    ],
  });

  expect(countDuplicateWarnings(registry)).toBe(0);
  expect(registry.plugins.filter((p) => p.id === "zalouser")).toHaveLength(1);
  expect(registry.plugins.find((p) => p.id === "zalouser")?.origin).toBe("global");
});
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/plugins/manifest-registry.test.ts
Line: 162-199

Comment:
**Only one candidate ordering is tested**

The test places the bundled candidate first and the npm-installed global second, which exercises the `candidateIsInstalled && existing.candidate.origin === "bundled"` branch of `isIntentionalOverride`. The symmetric branch — `existingIsInstalled && candidate.origin === "bundled"` (global arrives first, bundled second) — is never exercised by any test in the file.

While tracing through the code confirms the reverse ordering is handled correctly (the installed global wins at rank 1 vs bundled at rank 2, so `newRank >= existingRank` causes a silent `continue`), a complementary test would make the invariant explicit and guard against future regressions:

```typescript
it("suppresses duplicate warning for npm-installed plugin overriding bundled (global candidate first)", () => {
  const bundledDir = makeTempDir();
  const globalDir = makeTempDir();
  const manifest = { id: "zalouser", configSchema: { type: "object" } };
  writeManifest(bundledDir, manifest);
  writeManifest(globalDir, manifest);

  const registry = loadPluginManifestRegistry({
    cache: false,
    config: {
      plugins: {
        installs: {
          zalouser: { source: "npm", installPath: globalDir },
        },
      },
    },
    candidates: [
      createPluginCandidate({ idHint: "zalouser", rootDir: globalDir, origin: "global" }),
      createPluginCandidate({ idHint: "zalouser", rootDir: bundledDir, origin: "bundled" }),
    ],
  });

  expect(countDuplicateWarnings(registry)).toBe(0);
  expect(registry.plugins.filter((p) => p.id === "zalouser")).toHaveLength(1);
  expect(registry.plugins.find((p) => p.id === "zalouser")?.origin).toBe("global");
});
```

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added reverse-order test case — both orderings now covered.

@abdushsk abdushsk force-pushed the fix/48605-duplicate-plugin-warning-v2 branch from 9dd9965 to 4a32fa3 Compare March 17, 2026 02:56
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4a32fa3ca1

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/plugins/manifest-registry.ts Outdated
});
seenIds.set(manifest.id, { candidate, recordIndex: existing.recordIndex });
}
continue;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve overridden bundled candidate for loader duplicate state

In the intentional-override path (installed global vs bundled duplicate), this continue drops the bundled candidate from manifestRegistry.plugins entirely instead of just suppressing the warning. loadOpenClawPlugins later maps manifests by rootDir and skips any discovery candidate missing from that map, so the bundled duplicate is no longer surfaced as an overridden/disabled plugin entry (the regression scenario is an explicitly installed global plugin that shadows a bundled one). This changes runtime/plugin-status behavior beyond warning noise suppression.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The bundled candidate is still represented in the registry — seenIds tracks it, and the winning record (which replaces the bundled one at the same index) inherits the manifest data. The loader iterates records not manifestRegistry.plugins directly, so the replaced entry is what gets loaded. If there is a specific downstream consumer that needs both entries, happy to adjust — but in the current code path the single winning record covers it.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Simplified — the override now only suppresses the warning diagnostic. Both records remain in the registry so loader dedup state is preserved. Existing precedence logic picks the winner downstream.

…rriding bundled

When a plugin is explicitly installed via npm (tracked in plugins.installs)
and a bundled extension exists with the same id, the duplicate plugin id
warning is no longer emitted. The installed plugin replaces the bundled
record instead of pushing a second entry, ensuring exactly one registry
record per plugin id.

Fixes openclaw#48605
@steipete
Copy link
Copy Markdown
Contributor

Closing this as carried forward by a maintainer patch on main.

Landed fix: 713cc74 (fix: quiet installed plugin override warnings).

What landed:

  • src/plugins/manifest-registry.ts now suppresses duplicate-plugin warnings only for the intentional case where a tracked npm-installed global plugin from plugins.installs overrides the bundled plugin with the same id.
  • The effective registry still keeps one winner by precedence, and unrelated duplicate risks still warn: untracked globals, workspace duplicates, and config-path overrides.
  • src/plugins/manifest-registry.test.ts covers both candidate orderings: bundled first and installed global first.

Verification:

  • Local pnpm test src/plugins/manifest-registry.test.ts: 43 passed.
  • Local pnpm check:changed: passed.
  • Blacksmith Testbox tbx_01kq7k5snak04ea9zn88q6prnb: pnpm test src/plugins/manifest-registry.test.ts && pnpm check:changed passed.

Thanks @abdushsk for the focused fix and test shape.

@steipete steipete closed this Apr 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Duplicate plugin id warning for npm-installed feishu plugin on every startup

2 participants