Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 14 additions & 9 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<OpenClawPluginApi>();
// 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<OpenClawPluginApi, boolean>();

// Helper for tests to check registered APIs
export function _getRegisteredApisForTest(): Map<OpenClawPluginApi, boolean> {
return _registeredApis;
}

const memoryLanceDBProPlugin = {
id: "memory-lancedb-pro",
Expand All @@ -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);
Expand Down Expand Up @@ -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", () => {
Expand Down Expand Up @@ -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;
18 changes: 18 additions & 0 deletions src/reflection-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -428,6 +431,21 @@ function isReflectionMetadataType(type: unknown): boolean {

function isOwnedByAgent(metadata: Record<string, unknown>, 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";
}
Expand Down
66 changes: 66 additions & 0 deletions test/isOwnedByAgent.test.mjs
Original file line number Diff line number Diff line change
@@ -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);
});
});
});
Loading