diff --git a/index.ts b/index.ts index 25b2012f..28829715 100644 --- a/index.ts +++ b/index.ts @@ -1899,7 +1899,6 @@ const memoryLanceDBProPlugin = { api.logger.debug?.("memory-lancedb-pro: register() called again — skipping re-init (idempotent)"); return; } - _registeredApis.add(api); // Parse and validate configuration // ======================================================================== @@ -1908,8 +1907,22 @@ const memoryLanceDBProPlugin = { // the same singleton via destructuring. This prevents: // - Memory heap growth from repeated resource creation (~9 calls/process) // - Accumulated session Maps being lost on re-registration + // + // IMPORTANT: _registeredApis.add(api) is called AFTER successful init. + // This ensures that if _initPluginState throws, the api is NOT in the + // WeakSet, allowing a subsequent register() call with the same api to retry. + // (The old placement — before init — caused permanent breakage on init failure.) // ======================================================================== - if (!_singletonState) { _singletonState = _initPluginState(api); } + let singleton: typeof _singletonState; + try { + if (!_singletonState) { _singletonState = _initPluginState(api); } + singleton = _singletonState; + } catch (err) { + api.logger.error(`memory-lancedb-pro: _initPluginState failed — ${String(err)}`); + throw err; + } + _registeredApis.add(api); + const { config, resolvedDbPath, @@ -1930,7 +1943,7 @@ const memoryLanceDBProPlugin = { autoCaptureSeenTextCount, autoCapturePendingIngressTexts, autoCaptureRecentTexts, - } = _singletonState; + } = singleton; async function sleep(ms: number): Promise { diff --git a/src/reflection-store.ts b/src/reflection-store.ts index 38da5ce7..577a6b36 100644 Binary files a/src/reflection-store.ts and b/src/reflection-store.ts differ diff --git a/test/isOwnedByAgent.test.mjs b/test/isOwnedByAgent.test.mjs new file mode 100644 index 00000000..b23e4c4b --- /dev/null +++ b/test/isOwnedByAgent.test.mjs @@ -0,0 +1,65 @@ +// isOwnedByAgent unit tests — Issue #448 fix verification +import { describe, it } from "node:test"; +import assert from "node:assert"; + +// From reflection-store.ts: isOwnedByAgent function for isolated testing +function isOwnedByAgent(metadata, agentId) { + const owner = typeof metadata.agentId === "string" ? metadata.agentId.trim() : ""; + + const itemKind = metadata.itemKind; + + // derived: no main fallback, empty owner -> completely invisible + if (itemKind === "derived") { + if (!owner) return false; + return owner === agentId; + } + + // invariant / legacy / mapped: maintain original main fallback + if (!owner) return true; + return owner === agentId || owner === "main"; +} + +describe("isOwnedByAgent — derived ownership fix (Issue #448)", () => { + describe("itemKind === 'derived'", () => { + it("main's derived -> main visible", () => { + assert.strictEqual(isOwnedByAgent({ itemKind: "derived", agentId: "main" }, "main"), true); + }); + it("main's derived -> sub-agent invisible (core bug fix)", () => { + assert.strictEqual(isOwnedByAgent({ itemKind: "derived", agentId: "main" }, "sub-agent-A"), false); + }); + it("agent-x's derived -> agent-x visible", () => { + assert.strictEqual(isOwnedByAgent({ itemKind: "derived", agentId: "agent-x" }, "agent-x"), true); + }); + it("agent-x's derived -> agent-y invisible", () => { + assert.strictEqual(isOwnedByAgent({ itemKind: "derived", agentId: "agent-x" }, "agent-y"), false); + }); + it("derived + empty owner -> completely invisible (guard)", () => { + assert.strictEqual(isOwnedByAgent({ itemKind: "derived", agentId: "" }, "main"), false); + assert.strictEqual(isOwnedByAgent({ itemKind: "derived", agentId: "" }, "sub-agent"), false); + }); + }); + + describe("itemKind === 'invariant' (maintain fallback)", () => { + it("main's invariant -> sub-agent visible", () => { + assert.strictEqual(isOwnedByAgent({ itemKind: "invariant", agentId: "main" }, "sub-agent-A"), true); + }); + it("agent-x's invariant -> agent-x visible", () => { + assert.strictEqual(isOwnedByAgent({ itemKind: "invariant", agentId: "agent-x" }, "agent-x"), true); + }); + it("agent-x's invariant -> agent-y invisible", () => { + assert.strictEqual(isOwnedByAgent({ itemKind: "invariant", agentId: "agent-x" }, "agent-y"), false); + }); + }); + + describe("legacy / mapped (no itemKind, maintain fallback)", () => { + it("main legacy -> sub-agent visible", () => { + assert.strictEqual(isOwnedByAgent({ agentId: "main" }, "sub-agent-A"), true); + }); + it("agent-x legacy -> agent-x visible", () => { + assert.strictEqual(isOwnedByAgent({ agentId: "agent-x" }, "agent-x"), true); + }); + it("agent-x legacy -> agent-y invisible", () => { + assert.strictEqual(isOwnedByAgent({ agentId: "agent-x" }, "agent-y"), false); + }); + }); +}); \ No newline at end of file