Skip to content

fix: prevent memory contamination between agents in different sessions#17

Merged
blackms merged 4 commits into
mainfrom
fix/memory-contamination-issue-7
Jan 28, 2026
Merged

fix: prevent memory contamination between agents in different sessions#17
blackms merged 4 commits into
mainfrom
fix/memory-contamination-issue-7

Conversation

@blackms
Copy link
Copy Markdown
Owner

@blackms blackms commented Jan 28, 2026

Summary

Fixes a critical security vulnerability where agents in different sessions could access each other's memory due to:

  • Shared default namespace for all agents
  • No session→namespace mapping in spawner
  • AgentContext lacking sessionId field
  • getById() having no access filtering
  • Session end having no memory cleanup
  • REST endpoints being unprotected

Changes

Core Access Control (src/memory/access-control.ts - NEW)

  • MemoryAccessContext interface with required sessionId
  • MemoryAccessControl class for validating session-based access
  • Methods: getSessionNamespace(), validateAccess(), canAccessEntry(), deriveNamespace()

Memory Manager (src/memory/index.ts)

  • Updated AgentContext to require sessionId
  • Integrated access control into all operations (store, get, getById, delete, list, search)
  • Auto-derives session namespace when not explicitly provided

SQLite Store (src/memory/sqlite-store.ts)

  • Added deleteByNamespace() for session cleanup
  • Updated active_agents schema with memory_namespace column

Agent Spawner (src/agents/spawner.ts)

  • Generates memoryNamespace using session:${sessionId} or session:${agentId} as fallback

Session Hooks (src/hooks/session.ts)

  • Session end now cleans up session memory and stops all session agents

MCP Tools (src/mcp/tools/memory-tools.ts)

  • Made sessionId required for all memory operations
  • Each tool sets agent context and derives namespace from sessionId

REST Endpoints (src/web/routes/memory.ts)

  • Added optional sessionId query parameter for session-scoped operations

Types (src/types.ts)

  • Added memoryNamespace?: string to SpawnedAgent

Test plan

  • Run existing tests: npm test - 1,874 tests passing
  • Unit tests for access control (23 tests) - tests/unit/memory-access-control.test.ts
  • Integration tests for session isolation (11 tests) - tests/integration/memory-isolation.test.ts
    • Session A cannot read Session B's memory
    • Session A cannot write to Session B's namespace
    • getById() denies access to other session's entries
    • Session end deletes all session memory
    • Search respects session boundaries

Closes #7

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Session-based memory isolation: memory operations are session-scoped and derive per-session namespaces; sessionId is required for write/delete operations.
    • Automatic session cleanup: ending a session stops active agents and removes that session's memory; active agent listings include a memoryNamespace.
    • Public access-control API to manage session namespaces and access rules.
  • Tests

    • New integration and unit tests validating cross-session isolation, cleanup, access control, and session-scoped memory tooling.

✏️ Tip: You can customize this high-level summary in your review settings.

#7)

This commit fixes a critical security vulnerability where agents in different
sessions could access each other's memory due to shared default namespace,
lack of session-based isolation, and no access control enforcement.

Changes:
- Add MemoryAccessControl class for session-based memory isolation
- Require sessionId in AgentContext for all memory operations
- Add deleteByNamespace() to SQLite store for session cleanup
- Enforce namespace isolation in MemoryManager operations (store, get, list, search, delete)
- Update spawnAgent to generate memoryNamespace for each agent
- Add session cleanup in sessionEndHook (delete memory + stop agents)
- Secure MCP memory tools with required sessionId parameter
- Add optional sessionId filtering to REST memory endpoints
- Add memoryNamespace field to SpawnedAgent type

Tests:
- 23 unit tests for MemoryAccessControl
- 11 integration tests for end-to-end session isolation
- Updated existing MCP memory tools tests

Closes #7

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jan 28, 2026

Caution

Review failed

The pull request is closed.

📝 Walkthrough

Walkthrough

Implements session-scoped memory isolation and access control: new MemoryAccessControl, per-session namespaces for agents, MemoryManager and MCP tools require/derive sessionId, SQLite persistence and active_agents updated with memory_namespace, and session-end cleanup that deletes session memory and stops session agents.

Changes

Cohort / File(s) Summary
Access Control
src/memory/access-control.ts
New MemoryAccessControl and MemoryAccessContext with namespace derivation/validation, getAccessControl and resetAccessControl.
Core Memory Manager
src/memory/index.ts
Integrates access control into MemoryManager; AgentContext now includes sessionId (agentId optional); derives/validates namespaces for store/get/list/search/delete; exports access-control symbols.
SQLite Store / Persistence
src/memory/sqlite-store.ts
Adds deleteByNamespace(namespace: string); active_agents table gains memory_namespace; saveActiveAgent/loadActiveAgents persist/load memory_namespacememoryNamespace.
Agent Spawner
src/agents/spawner.ts
Computes per-agent memoryNamespace via access control (prefers options.sessionId), includes memoryNamespace in spawned agent payload and persists it.
Session Hooks
src/hooks/session.ts
sessionEndHook derives session namespace, deletes session-scoped memory (deleteByNamespace), lists and stops agents for the session, and logs counts.
MCP Memory Tools
src/mcp/tools/memory-tools.ts
MCP memory tool handlers require sessionId in input schemas, derive session namespace when omitted, and wrap operations with withContext to set/clear agent context.
Web Routes
src/web/routes/memory.ts
Request handlers derive namespace from sessionId when namespace omitted, set per-request agent/session context, propagate agentId, and clear context after operations.
Types
src/types.ts
SpawnedAgent gains optional memoryNamespace?: string.
Tests — Integration & Unit
tests/integration/memory-isolation.test.ts, tests/unit/*
New and updated tests validating access control, session namespace derivation, MCP schemas requiring sessionId, deleteByNamespace behavior, and session-end cleanup (agent termination + memory deletion).

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Spawner as Agent Spawner
    participant Access as MemoryAccessControl
    participant Memory as Memory Manager
    participant Store as SQLite Store

    Client->>Spawner: spawnAgent(options{sessionId})
    Spawner->>Access: getSessionNamespace(sessionId)
    Access-->>Spawner: namespace = "session:<id>"
    Spawner->>Store: saveActiveAgent(..., memory_namespace)
    Store-->>Spawner: persisted
    Spawner-->>Client: SpawnedAgent { memoryNamespace }

    Client->>Memory: store(key, content, sessionId)
    Memory->>Access: validateAccess(sessionId, write)
    Access-->>Memory: allowed
    Memory->>Access: deriveNamespace(sessionId)
    Access-->>Memory: namespace = "session:<id>"
    Memory->>Store: insert(key, content, namespace)
    Store-->>Memory: stored
    Memory-->>Client: success
Loading
sequenceDiagram
    participant SessionHook as Session Manager
    participant Access as MemoryAccessControl
    participant Store as SQLite Store
    participant Spawner as Agent Spawner
    participant Memory as Memory Manager

    SessionHook->>Access: getSessionNamespace(sessionId)
    Access-->>SessionHook: namespace = "session:<id>"
    SessionHook->>Store: deleteByNamespace(namespace)
    Store-->>SessionHook: rows_deleted
    SessionHook->>Spawner: listAgents(sessionId)
    Spawner-->>SessionHook: [agents...]
    loop for each agent
        SessionHook->>Spawner: stopAgent(agentId)
        Spawner-->>SessionHook: stopped
    end
    SessionHook->>Memory: clearAgentContext()
    Memory-->>SessionHook: context cleared
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~65 minutes

Possibly related PRs

Poem

🐰 I burrowed through namespaces wide and deep,

Session carrots now mine to keep,
I guard the crumbs, I hop and say,
"Each session has its own sweet hay!"
Cleaned and tidy, I curl to sleep.

🚥 Pre-merge checks | ✅ 4 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% 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 'fix: prevent memory contamination between agents in different sessions' directly and clearly describes the main security fix addressing cross-session memory access vulnerabilities.
Linked Issues check ✅ Passed The PR comprehensively addresses all critical objectives from issue #7: enforces session-scoped namespaces, integrates access control, requires sessionId, implements session cleanup, protects REST/MCP endpoints, and adds extensive tests for isolation.
Out of Scope Changes check ✅ Passed All changes are directly scoped to fixing the memory contamination vulnerability; no unrelated features, refactoring, or side-effect changes were introduced beyond the security requirements.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

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 and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/memory/sqlite-store.ts (1)

279-287: Add a migration for active_agents.memory_namespace.

CREATE TABLE IF NOT EXISTS won’t add the new column on existing DBs, so the new INSERT/UPDATE statements will fail at runtime. Add a lightweight migration (e.g., ALTER TABLE) during initialization.

🛠️ Proposed fix
  private initSchema(): void {
    this.db.exec(SCHEMA);
+    this.ensureSchemaMigrations();
  }
+
+  private ensureSchemaMigrations(): void {
+    const columns = this.db
+      .prepare('PRAGMA table_info(active_agents)')
+      .all() as Array<{ name: string }>;
+    if (!columns.some(c => c.name === 'memory_namespace')) {
+      this.db.exec('ALTER TABLE active_agents ADD COLUMN memory_namespace TEXT');
+    }
+  }

Also applies to: 1732-1760

src/mcp/tools/memory-tools.ts (1)

186-324: Always clear agent context on handler errors.

memory_get, memory_list, and memory_delete don’t clear context when an exception is thrown (e.g., access denied). That risks stale context leaking into later operations. Use try/finally.

🛠️ Proposed fix
-        // Set agent context for this operation
-        memory.setAgentContext({
-          sessionId: input.sessionId,
-        });
-
-        const entry = memory.get(input.key, namespace);
-
-        // Clear context after operation
-        memory.clearAgentContext();
-
-        if (!entry) {
-          return {
-            found: false,
-            message: 'Entry not found',
-          };
-        }
-
-        return {
-          found: true,
-          entry: {
-            id: entry.id,
-            key: entry.key,
-            content: entry.content,
-            namespace: entry.namespace,
-            metadata: entry.metadata,
-            createdAt: entry.createdAt.toISOString(),
-            updatedAt: entry.updatedAt.toISOString(),
-          },
-        };
+        // Set agent context for this operation
+        memory.setAgentContext({
+          sessionId: input.sessionId,
+        });
+
+        try {
+          const entry = memory.get(input.key, namespace);
+
+          if (!entry) {
+            return {
+              found: false,
+              message: 'Entry not found',
+            };
+          }
+
+          return {
+            found: true,
+            entry: {
+              id: entry.id,
+              key: entry.key,
+              content: entry.content,
+              namespace: entry.namespace,
+              metadata: entry.metadata,
+              createdAt: entry.createdAt.toISOString(),
+              updatedAt: entry.updatedAt.toISOString(),
+            },
+          };
+        } finally {
+          memory.clearAgentContext();
+        }
-        // Set agent context for this operation
-        // Only set agentId if explicitly provided for filtering
-        memory.setAgentContext({
-          agentId: input.agentId,  // undefined if not provided
-          sessionId: input.sessionId,
-          includeShared: input.includeShared ?? true,  // Default to including shared
-        });
-
-        const entries = memory.list(namespace, input.limit, input.offset, {
-          agentId: input.agentId,
-          includeShared: input.includeShared ?? true,  // Default to including shared
-        });
-        const total = memory.count(namespace);
-
-        // Clear context after operation
-        memory.clearAgentContext();
-
-        return {
-          total,
-          count: entries.length,
-          offset: input.offset ?? 0,
-          entries: entries.map(e => ({
-            id: e.id,
-            key: e.key,
-            namespace: e.namespace,
-            agentId: e.agentId,
-            contentPreview: e.content.slice(0, 200) + (e.content.length > 200 ? '...' : ''),
-            metadata: e.metadata,
-            createdAt: e.createdAt.toISOString(),
-            updatedAt: e.updatedAt.toISOString(),
-          })),
-        };
+        // Set agent context for this operation
+        // Only set agentId if explicitly provided for filtering
+        memory.setAgentContext({
+          agentId: input.agentId,  // undefined if not provided
+          sessionId: input.sessionId,
+          includeShared: input.includeShared ?? true,  // Default to including shared
+        });
+
+        try {
+          const entries = memory.list(namespace, input.limit, input.offset, {
+            agentId: input.agentId,
+            includeShared: input.includeShared ?? true,  // Default to including shared
+          });
+          const total = memory.count(namespace);
+
+          return {
+            total,
+            count: entries.length,
+            offset: input.offset ?? 0,
+            entries: entries.map(e => ({
+              id: e.id,
+              key: e.key,
+              namespace: e.namespace,
+              agentId: e.agentId,
+              contentPreview: e.content.slice(0, 200) + (e.content.length > 200 ? '...' : ''),
+              metadata: e.metadata,
+              createdAt: e.createdAt.toISOString(),
+              updatedAt: e.updatedAt.toISOString(),
+            })),
+          };
+        } finally {
+          memory.clearAgentContext();
+        }
-        // Set agent context for this operation
-        memory.setAgentContext({
-          sessionId: input.sessionId,
-        });
-
-        const deleted = memory.delete(input.key, namespace);
-
-        // Clear context after operation
-        memory.clearAgentContext();
-
-        return {
-          success: deleted,
-          message: deleted ? 'Entry deleted' : 'Entry not found',
-        };
+        // Set agent context for this operation
+        memory.setAgentContext({
+          sessionId: input.sessionId,
+        });
+
+        try {
+          const deleted = memory.delete(input.key, namespace);
+          return {
+            success: deleted,
+            message: deleted ? 'Entry deleted' : 'Entry not found',
+          };
+        } finally {
+          memory.clearAgentContext();
+        }
🤖 Fix all issues with AI agents
In `@src/memory/access-control.ts`:
- Around line 61-71: In validateAccess, the local variable sessionNamespace
(assigned via this.getSessionNamespace(context.sessionId)) is declared but never
used, triggering the no-unused-vars lint error; either remove the unused
declaration or use it where intended—open the validateAccess method and delete
the line declaring sessionNamespace, or if session-specific namespace checks
were intended, replace the unused variable with actual logic that uses
this.getSessionNamespace(context.sessionId) to enforce namespace rules; ensure
references to getSessionNamespace and validateAccess remain consistent.
- Around line 126-132: The canAccessEntry agent scoping is too permissive:
update the check in canAccessEntry to explicitly consider entry.shared and
require matching agentId when includeShared is false, and when includeShared is
true allow either a matching agentId or a shared entry. Specifically, inside the
context.agentId branch (where entryAgentId is available) change the condition so
that if context.includeShared === false you return false unless entryAgentId ===
context.agentId AND entry.shared !== true; if context.includeShared === true
only allow access when entryAgentId === context.agentId OR entry.shared ===
true; also handle cases where entryAgentId is missing by denying access unless
entry.shared === true and context.includeShared === true.

In `@src/web/routes/memory.ts`:
- Around line 31-37: deriveNamespace currently allows an explicit namespace like
"session:*" even when sessionId is missing, bypassing setSessionContext and
access checks; update deriveNamespace to reject session-scoped explicit
namespaces when sessionId is absent (i.e., if explicitNamespace startsWith
"session:" and sessionId is falsy, return undefined), and ensure callers still
call setSessionContext(sessionId) only when sessionId is present so
accessControl.getSessionNamespace(sessionId) is used for derived session
namespaces.
- Around line 79-93: The route sets session context with
setSessionContext(manager, sessionId, body.agentId) but calls
manager.clearAgentContext() only on the happy path, risking context leakage if
manager.store (or other manager methods) throws; change the flow to call
setSessionContext before invoking manager.store/search/list/get/delete and
ensure manager.clearAgentContext() is always invoked in a finally block (i.e.,
wrap the manager.* call(s) in try { ... } finally { manager.clearAgentContext();
}) so the agent context is cleared regardless of errors—apply the same pattern
to all routes that use setSessionContext.
- Around line 51-53: The pagination total is wrong because manager.count()
doesn't accept the same filtering options as manager.list(); update the count
method signature to count(namespace: string, options?: { agentId?: string;
includeShared?: boolean }) and implement the same filtering logic used by list
(apply agentId and includeShared in the query/where clause), then change the
call in memory route (where you call manager.count(namespace)) to pass the same
options object you pass to manager.list (e.g., { agentId }). Ensure the unique
symbols manager.count, manager.list, and the options shape are used consistently
so pagination totals match the filtered list.
🧹 Nitpick comments (1)
src/memory/sqlite-store.ts (1)

538-577: Consider relying on FK cascades in deleteByNamespace.

The schema already uses ON DELETE CASCADE for memory_tags, memory_versions, and memory_relationships, so manual deletes can be removed for a simpler and cheaper implementation.

♻️ Proposed simplification
     return this.transaction((db) => {
-      // Get all memory IDs in this namespace
-      const memoryIds = db
-        .prepare('SELECT id FROM memory WHERE namespace = ?')
-        .all(namespace) as Array<{ id: string }>;
-
-      if (memoryIds.length === 0) {
-        return 0;
-      }
-
-      const ids = memoryIds.map(m => m.id);
-
-      // Delete memory_tags (junction table)
-      const placeholders = ids.map(() => '?').join(',');
-      db.prepare(`DELETE FROM memory_tags WHERE memory_id IN (${placeholders})`).run(...ids);
-
-      // Delete memory_versions
-      db.prepare(`DELETE FROM memory_versions WHERE memory_id IN (${placeholders})`).run(...ids);
-
-      // Delete memory_relationships (both directions)
-      db.prepare(`DELETE FROM memory_relationships WHERE from_id IN (${placeholders}) OR to_id IN (${placeholders})`).run(...ids, ...ids);
-
-      // Delete the memory entries themselves
       const result = db
         .prepare('DELETE FROM memory WHERE namespace = ?')
         .run(namespace);
 
       log.info('Deleted memory by namespace', { namespace, count: result.changes });
       return result.changes;
     });

Comment thread src/memory/access-control.ts Outdated
Comment on lines +126 to +132
// If agentId filter is set on context and entry has an agentId
if (context.agentId && entryAgentId) {
// If includeShared is false, only allow matching agentId
if (context.includeShared === false && entryAgentId !== context.agentId) {
return false;
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Tighten agent scoping in canAccessEntry.

Current logic allows access to other agents’ private entries when includeShared is true, and doesn’t exclude shared entries when includeShared is false. This breaks agent-level isolation inside a session.

🛠️ Proposed fix
-    if (context.agentId && entryAgentId) {
-      // If includeShared is false, only allow matching agentId
-      if (context.includeShared === false && entryAgentId !== context.agentId) {
-        return false;
-      }
-    }
+    if (context.agentId) {
+      // Never allow access to another agent's private entry
+      if (entryAgentId && entryAgentId !== context.agentId) {
+        return false;
+      }
+      // If includeShared is false, exclude shared (agent_id = NULL) entries
+      if (context.includeShared === false && !entryAgentId) {
+        return false;
+      }
+    }
🤖 Prompt for AI Agents
In `@src/memory/access-control.ts` around lines 126 - 132, The canAccessEntry
agent scoping is too permissive: update the check in canAccessEntry to
explicitly consider entry.shared and require matching agentId when includeShared
is false, and when includeShared is true allow either a matching agentId or a
shared entry. Specifically, inside the context.agentId branch (where
entryAgentId is available) change the condition so that if context.includeShared
=== false you return false unless entryAgentId === context.agentId AND
entry.shared !== true; if context.includeShared === true only allow access when
entryAgentId === context.agentId OR entry.shared === true; also handle cases
where entryAgentId is missing by denying access unless entry.shared === true and
context.includeShared === true.

Comment thread src/web/routes/memory.ts
Comment thread src/web/routes/memory.ts
Comment on lines +51 to 53
const entries = manager.list(namespace, limit, offset, { agentId });
const total = manager.count(namespace);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify whether MemoryManager.count supports agentId/filters.
rg -n "count\s*\(" src/memory/index.ts src/memory/sqlite-store.ts -C2

Repository: blackms/aistack

Length of output: 613


🏁 Script executed:

#!/bin/bash
# Check the list() method signature and implementation
rg -n "^\s*list\s*\(" src/memory/index.ts src/memory/sqlite-store.ts -A5 -B2

Repository: blackms/aistack

Length of output: 853


🏁 Script executed:

#!/bin/bash
# Check the actual implementation details of list() and count() in sqlite-store
rg -n "list\s*\(" src/memory/sqlite-store.ts -A25 | head -50

Repository: blackms/aistack

Length of output: 907


🏁 Script executed:

#!/bin/bash
# Check the full count() implementation in sqlite-store
sed -n '617,635p' src/memory/sqlite-store.ts

Repository: blackms/aistack

Length of output: 685


🏁 Script executed:

#!/bin/bash
# Verify the usage context in memory.ts to confirm the issue
sed -n '48,55p' src/web/routes/memory.ts

Repository: blackms/aistack

Length of output: 297


Update count() to respect agentId filtering for accurate pagination totals.

list() filters by agentId (via options parameter), but count() does not accept this parameter, causing the total to include entries from other agents and mislead pagination.

The count() method signature should accept the same options?: { agentId?: string; includeShared?: boolean } parameter as list() and apply the corresponding filters in the query.

🤖 Prompt for AI Agents
In `@src/web/routes/memory.ts` around lines 51 - 53, The pagination total is wrong
because manager.count() doesn't accept the same filtering options as
manager.list(); update the count method signature to count(namespace: string,
options?: { agentId?: string; includeShared?: boolean }) and implement the same
filtering logic used by list (apply agentId and includeShared in the query/where
clause), then change the call in memory route (where you call
manager.count(namespace)) to pass the same options object you pass to
manager.list (e.g., { agentId }). Ensure the unique symbols manager.count,
manager.list, and the options shape are used consistently so pagination totals
match the filtered list.

Comment thread src/web/routes/memory.ts Outdated
Comment on lines +79 to +93
const manager = getManager();
const sessionId = body.sessionId ?? params.query.sessionId;
const namespace = deriveNamespace(sessionId, body.namespace);

setSessionContext(manager, sessionId, body.agentId);

const entry = await manager.store(body.key, body.content, {
namespace: body.namespace,
namespace,
metadata: body.metadata,
generateEmbedding: body.generateEmbedding,
agentId: body.agentId,
});

manager.clearAgentContext();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Always clear agent context in a finally block.

If store/search/list/get/delete throws, the context remains set and can leak into subsequent requests, undermining isolation.

♻️ Suggested fix (apply similarly to other routes using setSessionContext)
-  setSessionContext(manager, sessionId, body.agentId);
-
-  const entry = await manager.store(body.key, body.content, {
-    namespace,
-    metadata: body.metadata,
-    generateEmbedding: body.generateEmbedding,
-    agentId: body.agentId,
-  });
-
-  manager.clearAgentContext();
+  setSessionContext(manager, sessionId, body.agentId);
+  let entry;
+  try {
+    entry = await manager.store(body.key, body.content, {
+      namespace,
+      metadata: body.metadata,
+      generateEmbedding: body.generateEmbedding,
+      agentId: body.agentId,
+    });
+  } finally {
+    manager.clearAgentContext();
+  }
🤖 Prompt for AI Agents
In `@src/web/routes/memory.ts` around lines 79 - 93, The route sets session
context with setSessionContext(manager, sessionId, body.agentId) but calls
manager.clearAgentContext() only on the happy path, risking context leakage if
manager.store (or other manager methods) throws; change the flow to call
setSessionContext before invoking manager.store/search/list/get/delete and
ensure manager.clearAgentContext() is always invoked in a finally block (i.e.,
wrap the manager.* call(s) in try { ... } finally { manager.clearAgentContext();
}) so the agent context is cleared regardless of errors—apply the same pattern
to all routes that use setSessionContext.

blackms and others added 2 commits January 28, 2026 01:28
- Add error handling tests for memory_store and memory_search
- Add test for session end with active agents (covers agent cleanup)
- Add test for session memory cleanup on session end

These tests cover previously uncovered error paths:
- memory-tools.ts: lines 110-115, 176-182 (catch blocks)
- session.ts: lines 94-95, 97-98 (agent cleanup code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add withContext() helper to reduce context management duplication in memory-tools.ts
- Add comprehensive JSDoc documentation to access-control.ts
- Improve logging in session.ts: use debug level when no entries deleted
- Require sessionId for write/delete operations in REST memory endpoints
- Update routes tests to include sessionId for write operations

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@src/web/routes/memory.ts`:
- Around line 22-28: The setSessionContext function currently defaults agentId
to sessionId which alters access semantics; change setSessionContext (the
function that calls manager.setAgentContext) so it only includes agentId in the
object when agentId is explicitly provided (i.e., set { sessionId } when agentId
is undefined, and { agentId, sessionId } only when agentId is truthy),
preserving agentId as an optional filter rather than forcing it to sessionId.

Comment thread src/web/routes/memory.ts
Comment on lines +22 to +28
function setSessionContext(manager: ReturnType<typeof getManager>, sessionId?: string, agentId?: string) {
if (sessionId) {
manager.setAgentContext({
agentId: agentId ?? sessionId,
sessionId,
});
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Don’t default agentId to sessionId.
agentId is an optional filter; forcing it to sessionId changes access semantics and diverges from MCP tools (which only set agentId when provided). This can inadvertently hide agent-owned entries once stricter agent scoping is enforced.

✅ Suggested fix
   function setSessionContext(manager: ReturnType<typeof getManager>, sessionId?: string, agentId?: string) {
     if (sessionId) {
       manager.setAgentContext({
-        agentId: agentId ?? sessionId,
-        sessionId,
+        sessionId,
+        ...(agentId ? { agentId } : {}),
       });
     }
   }
🤖 Prompt for AI Agents
In `@src/web/routes/memory.ts` around lines 22 - 28, The setSessionContext
function currently defaults agentId to sessionId which alters access semantics;
change setSessionContext (the function that calls manager.setAgentContext) so it
only includes agentId in the object when agentId is explicitly provided (i.e.,
set { sessionId } when agentId is undefined, and { agentId, sessionId } only
when agentId is truthy), preserving agentId as an optional filter rather than
forcing it to sessionId.

Fixes lint error: 'sessionNamespace' is assigned a value but never used

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@blackms blackms merged commit 48a2024 into main Jan 28, 2026
5 of 6 checks passed
@blackms blackms deleted the fix/memory-contamination-issue-7 branch January 28, 2026 16:47
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.

Memory contamination between agents due to shared namespace and lack of isolation

1 participant