Skip to content

A refused session delete leaks an instance reference: the session stays pinned in that process and every later delete there is refused #1765

Description

@edwinyyyu

What happened

EpisodicMemoryManager.delete_episodic_session takes a reference on the cached instance before it checks whether the instance is in use, and does not release it when it refuses (packages/server/src/memmachine_server/episodic_memory/episodic_memory_manager.py, as of 82b6c6f58 main):

ref_count = await self._instance_cache.get_ref_count(session_key)  # line 301
instance = await self._instance_cache.get(session_key)             # line 302, takes a reference
if instance and ref_count > 0:
    raise SessionInUseError(session_key, ref_count)                # line 304, reference not released

MemoryInstanceCache.get increments the node's ref_count (episodic_memory/instance_lru_cache.py:132, :139). Every refused delete therefore leaves the count one higher than the number of requests holding the instance, so it never returns to zero. close_session reads the count and raises before it calls get (episodic_memory_manager.py:357-362) and does not leak.

Reproduced by calling the manager directly with one cached instance held by one request, at instance_cache_size 0 and at 100, with the same result:

Step Result
A request holds the instance ref_count 1
delete_episodic_session raises SessionInUseError, ref_count 2
The request releases its reference ref_count 1, instance still cached, not closed
delete_episodic_session again, nothing in flight raises SessionInUseError

Why it matters

One delete that overlaps a request on the same session leaves that process in a state only a restart clears.

Expected

A refused delete leaves the reference count as it found it: read the count and raise before calling get, as close_session does.

Notes

Found while reading the instance cache for #1571. The leak is reproduced at the manager; its effect on the open paths is read from the code, not run end to end. The defect goes away when the reference counts and the in-use check are removed (#1755, PR (e)); the fix above is independent of that and of the deletion queue (#1749).


🤖 Written by Claude Code (Claude Fable 5.1) on behalf of @edwinyyyu.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    concurrencyRaces, lost updates, unarbitrated read-then-write under concurrent requests or processesrecoveryA failure or crash leaves durable state nothing repairs: partial writes, lost jobs, no retry

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions