feat(plugins): support multi-kind plugins for dual slot ownership#57507
Conversation
Greptile SummaryThis PR adds multi-kind plugin support so a single plugin can declare Two minor points remain:
Confidence Score: 5/5Safe to merge — all prior P1 concerns are resolved and only minor P2 style issues remain. All previous review thread concerns (order-sensitive JSON.stringify comparison, hardcoded slot reverse-mapping, inline hasKind duplication) are confirmed fixed. The core multi-kind slot logic is correct: state accumulation in applyExclusiveSlotSelection is sound, competitor disabling works correctly for both single- and dual-kind plugins, and all equality checks across the codebase consistently use hasKind. The two remaining findings are P2 style/edge-case concerns that do not affect correctness. src/plugins/slots.ts (slotKeyForPluginKind singular API), src/plugins/manifest.ts (empty-array edge case in parsePluginKind)
|
| Filename | Overview |
|---|---|
| src/plugins/slots.ts | Core multi-kind slot logic. Helpers are correctly implemented. Minor concern: slotKeyForPluginKind (singular, exported) accepts multi-kind arrays but silently returns only the first kind's slot key. |
| src/plugins/manifest.ts | New parsePluginKind helper correctly handles string and array forms. Minor edge case: kind: [] passes through as [] rather than undefined. |
| src/plugins/loader.ts | All kind === "memory" comparisons replaced with hasKind. Kind mismatch diagnostic uses normalizeKinds().sort() for order-insensitive comparison in both load paths. |
| src/plugins/registry.ts | All four memory-gated registration guards updated to use hasKind. Type widened correctly. |
| src/plugins/config-state.ts | resolveMemorySlotDecision now accepts string |
| src/plugins/slots.test.ts | New tests cover normalizeKinds, hasKind, slotKeysForPluginKind, and the multi-kind dual-slot selection scenario. |
| src/plugins/types.ts | OpenClawPluginDefinition.kind widened to PluginKind |
| src/plugins/manifest-registry.ts | PluginManifestRecord.kind type widened to PluginKind |
| src/agents/skills/plugin-skills.ts | record.kind === "memory" replaced with hasKind(record.kind, "memory"). Correct one-line update. |
| src/hooks/plugin-hooks.ts | record.kind === "memory" replaced with hasKind(record.kind, "memory"). Correct one-line update. |
| src/config/validation.ts | record.kind === "memory" replaced with hasKind(record.kind, "memory"). Correct one-line update. |
Prompt To Fix All With AI
This is a comment left during a code review.
Path: src/plugins/slots.ts
Line: 41-47
Comment:
**`slotKeyForPluginKind` silently truncates multi-kind arrays**
The function signature was widened to accept `PluginKind | PluginKind[]`, but the implementation only ever returns the slot key for the *first* element of an array. A future caller passing `["memory", "context-engine"]` would silently receive only `"memory"` with no indication that the second kind was ignored.
`slotKeysForPluginKind` (plural) is the correct API for multi-kind plugins and is the function used everywhere else in this PR. Since `slotKeyForPluginKind` is exported (public API) and currently has no callers in the codebase, consider either:
1. Narrowing its signature back to `PluginKind` only (makes the single-result contract explicit), or
2. Adding a JSDoc comment that makes the "first kind wins" behaviour clear.
```suggestion
/**
* Returns the slot key for a single-kind plugin.
* For multi-kind plugins use `slotKeysForPluginKind` instead.
*/
export function slotKeyForPluginKind(kind?: PluginKind): PluginSlotKey | null {
if (!kind) {
return null;
}
return SLOT_BY_KIND[kind] ?? null;
}
```
How can I resolve this? If you propose a fix, please make it concise.
---
This is a comment left during a code review.
Path: src/plugins/manifest.ts
Line: 236-244
Comment:
**Empty array `kind: []` silently passes through as a valid (empty) kind**
`[].every(...)` is vacuously `true`, so a manifest with `kind: []` returns `[]` rather than `undefined`. Downstream code handles the empty array correctly (e.g. `normalizeKinds([])` → `[]`, `hasKind([], ...)` → `false`), but a plugin author who accidentally writes `kind: []` gets no validation feedback and the plugin is treated as if it had no kind at all.
Consider rejecting empty arrays explicitly:
```suggestion
function parsePluginKind(raw: unknown): PluginKind | PluginKind[] | undefined {
if (typeof raw === "string") {
return raw as PluginKind;
}
if (Array.isArray(raw) && raw.length > 0 && raw.every((k) => typeof k === "string")) {
return raw.length === 1 ? (raw[0] as PluginKind) : (raw as PluginKind[]);
}
return undefined;
}
```
How can I resolve this? If you propose a fix, please make it concise.Reviews (2): Last reviewed commit: "fix: address review feedback on multi-ki..." | Re-trigger Greptile
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 377a6794fa
ℹ️ 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".
377a679 to
6de0e43
Compare
|
@greptileai another review please |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a4bd2fee6a
ℹ️ 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".
|
This is the exact thing that my plugin needs to function correctly |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 0992aae02c
ℹ️ 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".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 60ff18cfaf
ℹ️ 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".
|
nice approach — the helpers clean up the scattered the kind mismatch comparison in loader.ts (
overall looks solid, the dual-kind "stays enabled when it loses one slot" behavior makes sense and the tests cover it well |
|
@miyago9267 Thanks for the review! Addressed both points in 9d8293f:
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ed8bbaac31
ℹ️ 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".
- Use sorted normalizeKinds() for kind-mismatch comparison in loader.ts (fixes order-sensitive JSON.stringify for arrays) - Derive slot-to-kind reverse mapping from SLOT_BY_KIND in slots.ts (removes hardcoded ternary that would break for future slot types) - Use shared hasKind() helper in config-state.ts instead of inline logic
When a new plugin takes over one slot, a dual-kind plugin that still owns the other slot must not be disabled — otherwise context engine resolution fails at runtime.
A plugin with kind: ["memory", "context-engine"] must stay enabled even when it loses the memory slot, so its context engine role can still load.
- Pass manifest kind (not hardcoded "memory") in early memory gating - Extract kindsEqual() helper for DRY kind comparison in loader.ts - Narrow slotKeyForPluginKind back to single PluginKind with JSDoc - Reject empty array in parsePluginKind - Add kindsEqual tests
…al-kind memory registration
e2e5c9f to
4718725
Compare
obviyus
left a comment
There was a problem hiding this comment.
Reviewed latest changes; landing now.
|
Landed on main. Thanks @fuller-stack-dev. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 4718725303
ℹ️ 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".
| } | ||
| // A dual-kind plugin (e.g. ["memory", "context-engine"]) that lost the | ||
| // memory slot must stay enabled so its other slot role can still load. | ||
| const isMultiKind = Array.isArray(params.kind) && params.kind.length > 1; |
There was a problem hiding this comment.
Determine dual-kind status from unique mapped kinds
resolveMemorySlotDecision uses Array.isArray(params.kind) && params.kind.length > 1 to decide whether a plugin should be exempt from memory-slot disablement, but manifest parsing accepts any non-empty string array. That means inputs like kind: ["memory", "memory"] (or "memory" plus a typo) are treated as dual-kind and remain enabled when plugins.slots.memory is set to another plugin or none, so the plugin still runs register() side effects despite not owning a valid second slot. The dual-kind check should use distinct, recognized slot-mapped kinds rather than raw array length.
Useful? React with 👍 / 👎.
…) (thanks @fuller-stack-dev) * feat(plugins): support multi-kind plugins for dual slot ownership * fix: address review feedback on multi-kind plugin support - Use sorted normalizeKinds() for kind-mismatch comparison in loader.ts (fixes order-sensitive JSON.stringify for arrays) - Derive slot-to-kind reverse mapping from SLOT_BY_KIND in slots.ts (removes hardcoded ternary that would break for future slot types) - Use shared hasKind() helper in config-state.ts instead of inline logic * fix: don't disable dual-kind plugin that still owns another slot When a new plugin takes over one slot, a dual-kind plugin that still owns the other slot must not be disabled — otherwise context engine resolution fails at runtime. * fix: exempt dual-kind plugins from memory slot disablement A plugin with kind: ["memory", "context-engine"] must stay enabled even when it loses the memory slot, so its context engine role can still load. * fix: address remaining review feedback - Pass manifest kind (not hardcoded "memory") in early memory gating - Extract kindsEqual() helper for DRY kind comparison in loader.ts - Narrow slotKeyForPluginKind back to single PluginKind with JSDoc - Reject empty array in parsePluginKind - Add kindsEqual tests * fix: use toSorted() instead of sort() per lint rules * plugins: include default slot ownership in disable checks and gate dual-kind memory registration
…) (thanks @fuller-stack-dev) * feat(plugins): support multi-kind plugins for dual slot ownership * fix: address review feedback on multi-kind plugin support - Use sorted normalizeKinds() for kind-mismatch comparison in loader.ts (fixes order-sensitive JSON.stringify for arrays) - Derive slot-to-kind reverse mapping from SLOT_BY_KIND in slots.ts (removes hardcoded ternary that would break for future slot types) - Use shared hasKind() helper in config-state.ts instead of inline logic * fix: don't disable dual-kind plugin that still owns another slot When a new plugin takes over one slot, a dual-kind plugin that still owns the other slot must not be disabled — otherwise context engine resolution fails at runtime. * fix: exempt dual-kind plugins from memory slot disablement A plugin with kind: ["memory", "context-engine"] must stay enabled even when it loses the memory slot, so its context engine role can still load. * fix: address remaining review feedback - Pass manifest kind (not hardcoded "memory") in early memory gating - Extract kindsEqual() helper for DRY kind comparison in loader.ts - Narrow slotKeyForPluginKind back to single PluginKind with JSDoc - Reject empty array in parsePluginKind - Add kindsEqual tests * fix: use toSorted() instead of sort() per lint rules * plugins: include default slot ownership in disable checks and gate dual-kind memory registration
…) (thanks @fuller-stack-dev) * feat(plugins): support multi-kind plugins for dual slot ownership * fix: address review feedback on multi-kind plugin support - Use sorted normalizeKinds() for kind-mismatch comparison in loader.ts (fixes order-sensitive JSON.stringify for arrays) - Derive slot-to-kind reverse mapping from SLOT_BY_KIND in slots.ts (removes hardcoded ternary that would break for future slot types) - Use shared hasKind() helper in config-state.ts instead of inline logic * fix: don't disable dual-kind plugin that still owns another slot When a new plugin takes over one slot, a dual-kind plugin that still owns the other slot must not be disabled — otherwise context engine resolution fails at runtime. * fix: exempt dual-kind plugins from memory slot disablement A plugin with kind: ["memory", "context-engine"] must stay enabled even when it loses the memory slot, so its context engine role can still load. * fix: address remaining review feedback - Pass manifest kind (not hardcoded "memory") in early memory gating - Extract kindsEqual() helper for DRY kind comparison in loader.ts - Narrow slotKeyForPluginKind back to single PluginKind with JSDoc - Reject empty array in parsePluginKind - Add kindsEqual tests * fix: use toSorted() instead of sort() per lint rules * plugins: include default slot ownership in disable checks and gate dual-kind memory registration
…) (thanks @fuller-stack-dev) * feat(plugins): support multi-kind plugins for dual slot ownership * fix: address review feedback on multi-kind plugin support - Use sorted normalizeKinds() for kind-mismatch comparison in loader.ts (fixes order-sensitive JSON.stringify for arrays) - Derive slot-to-kind reverse mapping from SLOT_BY_KIND in slots.ts (removes hardcoded ternary that would break for future slot types) - Use shared hasKind() helper in config-state.ts instead of inline logic * fix: don't disable dual-kind plugin that still owns another slot When a new plugin takes over one slot, a dual-kind plugin that still owns the other slot must not be disabled — otherwise context engine resolution fails at runtime. * fix: exempt dual-kind plugins from memory slot disablement A plugin with kind: ["memory", "context-engine"] must stay enabled even when it loses the memory slot, so its context engine role can still load. * fix: address remaining review feedback - Pass manifest kind (not hardcoded "memory") in early memory gating - Extract kindsEqual() helper for DRY kind comparison in loader.ts - Narrow slotKeyForPluginKind back to single PluginKind with JSDoc - Reject empty array in parsePluginKind - Add kindsEqual tests * fix: use toSorted() instead of sort() per lint rules * plugins: include default slot ownership in disable checks and gate dual-kind memory registration
…) (thanks @fuller-stack-dev) * feat(plugins): support multi-kind plugins for dual slot ownership * fix: address review feedback on multi-kind plugin support - Use sorted normalizeKinds() for kind-mismatch comparison in loader.ts (fixes order-sensitive JSON.stringify for arrays) - Derive slot-to-kind reverse mapping from SLOT_BY_KIND in slots.ts (removes hardcoded ternary that would break for future slot types) - Use shared hasKind() helper in config-state.ts instead of inline logic * fix: don't disable dual-kind plugin that still owns another slot When a new plugin takes over one slot, a dual-kind plugin that still owns the other slot must not be disabled — otherwise context engine resolution fails at runtime. * fix: exempt dual-kind plugins from memory slot disablement A plugin with kind: ["memory", "context-engine"] must stay enabled even when it loses the memory slot, so its context engine role can still load. * fix: address remaining review feedback - Pass manifest kind (not hardcoded "memory") in early memory gating - Extract kindsEqual() helper for DRY kind comparison in loader.ts - Narrow slotKeyForPluginKind back to single PluginKind with JSDoc - Reject empty array in parsePluginKind - Add kindsEqual tests * fix: use toSorted() instead of sort() per lint rules * plugins: include default slot ownership in disable checks and gate dual-kind memory registration
…) (thanks @fuller-stack-dev) * feat(plugins): support multi-kind plugins for dual slot ownership * fix: address review feedback on multi-kind plugin support - Use sorted normalizeKinds() for kind-mismatch comparison in loader.ts (fixes order-sensitive JSON.stringify for arrays) - Derive slot-to-kind reverse mapping from SLOT_BY_KIND in slots.ts (removes hardcoded ternary that would break for future slot types) - Use shared hasKind() helper in config-state.ts instead of inline logic * fix: don't disable dual-kind plugin that still owns another slot When a new plugin takes over one slot, a dual-kind plugin that still owns the other slot must not be disabled — otherwise context engine resolution fails at runtime. * fix: exempt dual-kind plugins from memory slot disablement A plugin with kind: ["memory", "context-engine"] must stay enabled even when it loses the memory slot, so its context engine role can still load. * fix: address remaining review feedback - Pass manifest kind (not hardcoded "memory") in early memory gating - Extract kindsEqual() helper for DRY kind comparison in loader.ts - Narrow slotKeyForPluginKind back to single PluginKind with JSDoc - Reject empty array in parsePluginKind - Add kindsEqual tests * fix: use toSorted() instead of sort() per lint rules * plugins: include default slot ownership in disable checks and gate dual-kind memory registration
…) (thanks @fuller-stack-dev) * feat(plugins): support multi-kind plugins for dual slot ownership * fix: address review feedback on multi-kind plugin support - Use sorted normalizeKinds() for kind-mismatch comparison in loader.ts (fixes order-sensitive JSON.stringify for arrays) - Derive slot-to-kind reverse mapping from SLOT_BY_KIND in slots.ts (removes hardcoded ternary that would break for future slot types) - Use shared hasKind() helper in config-state.ts instead of inline logic * fix: don't disable dual-kind plugin that still owns another slot When a new plugin takes over one slot, a dual-kind plugin that still owns the other slot must not be disabled — otherwise context engine resolution fails at runtime. * fix: exempt dual-kind plugins from memory slot disablement A plugin with kind: ["memory", "context-engine"] must stay enabled even when it loses the memory slot, so its context engine role can still load. * fix: address remaining review feedback - Pass manifest kind (not hardcoded "memory") in early memory gating - Extract kindsEqual() helper for DRY kind comparison in loader.ts - Narrow slotKeyForPluginKind back to single PluginKind with JSDoc - Reject empty array in parsePluginKind - Add kindsEqual tests * fix: use toSorted() instead of sort() per lint rules * plugins: include default slot ownership in disable checks and gate dual-kind memory registration
Summary
kindas an array (e.g.["memory", "context-engine"]) so a single plugin can occupy multiple exclusive slots simultaneously.normalizeKinds(),hasKind(), andslotKeysForPluginKind()helpers inslots.tsfor uniform multi-kind iteration and comparison.record.kind === "memory"comparisons to use the new helpers.Motivation
A plugin that acts as both a memory provider and a context engine currently can only declare one
kindand occupy one slot. This change lets such a plugin declarekind: ["memory", "context-engine"]to occupy both slots, disabling competing plugins in each slot as expected.Changes
src/plugins/slots.ts-- AddednormalizeKinds,hasKind,slotKeysForPluginKind. UpdatedslotKeyForPluginKindandapplyExclusiveSlotSelectionto acceptPluginKind | PluginKind[]and iterate over all slot keys.src/plugins/types.ts--OpenClawPluginDefinition.kindnow acceptsPluginKind | PluginKind[].src/plugins/manifest.ts-- AddedparsePluginKind()to handle both string and arraykindin manifests.src/plugins/registry.ts--PluginRecord.kindacceptsPluginKind | PluginKind[]; memory-gated registrations usehasKind().src/plugins/manifest-registry.ts-- UpdatedPluginManifestRecord.kindtype.src/plugins/loader.ts-- Allrecord.kind === "memory"checks replaced withhasKind(record.kind, "memory"). Kind mismatch comparison updated for array support.src/plugins/config-state.ts--resolveMemorySlotDecisionacceptsstring | string[]kind param.src/hooks/plugin-hooks.ts,src/agents/skills/plugin-skills.ts,src/config/validation.ts-- Updated memory kind checks to usehasKind().Test plan
normalizeKinds,hasKind,slotKeysForPluginKindinslots.test.tsslots.test.tstests pass (15 total)loader.test.tstests pass (73 total)config-state.test.tstests pass (25 total)pnpm lintpasses with 0 errors