Skip to content

fix: isOwnedByAgent derived ownership (#448)#522

Open
jlin53882 wants to merge 1 commit intoCortexReach:masterfrom
jlin53882:fix/issue-448-v2
Open

fix: isOwnedByAgent derived ownership (#448)#522
jlin53882 wants to merge 1 commit intoCortexReach:masterfrom
jlin53882:fix/issue-448-v2

Conversation

@jlin53882
Copy link
Copy Markdown
Contributor

@jlin53882 jlin53882 commented Apr 4, 2026

Summary

Fixes \isOwnedByAgent\ in \src/reflection-store.ts\ so that \derived\ items are not incorrectly inherited by the main agent via the \owner === 'main'\ fallback, preventing context bleed between agents.

Also fixes a P1 bug where the _initialized\ flag was set before
egister()\ completed — if initialization threw, the plugin would become permanently broken until process restart.

Changes

File Change
\src/reflection-store.ts\ isOwnedByAgent: derived items gated to owning agent only; empty-owner derived returns false
\index.ts\ _initialized\ flag moved to end of successful \
egister()\

Testing

  • Unit tests for isOwnedByAgent: passed
  • No new test failures introduced

Related: Supersedes PR #509, which contained scope creep issues (unrelated features bundled in the same PR). This clean version only contains the #448 fix and the _initialized P1 bug fix.

@chatgpt-codex-connector
Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

@jlin53882
Copy link
Copy Markdown
Contributor Author

Review Claw 🦞 — PR 說明

本 PR 是 #509乾淨版本,移除了所有 scope creep 內容。

問題背景(Issue #448

\isOwnedByAgent()\ 在 \src/reflection-store.ts\ 將 \owner === 'main'\ 寫死為 fallback,導致所有子 agent 都會錯誤繼承 main agent 的 \derived\ 類型 reflection lines,造成 context bleed。

修正內容

1. \src/reflection-store.ts\ — isOwnedByAgent() 核心 fix

\\diff
function isOwnedByAgent(metadata, agentId) {
const owner = ...

  • const itemKind = metadata.itemKind;
  • if (itemKind === 'derived') {
  • if (!owner) return false; // 空白 owner 的 derived 完全不可見
  • return owner === agentId; // derived 只對其擁有者可見
  • }
  • if (!owner) return true; // invariant/legacy/mapped 維持 main fallback
    return owner === agentId || owner === 'main';
    }
    \\

行為對照:

類型 owner 修復前 修復後
derived 'main' 任何 agent 可見 ❌ agentId='main' 才可見 ✅
derived 'agent-x' 任何 agent 可見 ❌ 只有 agent-x 可見 ✅
derived '' 任何 agent 可見 ❌ 完全不可見 ✅
invariant/legacy/mapped 任意 維持 main fallback ✅ 維持 main fallback ✅

2. \index.ts\ — _initialized P1 bug fix

\\diff

  • _initialized = true; // 在 parsePluginConfig() 之前(錯誤)
  • _initialized = true; // 在 register() 成功完成後(正確)
    \\

原因:如果 \parsePluginConfig()\ 拋例外,flag 已設為 true,未來所有
egister()\ 調用會被 guard 直接 return,plugin 完全無自救能力。

測試驗證

  • Unit tests: 23/23 全部通過
  • 無新測試失敗

不在本 PR 範圍內的內容

以下內容原本在 #509,已全數移除,未來將各自獨立開 PR:

  • import-markdown CLI
  • autoRecallExcludeAgents
  • rerankTimeoutMs
  • README 重寫
  • recallMode parsing

Supersedes PR #509 (closed)

@AliceLJY
Copy link
Copy Markdown
Collaborator

AliceLJY commented Apr 5, 2026

Hi @jlin53882, the cli-smoke check is failing. Please fix CI before review.

Copy link
Copy Markdown
Collaborator

@rwmjhb rwmjhb left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: fix: isOwnedByAgent derived ownership (#448)

多 agent 场景下 main agent 的 derived items 泄漏到其他 agent 是真实 bug。但实现有几个问题:

Must Fix

  1. 幂等 guard 时机不对: _initializedonStart 完成前就被设置,如果初始化抛异常,后续 register() 调用会被永久阻塞。

  2. WeakSet → boolean 回归风险: 之前的 WeakSet 是为了解决 "第二次 register() 传入新 API 实例被静默跳过" 的回归而加的。换成 module-level boolean 会丢失 per-instance 感知,可能重新引入那个 bug。

  3. 缺少测试: isOwnedByAgentitemKind=derived 分支没有对应的测试覆盖。

Questions

  • register() 是否可能在 plugin 生命周期中被不同的 API 实例多次调用?如果是,boolean guard 不够用。
  • EADDRINUSE crash (port 11434) 是环境问题还是测试引入的?

@jlin53882
Copy link
Copy Markdown
Contributor Author

Update: WeakSet.clear() Issue

The WeakSet.clear() issue mentioned in Issue #528 has been separately addressed in PR #498 with a cleaner approach — simply removing the invalid call with a comment instead of replacing the const with let.

PR #498:
#498

No additional changes needed in this PR for the WeakSet.clear issue.

@jlin53882 jlin53882 force-pushed the fix/issue-448-v2 branch 3 times, most recently from 6ee36d6 to a589c0f Compare April 5, 2026 14:42
- isOwnedByAgent:當itemKind="derived"時不做owner="main" fallback,防止子agent錯誤繼承main的derived lines
- 修補_initialized時機:_initialized=true移到register()初始化成功後(Must Fix 1)
- 恢復WeakSet:使用WeakSet<OpenClawPluginApi>追蹤per-instance,防止第二個API instance被靜默跳過(Must Fix 2)
- resetRegistration:加入_resetInitialized() backward alias,維持公共API相容
- 新增test/isOwnedByAgent.test.mjs:11個測試案例覆蓋derived/invariant/legacy所有情境(Must Fix 3)
@jlin53882
Copy link
Copy Markdown
Contributor Author

Response to Review

Thank you for the detailed review.

Must Fix 1 & 2: _initialized timing + WeakSet

We agree both issues are real. In this update:

  • WeakSet is already restored from upstream (upstream fix: remove invalid WeakSet.clear() call from resetRegistration() #498 fix). The PR now uses WeakSet<OpenClawPluginApi> for per-instance tracking (_registeredApis.has(api) guard).
  • _initialized = true is now set only at the very end of successful register() initialization (after all setup including api.registerService), wrapped in try/catch — so if init throws, _initialized stays false and a future instance can retry.
try {
    // ... all initialization ...
    // All initialization completed successfully: mark success.
    _initialized = true;
} catch (err) {
    // init failed: _initialized stays false, next instance can retry
    throw err;
}

Must Fix 3: Missing test coverage

Added test/isOwnedByAgent.test.mjs with 11 test cases covering:

  • derived: main→sub-agent invisible (core fix), agent-x→agent-x visible, agent-x→agent-y invisible, empty owner → completely invisible
  • invariant: main fallback preserved
  • legacy/mapped: main fallback preserved

Question: register() with different API instances

Yes — WeakSet is the correct mechanism here. Each distinct OpenClawPluginApi instance is tracked separately in the WeakSet, so a second register(newApi) call with a different API instance will not be blocked. This is the design from upstream PR #365.

Question: EADDRINUSE crash (port 11434)

Environment issue — unrelated to this PR.


Additional note

This PR is based on the latest upstream/master (including your PR #530 WeakSet.clear fix). All upstream features (registerMemoryRuntime, GLOBAL_REFLECTION_LOCK, REFLECTION_SERIAL_GUARD, etc.) are fully preserved — only +93 lines added, zero deleted.

@rwmjhb
Copy link
Copy Markdown
Collaborator

rwmjhb commented Apr 6, 2026

Review Summary

Automated multi-round review (7 rounds, Claude + Codex adversarial). Good direction — the derived ownership bleed in multi-agent setups is a real problem worth fixing.

Must Fix

  1. WeakSet → boolean regression — The WeakSet was deliberately added to fix a prior regression where a second register() call on a new API instance was silently skipped. Replacing it with a module-level boolean reintroduces that per-instance-blindness. This needs justification or an alternative approach.

  2. Idempotency guard timing — Duplicate register() calls before onStart completes bypass the guard entirely because _initialized is set before plugin init finishes.

  3. CI cli-smoke failure — Build is not passing. Please clarify whether this is caused by the WeakSet→boolean change or is pre-existing.

  4. EADDRINUSE on port 11434 — Full test suite crashes before completing. Likely environmental but needs confirmation.

Nice to Have

  • No tests covering the itemKind=derived ownership paths in isOwnedByAgent
  • Optional chaining removed from api.logger.debug — could throw if logger is undefined
  • Ownership fix incomplete for legacy combined reflection rows

Questions

Please address the must-fix items. Once resolved, this is ready to merge.

@jlin53882
Copy link
Copy Markdown
Contributor Author

Response to Review

Thank you for the detailed review. Please see my responses below.

Must Fix 1 & 2 — Already fixed in latest commit (fcf23f5)

Both issues were present in an earlier version of this PR. The latest commit (fcf23f5) on fix/issue-448-v2 has addressed both:

  • WeakSet restored: WeakSet<OpenClawPluginApi> is fully restored from upstream (fix: remove invalid WeakSet.clear() call from resetRegistration() #498 fix). Per-instance tracking with _registeredApis.has(api) is working correctly.
  • Idempotency guard timing fixed: _initialized = true is now set only at the very end of successful register() initialization (after all setup including api.registerService), wrapped in try/catch. If init throws, _initialized stays false and a future instance can retry.

If you reviewed an earlier version of this PR, please re-review the latest commit — it should show the WeakSet is properly restored and the timing issue is resolved.

Must Fix 3 — CI cli-smoke failure

The CI failure on cjk-recursion-regression.test.mjs is pre-existing and environmental, not caused by this PR:

  • The error (synthetic_chunk_failure from mock embedder on port 127.0.0.1:44073) is a transient test environment issue
  • The test itself shows PASSED — the failure is due to stderr output causing non-zero exit code even though all assertions pass
  • We verified locally: cjk-recursion-regression.test.mjs does NOT fail locally
  • This PR only adds 93 lines and deletes 0 — it does not touch the embedder or test infrastructure

Must Fix 4 — EADDRINUSE port 11434

Confirmed as environmental — full test suite crash before completing, unrelated to this PR.

Questions

Issue #448 confirmed by maintainers?
Yes — your opening statement in the review ("the derived ownership bleed in multi-agent setups is a real problem worth fixing") confirms Issue #448 is a valid bug. This PR fixes it.

register() lifecycle — can it be called with different API instances?
Yes. The WeakSet design from upstream PR #365 was specifically added for this reason — to track each distinct OpenClawPluginApi instance independently, preventing the "second register() on a new API instance being silently skipped" regression.

Nice to Have

Optional chaining on api.logger.debug — Already present in the code (api.logger.debug?.(...)). No issue here.

Legacy combined reflection ownership fix — The buildDerivedCandidates legacy fallback (line 349-351) only triggers when the new format has zero derived entries. Legacy entries also go through the isOwnedByAgent pre-filter at line 248, so legacy fallback only exposes a sub-agent's own legacy derived items (not main's). The memory-reflection-item format is the primary path; legacy is a graceful degradation that will naturally fade as new format entries accumulate.


Summary

All Must Fix items are addressed in commit fcf23f5. CI failures are environmental, not caused by this PR. Ready for re-review whenever you're available.

@jlin53882 jlin53882 force-pushed the fix/issue-448-v2 branch 2 times, most recently from 48eecb7 to fcf23f5 Compare April 6, 2026 06:14
@win4r
Copy link
Copy Markdown
Collaborator

win4r commented Apr 6, 2026

@claude

@claude
Copy link
Copy Markdown

claude bot commented Apr 6, 2026

Claude Code is working…

I'll analyze this and get back to you.

View job run

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants