fix: auto-namespace messageMeta by pluginId in hook runner instead of plugin self-wrapping

This commit is contained in:
scotthuang 2026-03-15 17:16:52 +08:00
parent 0adf720f39
commit da46e86951
3 changed files with 40 additions and 19 deletions

View File

@ -565,14 +565,12 @@ const memoryPlugin = {
results.map((r) => ({ category: r.entry.category, text: r.entry.text })),
),
messageMeta: {
"memory-lancedb": {
displayStripPatterns: [
{
regex:
"<\\s*relevant[-_]memories\\b[^>]*>[\\s\\S]*?<\\s*/\\s*relevant[-_]memories\\s*>\\s*",
},
],
},
displayStripPatterns: [
{
regex:
"<\\s*relevant[-_]memories\\b[^>]*>[\\s\\S]*?<\\s*/\\s*relevant[-_]memories\\s*>\\s*",
},
],
},
};
} catch (err) {

View File

@ -121,11 +121,12 @@ function getHooksForName<K extends PluginHookName>(
}
/**
* Deep-merge two messageMeta bags. Top-level keys are plugin-namespaced
* (e.g. `"memory-lancedb"`) and each plugin's value is an object whose
* array-valued keys are concatenated so multiple hook phases can each
* contribute entries without overwriting each other. Scalar keys inside a
* plugin namespace use last-wins semantics.
* Deep-merge two messageMeta bags. Top-level keys are plugin IDs
* (auto-injected by the hook runner via {@link namespaceMessageMeta})
* and each plugin's value is an object whose array-valued keys are
* concatenated so multiple hook phases can each contribute entries
* without overwriting each other. Scalar keys inside a plugin
* namespace use last-wins semantics.
*/
function mergeMessageMeta(
acc: Record<string, unknown> | undefined,
@ -170,6 +171,24 @@ function mergeMessageMeta(
return merged;
}
/**
* Wrap a handler result's `messageMeta` inside a `{ [pluginId]: ... }` envelope.
* This is called by the hook runner **before** merging so that each plugin's
* meta is isolated under its own key a plugin returning `{ displayStripPatterns: [...] }`
* becomes `{ "memory-lancedb": { displayStripPatterns: [...] } }` automatically.
* Plugins cannot write into another plugin's namespace because the runner
* controls the wrapping.
*/
function namespaceMessageMeta(
result: { messageMeta?: Record<string, unknown> } | undefined | null,
pluginId: string,
): void {
if (!result?.messageMeta || Object.keys(result.messageMeta).length === 0) {
return;
}
result.messageMeta = { [pluginId]: result.messageMeta };
}
/**
* Create a hook runner for a specific registry.
*/
@ -300,6 +319,11 @@ export function createHookRunner(registry: PluginRegistry, options: HookRunnerOp
)(event, ctx);
if (handlerResult !== undefined && handlerResult !== null) {
// Auto-namespace messageMeta by pluginId so plugins cannot write
// into another plugin's namespace. The plugin returns flat meta
// (e.g. { displayStripPatterns: [...] }) and we wrap it as
// { [pluginId]: { displayStripPatterns: [...] } }.
namespaceMessageMeta(handlerResult, hook.pluginId);
if (mergeResults && result !== undefined) {
result = mergeResults(result, handlerResult);
} else {

View File

@ -544,12 +544,11 @@ export type PluginHookBeforePromptBuildResult = {
appendSystemContext?: string;
/**
* Generic key-value bag for plugins to attach metadata to the persisted
* user message. Top-level keys are namespaced by plugin ID (e.g.
* `"memory-lancedb"`) so each plugin's data is isolated from others.
*
* Example: memory-lancedb stores `displayStripPatterns` under its own
* namespace: `{ "memory-lancedb": { displayStripPatterns: [...] } }`.
* The UI reads only `messageMeta["memory-lancedb"]` to apply strip patterns.
* user message. Plugins return flat meta (e.g. `{ displayStripPatterns: [...] }`)
* and the hook runner automatically wraps it under the plugin's ID
* (e.g. `{ "memory-lancedb": { displayStripPatterns: [...] } }`).
* This ensures each plugin's data is isolated a plugin cannot write
* into another plugin's namespace.
*/
messageMeta?: Record<string, unknown>;
};