diff --git a/index.ts b/index.ts index 4baf40f9..3f4f35cd 100644 --- a/index.ts +++ b/index.ts @@ -1615,10 +1615,14 @@ const pluginVersion = getPluginVersion(); // Plugin Definition // ============================================================================ -// WeakSet keyed by API instance — each distinct API object tracks its own initialized state. -// Using WeakSet instead of a module-level boolean avoids the "second register() call skips -// hook/tool registration for the new API instance" regression that rwmjhb identified. -const _registeredApis = new WeakSet(); +// Map keyed by API instance — each distinct API object tracks its own initialized state. +// Using Map instead of WeakSet allows tracking per-instance state AND enables clear() for reset. +const _registeredApis = new Map(); + +// Helper for tests to check registered APIs +export function _getRegisteredApisForTest(): Map { + return _registeredApis; +} const memoryLanceDBProPlugin = { id: "memory-lancedb-pro", @@ -1633,7 +1637,7 @@ const memoryLanceDBProPlugin = { api.logger.debug?.("memory-lancedb-pro: register() called again — skipping re-init (idempotent)"); return; } - _registeredApis.add(api); + // Note: Map.set(api, true) is called AFTER successful init // Parse and validate configuration const config = parsePluginConfig(api.pluginConfig); @@ -2113,6 +2117,9 @@ const memoryLanceDBProPlugin = { } ); + // Mark as successfully initialized - only called after all init completes + _registeredApis.set(api, true); + // Auto-compaction at gateway_start (if enabled, respects cooldown) if (config.memoryCompaction?.enabled) { api.on("gateway_start", () => { @@ -4054,10 +4061,8 @@ export function parsePluginConfig(value: unknown): PluginConfig { * @public */ export function resetRegistration() { - // Note: WeakSets cannot be cleared by design. In test scenarios where the - // same process reloads the module, a fresh module state means a new WeakSet. - // For hot-reload scenarios, the module is re-imported fresh. - // (WeakSet.clear() does not exist, so we do nothing here.) + // Clear the Map to allow re-registration after failure or hot-reload + _registeredApis.clear(); } export default memoryLanceDBProPlugin; diff --git a/src/reflection-store.ts b/src/reflection-store.ts index 38da5ce7..7dcc91c8 100644 --- a/src/reflection-store.ts +++ b/src/reflection-store.ts @@ -353,6 +353,9 @@ function buildDerivedCandidates( const lines = sanitizeInjectableReflectionLines(toStringArray(metadata.derived)); if (lines.length === 0) return []; + const owner = typeof metadata.agentId === "string" ? metadata.agentId.trim() : ""; + if (owner === "main") return []; + const defaults = { midpointDays: REFLECTION_DERIVE_LOGISTIC_MIDPOINT_DAYS, k: REFLECTION_DERIVE_LOGISTIC_K, @@ -428,6 +431,21 @@ function isReflectionMetadataType(type: unknown): boolean { function isOwnedByAgent(metadata: Record, agentId: string): boolean { const owner = typeof metadata.agentId === "string" ? metadata.agentId.trim() : ""; + + // itemKind 只存在於 memory-reflection-item 類型 + // legacy (memory-reflection) 和 mapped (memory-reflection-mapped) 都沒有 itemKind + // 因此 undefined !== "derived",會走原本的 main fallback(維持相容) + const itemKind = metadata.itemKind; + + // 如果是 derived 項目(memory-reflection-item):不做 main fallback, + // 且 derived 不允許空白 owner(空白 owner 的 derived 應完全不可見,防止洩漏) + // itemKind 必須是 string type,否則會錯誤進入 derived 分支(null/undefined/number 等應走 legacy fallback) + if (typeof itemKind === "string" && itemKind === "derived") { + if (!owner) return false; + return owner === agentId; + } + + // invariant / legacy / mapped:允許空白 owner 可見,維持原本的 main fallback if (!owner) return true; return owner === agentId || owner === "main"; } diff --git a/test/isOwnedByAgent.test.mjs b/test/isOwnedByAgent.test.mjs new file mode 100644 index 00000000..6e9a8e3b --- /dev/null +++ b/test/isOwnedByAgent.test.mjs @@ -0,0 +1,66 @@ +// isOwnedByAgent unit tests — Issue #448 fix verification +import { describe, it } from "node:test"; +import assert from "node:assert"; + +// 從 reflection-store.ts 直接拷貝 isOwnedByAgent 函數(隔離測試) +function isOwnedByAgent(metadata, agentId) { + const owner = typeof metadata.agentId === "string" ? metadata.agentId.trim() : ""; + + const itemKind = metadata.itemKind; + + // derived:不做 main fallback,空白 owner → 完全不可見 + if (itemKind === "derived") { + if (!owner) return false; + return owner === agentId; + } + + // invariant / legacy / mapped:維持原本的 main fallback + if (!owner) return true; + return owner === agentId || owner === "main"; +} + +describe("isOwnedByAgent — derived ownership fix (Issue #448)", () => { + // === Must Fix 3: 缺少 derived 分支測試 === + describe("itemKind === 'derived'", () => { + it("main's derived → main 可見", () => { + assert.strictEqual(isOwnedByAgent({ itemKind: "derived", agentId: "main" }, "main"), true); + }); + it("main's derived → sub-agent 不可見(核心 bug fix)", () => { + assert.strictEqual(isOwnedByAgent({ itemKind: "derived", agentId: "main" }, "sub-agent-A"), false); + }); + it("agent-x's derived → agent-x 可見", () => { + assert.strictEqual(isOwnedByAgent({ itemKind: "derived", agentId: "agent-x" }, "agent-x"), true); + }); + it("agent-x's derived → agent-y 不可見", () => { + assert.strictEqual(isOwnedByAgent({ itemKind: "derived", agentId: "agent-x" }, "agent-y"), false); + }); + it("derived + 空白 owner → 完全不可見(防呆)", () => { + assert.strictEqual(isOwnedByAgent({ itemKind: "derived", agentId: "" }, "main"), false); + assert.strictEqual(isOwnedByAgent({ itemKind: "derived", agentId: "" }, "sub-agent"), false); + }); + }); + + describe("itemKind === 'invariant'(維持 fallback)", () => { + it("main's invariant → sub-agent 可見", () => { + assert.strictEqual(isOwnedByAgent({ itemKind: "invariant", agentId: "main" }, "sub-agent-A"), true); + }); + it("agent-x's invariant → agent-x 可見", () => { + assert.strictEqual(isOwnedByAgent({ itemKind: "invariant", agentId: "agent-x" }, "agent-x"), true); + }); + it("agent-x's invariant → agent-y 不可見", () => { + assert.strictEqual(isOwnedByAgent({ itemKind: "invariant", agentId: "agent-x" }, "agent-y"), false); + }); + }); + + describe("legacy / mapped(無 itemKind,維持 fallback)", () => { + it("main legacy → sub-agent 可見", () => { + assert.strictEqual(isOwnedByAgent({ agentId: "main" }, "sub-agent-A"), true); + }); + it("agent-x legacy → agent-x 可見", () => { + assert.strictEqual(isOwnedByAgent({ agentId: "agent-x" }, "agent-x"), true); + }); + it("agent-x legacy → agent-y 不可見", () => { + assert.strictEqual(isOwnedByAgent({ agentId: "agent-x" }, "agent-y"), false); + }); + }); +});