This commit is contained in:
ktpriyatham 2026-01-30 18:08:22 +08:00 committed by GitHub
commit 2b333f1181
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 52 additions and 9 deletions

1
.gitignore vendored
View File

@ -71,3 +71,4 @@ USER.md
# local tooling
.serena/
package-lock.json

View File

@ -555,7 +555,7 @@ const FIELD_HELP: Record<string, string> = {
"plugins.load.paths": "Additional plugin files or directories to load.",
"plugins.slots": "Select which plugins own exclusive slots (memory, etc.).",
"plugins.slots.memory":
'Select the active memory plugin by id, or "none" to disable memory plugins.',
'Select the active memory plugin(s): a single id, an array of ids for stackable plugins, or "none" to disable.',
"plugins.entries": "Per-plugin settings keyed by plugin id (enable/disable + config payloads).",
"plugins.entries.*.enabled": "Overrides plugin enable/disable for this entry (restart required).",
"plugins.entries.*.config": "Plugin-defined config payload (schema is provided by the plugin).",

View File

@ -4,8 +4,13 @@ export type PluginEntryConfig = {
};
export type PluginSlotsConfig = {
/** Select which plugin owns the memory slot ("none" disables memory plugins). */
memory?: string;
/**
* Select which plugin(s) own the memory slot.
* - Single string: "memory-lancedb" - one plugin active
* - Array: ["memory-core", "memory-lancedb"] - multiple plugins active (stackable)
* - "none": disables all memory plugins
*/
memory?: string | string[];
};
export type PluginsLoadConfig = {

View File

@ -199,11 +199,21 @@ export function validateConfigObjectWithPlugins(raw: unknown):
}
const memorySlot = normalizedPlugins.slots.memory;
// Validate memory slot - can be string or array of strings
if (typeof memorySlot === "string" && memorySlot.trim() && !knownIds.has(memorySlot)) {
issues.push({
path: "plugins.slots.memory",
message: `plugin not found: ${memorySlot}`,
});
} else if (Array.isArray(memorySlot)) {
for (const slotId of memorySlot) {
if (typeof slotId === "string" && slotId.trim() && !knownIds.has(slotId)) {
issues.push({
path: "plugins.slots.memory",
message: `plugin not found: ${slotId}`,
});
}
}
}
const allowedChannels = new Set<string>(["defaults", ...CHANNEL_IDS]);

View File

@ -495,7 +495,7 @@ export const OpenClawSchema = z
.optional(),
slots: z
.object({
memory: z.string().optional(),
memory: z.union([z.string(), z.array(z.string())]).optional(),
})
.strict()
.optional(),

View File

@ -8,7 +8,7 @@ export type NormalizedPluginsConfig = {
deny: string[];
loadPaths: string[];
slots: {
memory?: string | null;
memory?: string | string[] | null;
};
entries: Record<string, { enabled?: boolean; config?: unknown }>;
};
@ -20,7 +20,15 @@ const normalizeList = (value: unknown): string[] => {
return value.map((entry) => (typeof entry === "string" ? entry.trim() : "")).filter(Boolean);
};
const normalizeSlotValue = (value: unknown): string | null | undefined => {
const normalizeSlotValue = (value: unknown): string | string[] | null | undefined => {
// Handle array of plugin ids (stackable mode)
if (Array.isArray(value)) {
const normalized = value
.map((entry) => (typeof entry === "string" ? entry.trim() : ""))
.filter(Boolean);
return normalized.length > 0 ? normalized : undefined;
}
// Handle single string
if (typeof value !== "string") return undefined;
const trimmed = value.trim();
if (!trimmed) return undefined;
@ -144,7 +152,9 @@ export function resolveEnableState(
if (config.allow.length > 0 && !config.allow.includes(id)) {
return { enabled: false, reason: "not in allowlist" };
}
if (config.slots.memory === id) {
// Check if plugin is selected in memory slot (handles both string and array)
const memorySlot = config.slots.memory;
if (memorySlot === id || (Array.isArray(memorySlot) && memorySlot.includes(id))) {
return { enabled: true };
}
const entry = config.entries[id];
@ -166,13 +176,24 @@ export function resolveEnableState(
export function resolveMemorySlotDecision(params: {
id: string;
kind?: string;
slot: string | null | undefined;
slot: string | string[] | null | undefined;
selectedId: string | null;
}): { enabled: boolean; reason?: string; selected?: boolean } {
if (params.kind !== "memory") return { enabled: true };
if (params.slot === null) {
return { enabled: false, reason: "memory slot disabled" };
}
// Handle array of memory plugins (stackable mode)
if (Array.isArray(params.slot)) {
if (params.slot.includes(params.id)) {
return { enabled: true, selected: true };
}
return {
enabled: false,
reason: `memory slot set to [${params.slot.join(", ")}]`,
};
}
// Handle single string (exclusive mode - original behavior)
if (typeof params.slot === "string") {
if (params.slot === params.id) {
return { enabled: true, selected: true };

View File

@ -52,9 +52,15 @@ export function applyExclusiveSlotSelection(params: {
};
const inferredPrevSlot = prevSlot ?? defaultSlotIdForKey(slotKey);
// Format slot value for display (handles both string and array)
const formatSlotValue = (v: string | string[] | null | undefined): string => {
if (v === null || v === undefined) return "none";
if (Array.isArray(v)) return `[${v.join(", ")}]`;
return v;
};
if (inferredPrevSlot && inferredPrevSlot !== params.selectedId) {
warnings.push(
`Exclusive slot "${slotKey}" switched from "${inferredPrevSlot}" to "${params.selectedId}".`,
`Exclusive slot "${slotKey}" switched from "${formatSlotValue(inferredPrevSlot)}" to "${params.selectedId}".`,
);
}