Skip to content

feat: per-agent memory opt-out (excludeAgents / excludeSubagents) - #357

Merged
xDarkicex merged 4 commits into
xDarkicex:mainfrom
Marvinthebored:feat/exclude-agents
Aug 1, 2026
Merged

feat: per-agent memory opt-out (excludeAgents / excludeSubagents)#357
xDarkicex merged 4 commits into
xDarkicex:mainfrom
Marvinthebored:feat/exclude-agents

Conversation

@Marvinthebored

@Marvinthebored Marvinthebored commented Jun 28, 2026

Copy link
Copy Markdown
Contributor

Summary

Multi-agent OpenClaw gateways run agents with very different memory needs: a primary assistant that wants full recall, a latency-critical voice agent where every injected token and embed round-trip is pure overhead, and ephemeral subagents that should run lean on a focused task. The context engine currently treats every agent identically.

This adds two opt-in config keys so an operator can make LibraVDB fully inert for specific agents:

Key Type Effect
excludeAgents string[] Sessions whose agent id (parsed from the agent:<agentId>:... session key) is listed skip all memory/context work — no injection, ingestion, compaction, or daemon RPCs.
excludeSubagents boolean When true, every subagent session (tracked via the prepareSubagentSpawn lifecycle) skips all memory/context work.

Both default off; behavior is unchanged unless configured.

"libravdb-memory": {
  "config": {
    "excludeAgents": ["fastbot"],   // e.g. a voice agent
    "excludeSubagents": true
  }
}

Design

The plugin already parses agentId out of the session key for namespacing — this just consults it for behavior.

  • assemble() is a true no-op for excluded sessions: it returns the host's messages byte-identical with an empty systemPromptAddition. It deliberately does not run them through budget-fitting — budget-fitting can drop messages mid-tool-protocol, which strict providers reject ("invalid schema/tool payload"). Context budget stays the host's responsibility.
  • bootstrap / ingest / afterTurn early-return a no-op result.
  • compact() only receives sessionId (no sessionKey), so excluded sessions are recorded in a small bounded set at bootstrap time, letting compact() short-circuit too.
  • Subagents: prepareSubagentSpawn marks the child session key when excludeSubagents is set (and skips granting an expansion budget, since there's no memory to expand); onSubagentEnded / the spawn rollback clear the marker. All per-session state is cleared in dispose.

Testing

  • tsc --noEmit clean; tsc -p tsconfig.build.json + bundle clean.
  • Unit suite green (77/77), including 4 new tests: excluded agent makes zero daemon calls (verified with a runtime whose getClient throws) and returns byte-identical passthrough; non-excluded agent still reaches the daemon; subagent exclusion + lifecycle teardown; excludeSubagents off-by-default control.
  • The same semantics have been running in a production multi-agent gateway (hand-patched on an earlier release) on an Intel Mac; this is the clean port to current main with tests. Happy to test any requested changes on that box.

Notes

Backward compatible and fully opt-in. Schema entries added to openclaw.plugin.json (the schema is additionalProperties: false, so the keys must be declared to be accepted).

Summary

  • Added two opt-in memory/context exclusion settings: excludeAgents and excludeSubagents.
  • Updated context handling so excluded agent sessions and subagent sessions skip injection, ingestion, compaction, and daemon RPCs.
  • Preserved passthrough behavior for excluded sessions: assemble() returns the host messages unchanged with an empty systemPromptAddition.
  • Tracked excluded sessions during bootstrap so compact() can short-circuit later, and cleaned up exclusion state on subagent end and disposal.
  • Extended the config schema and public plugin types to accept the new options.
  • Implemented a fallback so exclusion checks use the sessionId recorded at bootstrap when sessionKey is not provided, and cleared stale exclusion markers for non-excluded bootstrap paths.

Tests

  • Added coverage for excluded-agent passthrough and daemon-call avoidance.
  • Added coverage for normal behavior when sessions are not excluded.
  • Added coverage for subagent exclusion, rollback, and lifecycle cleanup.

Review notes

  • Big‑O: No Big‑O complexity regression identified. The new exclusion checks are constant-time set lookups/branches (O(1)) and excluded flows intentionally skip the heavier memory/daemon work.
  • Cyclomatic complexity: src/context-engine.ts has additional exclusion/lifecycle branching; a cyclomatic-complexity proxy increased slightly (heuristic decision-point count +3 from the parent version, driven mainly by added ternary/conditional operators).

…-out

Multi-agent gateways run a mix of agents with very different memory needs:
a primary assistant that wants full recall, a latency-critical voice agent
where every injected token and embed round-trip is pure overhead, and
ephemeral subagents that should run lean. The context engine currently
treats every agent identically.

This adds two opt-in config keys:

- excludeAgents: string[] — sessions whose agent id (parsed from the
  agent:<agentId>:... session key) is listed skip ALL memory/context work:
  no injection, ingestion, compaction, or daemon RPCs.
- excludeSubagents: boolean — when true, every subagent session (tracked via
  the prepareSubagentSpawn lifecycle) skips all memory/context work.

For an excluded session, assemble() is a true no-op: it returns the host's
messages byte-identical with an empty systemPromptAddition (no budget-fitting,
which can drop messages mid-tool-protocol and trip strict providers).
bootstrap/ingest/afterTurn early-return; compact() short-circuits via session
ids recorded at bootstrap (compact only receives sessionId, not sessionKey).

Adds JSON schema entries and unit tests covering exclusion, the
daemon-untouched guarantee, the non-excluded control paths, and subagent
lifecycle teardown.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 28, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@xDarkicex, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 37 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 03ffadaf-39f4-4da0-8788-d29de0735b00

📥 Commits

Reviewing files that changed from the base of the PR and between 18b5a14 and b8f731e.

📒 Files selected for processing (3)
  • openclaw.plugin.json
  • src/context-engine.ts
  • test/unit/context-engine.test.ts
📝 Walkthrough

Walkthrough

Adds opt-in excludeAgents and excludeSubagents settings to the plugin schema and PluginConfig, then updates the context engine so matching sessions skip memory/context work across lifecycle and subagent paths. Tests cover excluded sessions, bounded tracking, and normal behavior.

Changes

Agent/subagent exclusion feature

Layer / File(s) Summary
Config schema and PluginConfig types
src/types.ts, openclaw.plugin.json
Adds excludeAgents?: string[] and excludeSubagents?: boolean to the public configuration shape and plugin schema.
Agent exclusion state and session shortcuts
src/context-engine.ts
Tracks excluded agents and sessions, bounds exclusion markers, and short-circuits bootstrap, ingest, assemble, compact, and afterTurn.
Subagent exclusion and cleanup
src/context-engine.ts
Excludes spawned subagents when enabled and clears exclusion state during rollback, completion, and disposal.
Exclusion behavior validation
test/unit/context-engine.test.ts
Tests excluded and non-excluded agent behavior, side-table overflow handling, daemon access, and subagent lifecycle behavior.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant PluginConfig
  participant buildContextEngineFactory
  participant ContextEngine
  participant DaemonClient
  PluginConfig->>buildContextEngineFactory: Provide exclusion settings
  buildContextEngineFactory->>ContextEngine: Initialize exclusion state
  ContextEngine->>ContextEngine: Evaluate session exclusion
  ContextEngine-->>DaemonClient: Skip memory and context RPCs
Loading

Possibly related PRs

Suggested labels: release:minor

Poem

I’m a rabbit with a config key, 🐇
Skipping daemon hops with glee.
Quiet sessions pass through the day,
While subagents hop the same soft way.
No memory maze, no context queue—
Just gentle hops for me and you.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: adding per-agent and subagent memory opt-out settings.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@compoodment compoodment left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Vale Review — PR #357

Quality: Q4/5 — sharp
Head: dc475b3

Findings:

  • src/context-engine.ts:2773 — compact() only skips excluded sessions while their id is still present in the bootstrap-populated excludedSessionIds set, but that set is capped at 1000 and evicts the oldest entry at src/context-engine.ts:2351. After 1001 excluded sessions, an older still-active excluded session can fall through to normal compaction and daemon work, breaking the advertised “no compaction, or daemon RPCs” guarantee. major

Proof gaps: No local gate run; source path is deterministic and current checks are still pending.

Verdict: request-changes — excluded sessions must stay inert for compaction even after the side table hits its bound.

– Vale

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
test/unit/context-engine.test.ts (1)

2624-2626: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert that teardown really clears exclusion state.

These calls are only smoke-tested right now. If either cleanup path stops removing excludedSubagentKeys, this test still passes. Add one post-teardown bootstrap/assertion with a non-throwing runtime and verify the same childSessionKey reaches bootstrapSessionKernel again.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/unit/context-engine.test.ts` around lines 2624 - 2626, The teardown path
in the context engine test is only smoke-tested and does not verify that
exclusion state is actually cleared. Update the test around the
`handle.rollback?.()` and `engine.onSubagentEnded(...)` cleanup so it performs
one post-teardown bootstrap/assertion using a non-throwing runtime, then confirm
the same `childSessionKey` is allowed through `bootstrapSessionKernel` again.
Use the existing `excludedSubagentKeys`-related flow in `ContextEngine` to
locate the assertion point.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/context-engine.ts`:
- Around line 1703-1711: `isExcludedSession` currently only checks `sessionKey`,
so an excluded bootstrapped session can slip through when the hook omits it.
Update `isExcludedSession` in `context-engine.ts` to fall back to the recorded
`sessionId` whenever `sessionKey` is undefined, and apply the same fallback in
the other exclusion checks referenced by `sessionKey`-based logic so
`excludedAgents` and `excludedSubagentKeys` still match the session
consistently.
- Around line 2350-2357: The exclusion-tracking logic in the
isExcludedSession/sessionId path leaves stale entries in excludedSessionIds, so
a reused sessionId can keep causing compact() to return “agent excluded.” Update
the bootstrap handling around the existing isExcludedSession(args.sessionKey)
branch to clear any prior exclusion marker for that sessionId when the current
session is not excluded, and keep the add/eviction behavior only for truly
excluded sessions. Make sure the fix is applied in the same context that manages
excludedSessionIds and the compact() exclusion check so reused IDs cannot retain
old state.

---

Nitpick comments:
In `@test/unit/context-engine.test.ts`:
- Around line 2624-2626: The teardown path in the context engine test is only
smoke-tested and does not verify that exclusion state is actually cleared.
Update the test around the `handle.rollback?.()` and
`engine.onSubagentEnded(...)` cleanup so it performs one post-teardown
bootstrap/assertion using a non-throwing runtime, then confirm the same
`childSessionKey` is allowed through `bootstrapSessionKernel` again. Use the
existing `excludedSubagentKeys`-related flow in `ContextEngine` to locate the
assertion point.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 15af0409-d3bb-4593-b64b-cbbad518f5a2

📥 Commits

Reviewing files that changed from the base of the PR and between be9cbcb and dc475b3.

📒 Files selected for processing (4)
  • openclaw.plugin.json
  • src/context-engine.ts
  • src/types.ts
  • test/unit/context-engine.test.ts

Comment thread src/context-engine.ts Outdated
Comment thread src/context-engine.ts Outdated
Addresses CodeRabbit review on the per-agent memory opt-out:

- isExcludedSession now falls back to the sessionId recorded at bootstrap
  when sessionKey is absent. sessionKey is optional on these hooks, so an
  excluded session could otherwise reach daemon work when the host omits it.
  bootstrap/ingest/assemble/afterTurn pass sessionId through.
- bootstrap clears any stale excludedSessionIds marker on the non-excluded
  path, so a reused sessionId can no longer make compact() keep returning
  "agent excluded".

@compoodment compoodment left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Vale Review — PR #357

Quality: Q4/5 — sharp
Head: c43b7d4

Findings:

  • src/context-engine.ts:2358 — excluded sessions are still tracked in a capped excludedSessionIds set, while compact() later trusts that set alone at src/context-engine.ts:2783. After enough excluded sessions evict an older active session id, compact() receives only sessionId, misses the exclusion, and can fall through to normal compaction/daemon work despite the PR promising excluded sessions skip compaction and daemon RPCs. major

Proof gaps: No local gate run; source path is deterministic, but needs a regression test for more than EXCLUDED_SESSION_IDS_MAX excluded bootstraps followed by compact on the evicted session.

Verdict: request-changes — the stale-marker fix helps reused ids, but the bounded compact side table still breaks the exclusion guarantee.

– Vale

… survive side-table overflow

compact() previously trusted only the bounded excludedSessionIds set. Past
EXCLUDED_SESSION_IDS_MAX cumulative excluded bootstraps, an older but still-active
excluded session was evicted and its next compact() fell through to real
compaction and daemon RPCs — including on the on-demand /compact path, which is
not preceded by an assemble() that could refresh the marker.

The host already threads sessionKey through every compaction path (timeout and
overflow recovery in run.ts, and the manual /compact lane in compact.queued.ts),
so compact() can resolve exclusion authoritatively from the agent id instead of
the evictable side table. Declare sessionKey?: string on the compact args and
route the check through isExcludedSession(); this is eviction-proof and also
covers excludeSubagents (the child sessionKey recorded at prepareSubagentSpawn).

The sessionId side table is retained only as a best-effort fallback for the rare
case where the host cannot backfill a sessionKey, and every per-turn hook that
carries the authoritative sessionKey (assemble/ingest/afterTurn) now refreshes
the marker (MRU) so an id evicted while idle is re-established on the next turn.

Adds two regression tests: a direct sessionKey-carrying compact() after 1001
excluded bootstraps, and the sessionKey-less fallback after eviction + refresh.
@Marvinthebored

Copy link
Copy Markdown
Contributor Author

Thanks @compoodment — the bounded-side-table finding was correct, and the root cause was deeper than the cap: compact() was trusting excludedSessionIds alone, so any eviction (not just the reused-id case) could let a still-active excluded session fall through — including the on-demand /compact path, which isn't preceded by an assemble() that could refresh the marker.

Fixed authoritatively in 18b5a14 rather than by resizing the cap:

  • The host already passes sessionKey to compact() on every path — timeout recovery (run.ts:2591), overflow recovery (run.ts:2795), and the manual /compact lane (compact.queued.ts, incl. trigger === "manual"). The old // compact() only receives sessionId comment was stale. So compact() now declares sessionKey?: string and routes through isExcludedSession(args.sessionKey, args.sessionId) — exclusion is resolved from the agent id and is eviction-proof, regardless of the side table's state. This also covers excludeSubagents (the child sessionKey recorded at prepareSubagentSpawn).
  • The excludedSessionIds set is now only a best-effort fallback for the rare case where the host couldn't backfill a sessionKey. To keep that path correct too, markExcludedSession() refreshes the marker (MRU) from every per-turn hook that carries the authoritative sessionKey (assemble/ingest/afterTurn), so an id evicted while idle is re-established on the session's next turn.

Regression tests (both confirmed to fail on the pre-fix code):

  1. excludeAgents: a direct compact() carrying sessionKey stays inert after the side table overflows — bootstraps 1001 excluded sessions to evict the target, then a bare compact() with no preceding assemble() still returns "agent excluded" without touching the (throwing) client.
  2. excludeAgents: an active excluded session stays inert for a sessionKey-less compact via the refreshed side table — the fallback path.

Only residual is the explicitly best-effort case (a compact() with no sessionKey for an evicted, unrefreshed session), which the host paths above don't hit. Ready for another look.

compoodment
compoodment previously approved these changes Jul 13, 2026

@compoodment compoodment left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Vale Review — PR #357

Quality: Q4/5 — sharp
Head: 18b5a14

Findings:

  • src/context-engine.ts:2803 — None: compact() now accepts sessionKey and resolves exclusion before touching the daemon client; src/context-engine.ts:1709-1728 keeps the sessionId set as fallback only, with overflow regressions covering the prior blocker. info

Proof gaps: No local gate run in this worker; host-side compact sessionKey threading is asserted from the PR discussion, not independently tested here.

Verdict: approve — current head fixes the bounded side-table compaction escape that caused the previous request-changes review.

– Vale
use @vale review

@xDarkicex xDarkicex added the release:patch Bump patch version on merge label Aug 1, 2026
@xDarkicex
xDarkicex merged commit c5bbc6f into xDarkicex:main Aug 1, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

release:patch Bump patch version on merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants