CrewAI memory: forget() and reset() miss records whose scope has no leading slash
In CrewAI 1.15.x, a scope written without a leading slash is stored as-is, and recall, forget and reset don't agree on which records it names. Anchor every scope yourself until the fix lands.
Disclosure: I work on MemorySync (I'm Mohammed, the founder). I drafted this article with AI assistance and checked it against the GitHub issue, the CrewAI docs and the CrewAI 1.15.33 source, and the MemorySync docs listed under Sources on Oct 29, 2026. Code status: the snippets below are adapted from the CrewAI docs, the 1.15.33 source and the reproduction in the issue, and were not run. I didn't run a crew, a LanceDB store or a MemorySync request for this article.
CrewAI issue #8255 (Memory scope paths are not anchored: "sales" and "/sales" select different records) (opened Oct 18, 2026, still open on Oct 29) reports a quiet data bug in the unified Memory class. A scope written without a leading slash is stored exactly as passed, and the default LanceDB backend matches scopes by raw string prefix. In the issue's words: "With the default backend, Memory.recall(scope="sales") returns the record while Memory.forget(scope="sales") deletes nothing and Memory.reset(scope="sales") leaves the record in place."
That matters most when you delete. The reporter notes that reset(scope=...) is what Memory.reset() uses for a single scope, so "a scoped reset silently keeps the records it was asked to delete." If you scope memory per customer and promise to erase a customer's data, check this before you trust a scoped delete.
MemorySync doesn't fix this bug. The fix is on your side: pass every scope in canonical form (/sales) and repair rows that were already written as sales. This page shows both. CrewAI behaviour comes from the issue, the CrewAI memory docs and the 1.15.33 source.
Skip the memory plumbing around this setup. Once your scopes are anchored, the MemorySync CrewAI guide shows how to point
Memory(storage=...)at MemorySync so crew memory survives redeploys and can be kept peruser_id. MemorySync doesn't replace the scope fix below. When you're ready, see MemorySync. Founding offer: use code FOUNDING30 for 30% off Starter for your first 3 months (Settings → Billing).
Fix status (Nov 2, 2026)
- Issue #8255: open, no comments yet.
- Pull request #8256, "fix(memory): anchor scope paths on write and in every filter": open, not merged.
- Latest release, 1.15.34 (Nov 2, 2026): the memory code is unchanged from 1.15.33. I read the encoding flow and LanceDB storage at the 1.15.33 tag. The fast path still stores
item.scopeas passed when noroot_scopeis set, andreset()still builds its range from the prefix as given. So treat this as unfixed in released versions until the PR ships.
Step 1: Know what you're looking at
CrewAI's docs describe scopes as filesystem-like paths: "Each scope is a path like /, /project/alpha, or /agent/researcher/findings." The full scope paths in the docs all start with a slash. The API doesn't enforce it, though. The issue's reproduction saves with an explicit scope="sales" (plus categories=[] and importance=0.5, so no LLM picks the scope) and the record is stored as 'sales'.
From there, the issue reports that on the default LanceDB backend:
| Call | Result reported in #8255 |
|---|---|
recall(..., scope="sales") |
finds the record |
recall(..., scope="/sales") |
finds nothing |
forget(scope="sales") |
deletes 0 records |
reset(scope="sales") |
leaves the record in place |
It also reports the reverse case. With records stored at the canonical /sales, reset(scope_prefix="sales") on the storage deleted 0 of them, because the range is built as scope >= 'sales' AND scope < 'sales/\uffff' "and / sorts before s, so an anchored record is never inside the range." The Qdrant Edge backend answered all of these consistently in the reporter's test; LanceDB, the default, did not.
Two quick signs you're affected:
memory.tree()ormemory.list_scopes("/")doesn't show records you know you saved (the docs list both under Discovery).- A scoped
forget()returns0while a scopedrecall()still returns matches.
Step 2: Anchor every scope before it reaches Memory
CrewAI already ships the normaliser the PR builds on. In the 1.15.33 source, crewai.memory.utils.normalize_scope_path returns "a normalized path with leading slash, no trailing slash, no double slashes", and '/' for an empty path. Its docstring example: normalize_scope_path("crew/test") returns '/crew/test'.
Route every scope you pass through it. A thin wrapper keeps that in one place:
from crewai import Memory
from crewai.memory.utils import normalize_scope_path
memory = Memory() # keep your existing storage, llm and embedder settings
def remember(content: str, scope: str | None = None, **kwargs):
if scope is not None:
scope = normalize_scope_path(scope)
return memory.remember(content, scope=scope, **kwargs)
def recall(query: str, scope: str | None = None, **kwargs):
if scope is not None:
scope = normalize_scope_path(scope)
return memory.recall(query, scope=scope, **kwargs)
def forget(scope: str, **kwargs) -> int:
return memory.forget(scope=normalize_scope_path(scope), **kwargs)
def reset_scope(scope: str) -> None:
# Never pass "" or "/" here by accident: per the docs, reset() with no
# scope clears all scopes.
path = normalize_scope_path(scope)
if path == "/":
raise ValueError("refusing to reset the root scope")
memory.reset(scope=path)
Customer IDs and other values you build scopes from are the usual source of a missing slash, for example scope=customer_id instead of scope=f"/customer/{customer_id}". Build them with the slash and normalise anyway.
When you leave scope out, the docs say the LLM infers one from your existing scope tree. I didn't check whether inferred scopes always come back anchored, so the repair step below is worth running even if you never pass scope yourself.
Step 3: Prefer scoped views for agents
For per-agent or per-customer memory, the docs recommend memory.scope(...), which returns a MemoryScope that "restricts all operations to a branch of the tree":
researcher_memory = memory.scope("/agent/researcher")
researcher_memory.remember("Found three relevant papers on LLM memory.")
researcher_memory.recall("relevant papers")
In the 1.15.33 source, MemoryScope._scope_path adds a leading slash to relative paths and joins them under the view's root, so calls made through the view use anchored paths. Pass the root itself with a leading slash ("/agent/researcher", not "agent/researcher"); the view keeps the root you give it.
The same source shows that when Memory has a root_scope, explicit scopes are joined under it with join_scope_paths, which strips the inner path's slashes before joining. The bug in the issue appears on the path with no root_scope, which is the plain Memory() the docs start with. PR #8256's notes say unanchored rows are "only possible when remember(scope="sales") was called without a root_scope, since crews and flows set one". So standalone Memory code, scripts and custom tools that call remember() with a bare scope are the places to check first.
Step 4: Repair records already stored without the slash
Anchoring new calls doesn't move old rows. Collect every record first, then re-scope the ones that aren't canonical. list_records() and the MemoryRecord fields are in the docs and the 1.15.33 source; update(record_id, scope=...) is a Memory method in the same source, and with no new content it doesn't re-embed.
from crewai import Memory
from crewai.memory.utils import normalize_scope_path
memory = Memory() # same storage path and settings as your crew
# 1. Read everything first (list_records pages newest first, 200 by default).
records, offset = [], 0
while True:
page = memory.list_records(limit=200, offset=offset)
if not page:
break
records.extend(page)
offset += len(page)
# 2. Re-scope anything that isn't in canonical form.
fixed = 0
for record in records:
canonical = normalize_scope_path(record.scope)
if record.scope != canonical:
memory.update(record.id, scope=canonical)
fixed += 1
print(f"re-scoped {fixed} of {len(records)} records")
Notes before you run it:
- Back up the store first. For the default backend that's the LanceDB directory CrewAI uses:
$CREWAI_STORAGE_DIR/memoryif that's set, otherwise<platform user-data dir>/<project folder name>/memoryper the 1.15.33 source (the docs say./.crewai/memory; check both), or the path you passed asstorage=. - If your
Memoryhas aroot_scope,list_records()with no scope only lists that root (1.15.33 source). Run the repair with the same settings your crew uses, or once per root. - Per the source, a
Memory(read_only=True)leaves records untouched onupdate(), so don't run it with a read-only instance.
Step 5: Check that one spelling reaches the same rows
Adapted from the issue's reproduction. After Steps 2 to 4, both spellings should go through the wrapper and land on /sales:
remember("We chose Python for the sales pipeline.", scope="sales",
categories=[], importance=0.5, metadata={})
print(memory.info("/sales").record_count) # expect >= 1
print(len(recall("sales pipeline", scope="sales", depth="shallow")))
print(len(recall("sales pipeline", scope="/sales", depth="shallow"))) # same number
print(forget("sales")) # expect > 0
print(memory.info("/sales").record_count) # expect 0
Run that against a throwaway storage path, not your real store. If the two recall counts differ, something is still calling Memory directly with an unanchored scope.
Step 6: Watch the fix
Subscribe to #8255 and PR #8256. The PR description says it anchors the prefix filter so search, delete and reset match the same rows for both spellings, builds reset's range from the anchored prefix, and persists the anchored form on write. Once a release includes it, re-check the Fix status above against the release notes before you drop the wrapper. Run the Step 4 repair either way. The PR's own notes say rows an older version wrote with an unanchored scope "are no longer reachable through a scope filter once queries are anchored" (they still show up in unscoped reads such as list_records()), and that no backfill migration was added.
Skip the plumbing with MemorySync
Disclosure: I build MemorySync.
This doesn't change the fix above. The MemorySync CrewAI guide says MemorySyncStorage implements CrewAI's StorageBackend protocol, so Memory(storage=MemorySyncStorage(user_id=...)) runs the crew's save and recall loop against MemorySync instead of local LanceDB files, and records "survive redeploys". It also says that, because CrewAI hands the backend a query embedding, "scope prefixes, category filters, metadata filters and scores behave exactly like the built-in LanceDB backend." So keep the Step 2 normalisation with this backend too. I haven't tested how it handles an unanchored scope.
Two things from the guide that are relevant if you're here because of deletes: a full reset() with no scope is refused unless you opt in with allow_full_reset=True, and "deletes that remove nothing while matching records exist raise MemorySyncDeleteRefusedError." I haven't checked whether that error fires for the sales / /sales case, so don't rely on it as a detector for this bug.
From the guide:
Unverified (not run):
pip install crewai-memorysync
from crewai import Agent, Crew, Task
from crewai.memory import Memory
from crewai_memorysync import MemorySyncStorage
crew = Crew(
agents=[researcher, writer],
tasks=[research_task, write_task],
memory=Memory(
storage=MemorySyncStorage(user_id="support-crew"),
),
)
crew.kickoff()
Snippet adapted from the MemorySync CrewAI guide; I haven't run it for this page. The guide lists crewai-memorysync 1.1.0, which requires crewai 1.0+ and Python 3.10+, with MEMORYSYNC_API_KEY set in the environment. Setup from scratch is in my earlier tutorial, Persistent memory for CrewAI 1.x.
Try it: MemorySync is optional here: the steps above work without it. If you want the memory layer from the snippet, see MemorySync, create an API key in the dashboard and drop the snippet into your agent.
Troubleshooting
forget(scope=...)returns 0 butrecallwith the same scope finds records. That's the asymmetry in #8255. Normalise the scope (Step 2) and repair stored rows (Step 4).reset(scope="sales")ran and the records are still there. Per the issue, the reset range is built from the prefix as given and misses anchored rows. Call it with"/sales".crew.reset_memories(command_type="memory")cleared everything. That's documented behaviour for unified memory, not this bug. Usememory.reset(scope="/your/scope")for one subtree.- The repair script finds nothing to fix but deletes still miss. Check whether some code path still passes an unanchored scope straight to
Memoryor aMemorySlice, and that you ran the repair with the samestoragepath androot_scopeas the crew. /appalso matches/apple. That's a different, related bug the issue mentions (#7543 / #7544): prefix matching leaking into a sibling. Anchoring doesn't solve it.
Common questions
Are "sales" and "/sales" the same scope in CrewAI memory?
They should be, per the expected behaviour in #8255, but in 1.15.x the default LanceDB backend treats them differently depending on which method you call. Always pass /sales.
Is this fixed in the latest CrewAI? Not as of Nov 2, 2026 (1.15.34): the issue and PR #8256 are open, and 1.15.33 still stores the scope as passed. Check the issue before relying on a newer release.
Does memory.scope("/agent/researcher") avoid the problem?
For calls made through the view, yes: per the 1.15.33 source, it anchors relative paths under its root. Pass the root with a leading slash.
Do I need to migrate existing memories?
Yes, if any were written without a leading slash. The Step 4 script re-scopes them with update(). Back up the store first.
Related pages
- Persistent memory for CrewAI 1.x: keep crew memory across runs and redeploys
- Agno's clear_memory tool wipes every user's memories: how to scope Agno agent memory per user
- Persistent memory for a LangChain chatbot after LangChain 1.0
Sources
- github.com/crewAIInc/crewAI/issues/8255 (opened 2026-10-18, open, 0 comments, unlabeled; reproduction and storage-level table in the issue body; read 2026-10-29)
- github.com/crewAIInc/crewAI/pull/8256 (opened 2026-10-18, open, not merged; read 2026-10-29)
- CrewAI docs: Memory concepts page, docs.crewai.com/en/concepts/memory (scopes,
memory.scope(), slices, discovery, storage path, reset; fetched 2026-10-29) - CrewAI 1.15.33 source (release published 2026-10-29):
memory/utils.py,unified_memory.py,encoding_flow.py,memory_scope.py,storage/lancedb_storage.py(read 2026-10-29) - MemorySync CrewAI guide: docs.memorysync.io/guides/crewai (fetched 2026-10-29)
Try it
For an overview of MemorySync, see the MemorySync homepage.
Next step: read the MemorySync CrewAI guide, then see MemorySync. Founding offer: use code FOUNDING30 for 30% off Starter for your first 3 months (Settings → Billing).
About the author
- Building MemorySync (memorysync.io) — persistent memory infrastructure for AI agents, Cursor, and Claude Code.
Comments
Join the discussionNo comments yet. Be the first to comment.
