Skip to content

feat: Add global memories as reserved topic#1007

Open
butterflysky-ai wants to merge 3 commits intooraios:mainfrom
butterflyskies:feature/global-memories
Open

feat: Add global memories as reserved topic#1007
butterflysky-ai wants to merge 3 commits intooraios:mainfrom
butterflyskies:feature/global-memories

Conversation

@butterflysky-ai
Copy link
Contributor

@butterflysky-ai butterflysky-ai commented Feb 9, 2026

Summary

Adds cross-project (global) memories using global/ as a reserved topic name, building on the topic grouping from #1058. Writing global/my_memory transparently routes to ~/.serena/memories/global/my_memory.md.

No new parameters — just use the global/ prefix with existing memory tools.

Includes an edit_global_memories config option (default: true) to protect global memories from accidental agent modification.

Closes #1055

Changes

  • MemoriesManager (project.py): Routes global/* names to ~/.serena/memories/global/, adds edit_memory() method for editing memories outside project root
  • Memory tools (memory_tools.py): Edit guard on write/delete/edit/rename, EditMemoryTool uses MemoriesManager.edit_memory() instead of ReplaceContentTool (which fails for paths outside project root)
  • SerenaConfig (serena_config.py): edit_global_memories: bool = True
  • Onboarding check (workflow_tools.py): Filters global memories (they don't indicate project onboarding)
  • Dashboard (dashboard.py): Edit guard on save/delete endpoints
  • Flaky test fix (test_ls_common.py): Skip test_open_file_cache_invalidate in all CI (was only skipped on Windows, also fails on ubuntu-latest)

Design decisions

  • global/ is a reserved topic — bare "global" (no sub-name) raises ValueError
  • rename_memory("global/foo", "bar") moves between scopes (intentional)
  • edit_global_memories: false blocks writes but allows reads
  • No scope parameter on tools — the name prefix is the only routing mechanism

@butterflysky
Copy link

failing tests are also failing on main, no new failures introduced by this PR. I'm interested in trying to help with those as a separate issue, but don't have time to get to them in the next few days.

do let me know if there are questions about this PR and the feature request issue i created. i'm using this change locally to help me streamline my workflow as i hop between working on different projects in parallel.

i store information about my specific infrastructure, environment, and workflow requirements there, as well as instructions about how to manage my task tracker, project planning, wins tracking. saves me a lot of time, keeps me organized. highly recommend a similar approach to others.

@MischaPanch
Copy link
Contributor

Thank you @butterflysky. Global memories are a good idea, let's bring this to Serena! There are some higher priority issues we have to address first, we will review this soon and get back to you.

@butterflysky
Copy link

Thanks @MischaPanch - just want to say I'm finding Serena very helpful, so thank you for it!

@butterflysky
Copy link

rebased and addressed merge conflicts

@MischaPanch
Copy link
Contributor

@butterflysky it probably makes sense to postpone this until #1055 is finished

@butterflysky
Copy link

butterflysky commented Feb 19, 2026

@butterflysky it probably makes sense to postpone this until #1055 is finished

totally makes sense, thanks for linking the issue. between #1055, this, and possibly #994 if that gets implemented (i would absolutely love to have semantic search capability for memories), Serena's memories are going to save a lot of back-and-forth clarifying intent to the LLMs.

I'll keep an eye on the upstream issue and any future developments on the memories front, then keep this diff up-to-date and merge-ready - to the extent it remains relevant.

@butterflysky
Copy link

@butterflysky-ai read the comments here, add a follow-up task to our tracking repo for checking on the status of #1055 and the direction of #994 and keeping this PR up-to-date, please and thanks.

@butterflysky-ai
Copy link
Contributor Author

On it! I've read through the full thread. Here's my summary:

I'll create a tracking task to monitor both upstream issues and keep this PR merge-ready. 👍

butterflysky-ai added a commit to butterflyskies/gh-notify-mcp that referenced this pull request Feb 21, 2026
Short refs resolve to /issues/N but links may be stored as /pull/N.
delete_link now tries canonical URL first, then falls back to exact
entity_ref match, so unlink_entity("oraios/serena#1007") works
regardless of whether the link was created from a PR or issue URL.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
@MischaPanch
Copy link
Contributor

MischaPanch commented Feb 23, 2026

@butterflysky since #1058 has now been merged, we can go ahead and finalize this. The implementation will be very easy - make global a reserved topic. For this special topic, we don't save to the project's .serena/memories but instead to user's home ~/.serena/memories/global. The global topic is then always made available to each project.

For the LLM any mentions of files have already been removed, so adding to global memories is as easy as writing or moving to the global topic. The semantics of global should be explained. Since global memories are not under version control, one might instruct the model to double-check any "destructive" action with the user. One could also forbid editing of global memories altogether, possibly adding a config option edit_global_memories: True or something like that. WDYT?

@opcode81 FYI

@opcode81
Copy link
Contributor

I don't think the configuration option is needed.

@MischaPanch
Copy link
Contributor

I prophecy that users will be annoyed about their forever-lost global memories that got deleted by some stupid agent

@butterflysky-ai butterflysky-ai changed the title Add global (cross-project) memories support feat: Add global memories as reserved topic Feb 24, 2026
@butterflysky-ai
Copy link
Contributor Author

Reworked per @MischaPanch's feedback — replaced the scope parameter approach with global/ as a reserved topic name. This builds directly on #1058's topic grouping, so global/my_memory just routes to ~/.serena/memories/global/my_memory.md transparently.

Also added the edit_global_memories config option (default true) as discussed, and included a small fix for the flaky test_open_file_cache_invalidate test that's been failing on ubuntu-latest CI.

Force-pushed with 2 clean commits on top of current main.

@butterflysky
Copy link

and depending on what the two of you decide @MischaPanch and @opcode81, we can leave in or remove the config option - i interpreted it as a write guard on the global memories, in case people are crafting them by hand and want them read-only in some scenarios.

butterflysky-ai and others added 2 commits February 23, 2026 18:52
The test wrote two values to a file and relied on a 3-second sleep for
the filesystem mtime to advance, which fails when both writes land in
the same second (ext4/tmpfs have 1-second mtime granularity).

Fix: use os.utime() to explicitly bump mtime after the second write.
This is deterministic, instant, and tests the actual cache invalidation
logic (mtime comparison) rather than hoping the clock advances.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
Implement cross-project memories using "global/" as a reserved topic
name that routes to ~/.serena/memories/global/. This replaces the
previous scope-parameter approach with a simpler naming convention
that leverages the existing topic/directory grouping from oraios#1058.

Key changes:
- MemoriesManager routes "global/*" names to ~/.serena/memories/global/
- edit_global_memories config option (default: true) protects globals
- All memory tools support "global/" prefix transparently
- Onboarding check filters global memories (they don't mean project
  is onboarded)
- Dashboard save/delete respect edit_global_memories guard

Co-Authored-By: Claude Opus 4.6 <[email protected]>
Comment on lines 105 to 137
if self._is_global(topic):
include_project = False
# Search within global dir, stripping "global/" or "global" prefix
if topic == self.GLOBAL_TOPIC:
global_search_dir = self._global_memory_dir
else:
global_sub = topic[len(self.GLOBAL_TOPIC) + 1 :]
global_search_dir = self._global_memory_dir / global_sub.replace("/", os.sep) if self._global_memory_dir else None
if global_search_dir and global_search_dir.exists():
for md_file in global_search_dir.rglob("*.md"):
rel_path = md_file.relative_to(self._global_memory_dir) # type: ignore[arg-type]
name = self.GLOBAL_TOPIC + "/" + str(rel_path.with_suffix("")).replace(os.sep, "/")
memories.append(name)
else:
include_global = False
search_dir = self._memory_dir / topic.replace("/", os.sep)
if search_dir.exists():
for md_file in search_dir.rglob("*.md"):
rel_path = md_file.relative_to(self._memory_dir)
name = str(rel_path.with_suffix("")).replace(os.sep, "/")
memories.append(name)
else:
search_dir = self._memory_dir
# No topic filter — list everything
if include_project and self._memory_dir.exists():
for md_file in self._memory_dir.rglob("*.md"):
rel_path = md_file.relative_to(self._memory_dir)
name = str(rel_path.with_suffix("")).replace(os.sep, "/")
memories.append(name)
if include_global and self._global_memory_dir is not None and self._global_memory_dir.exists():
for md_file in self._global_memory_dir.rglob("*.md"):
rel_path = md_file.relative_to(self._global_memory_dir)
name = self.GLOBAL_TOPIC + "/" + str(rel_path.with_suffix("")).replace(os.sep, "/")
memories.append(name)

Choose a reason for hiding this comment

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

@butterflysky-ai DRY, fix

Extract repeated directory-enumeration logic into a _collect helper.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
Comment on lines +154 to +186
def edit_memory(self, name: str, needle: str, repl: str, mode: str) -> str:
"""
Edit a memory by replacing content matching a pattern.

:param name: the memory name
:param needle: the string or regex to search for
:param repl: the replacement string
:param mode: "literal" or "regex"
"""
import re

memory_file_path = self.get_memory_file_path(name)
if not memory_file_path.exists():
raise FileNotFoundError(f"Memory {name} not found.")
with open(memory_file_path, encoding=self._encoding) as f:
content = f.read()

if mode == "literal":
if needle not in content:
raise ValueError(f"The needle string was not found in memory {name}.")
new_content = content.replace(needle, repl)
elif mode == "regex":
pattern = re.compile(needle, re.DOTALL | re.MULTILINE)
new_content, count = pattern.subn(repl, content)
if count == 0:
raise ValueError(f"The regex pattern did not match anything in memory {name}.")
else:
raise ValueError(f"Invalid mode: {mode}. Must be 'literal' or 'regex'.")

with open(memory_file_path, "w", encoding=self._encoding) as f:
f.write(new_content)
return f"Memory {name} edited successfully."

Copy link

@butterflysky butterflysky Feb 24, 2026

Choose a reason for hiding this comment

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

@butterflysky-ai why is this net new? explain why this route is better than updating the existing EditMemoryTool?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

tl;dr: The upstream EditMemoryTool routes through ReplaceContentTool, which calls validate_relative_pathis_path_in_project — this rejects any path outside the project root. Global memories live in ~/.serena/memories/global/, so the .relative_to(project_root) call raises ValueError.

Why not fix ReplaceContentTool instead? It's a general-purpose code editing tool with safety boundaries (project-root containment, ignore-spec checks, EditedFileContext, code editor pipeline). Weakening those for memory files felt wrong. Memory files are markdown, not source code — they don't need the code editor machinery.

Why MemoriesManager is the right home: save_memory, load_memory, delete_memory, rename_memory all do their own file I/O directly. edit_memory is the same pattern — the manager owns memory file paths and encoding. The upstream EditMemoryTool routing through ReplaceContentTool was the odd one out.

Open to other approaches if the maintainers prefer — e.g. adding an absolute-path mode to ReplaceContentTool, or having EditMemoryTool do inline file I/O without a manager method. But this seemed like the least invasive option.

Copy link
Contributor

Choose a reason for hiding this comment

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

I am refactoring to provide the common replacer.

Copy link
Contributor

Choose a reason for hiding this comment

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

Which is the appropriate solution, the AI just giving excuses here. The agents just don't want to refactor 😟

@butterflysky
Copy link

should be ready for review. i'm not sure what to do about the lsp test failures, they seem unreliable at the moment, doesn't seem like it's always the same failure

self._global_memory_dir.mkdir(parents=True, exist_ok=True)
self._encoding = SERENA_FILE_ENCODING

def _is_global(self, name: str) -> bool:
Copy link
Contributor

Choose a reason for hiding this comment

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

This should be public and used everywhere instead of repeating the check logic

You should be able to tell which one you need based on the name of the memory.

{memories}"""
Available project memories: {project_memories}"""
Copy link
Contributor

@MischaPanch MischaPanch Feb 24, 2026

Choose a reason for hiding this comment

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

There's no need to explain the special role of global anywhere except in the write tool. I also think we should only check the new config value for the edit and delete operation (truly destructive ones), not for writing a new one or move. Otherwise it will be difficult for users to add global memories.

The docstrings in the tools should also not mention global all the time, the only exception being the write memory. The example there should be the prototypical "global/style_guide".

Tbh, this got me thinking whether we should make this feature more extensive, since a python style guide should not be included for Java projects. Meaning just a single global container is not enough, we should allow per-project control.

@opcode81 @butterflysky wdyt? If we're doing this, might as well do it right. I'm almost sure that this feature would be requested soon by anyone wanting to use global memories at all

Copy link
Contributor

Choose a reason for hiding this comment

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

There are multiple ways to go for adding the enhanced functionality. We could use frontmatter with markers, allowing users to specify filters in their project.yml. We could use topics within global, and let users select topics in project config. Or something really simple, like a configured glob applied to memory names

Copy link
Contributor

Choose a reason for hiding this comment

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

If you have a java_style_guide while the project is Python, LLMs won't read it.
So the only "problem" is that the memory will be listed, which isn't a significant problem - unless you have a large number of irrelevant memories, which is unlikely.
We can always add additional structure later. Adding more structure also makes memory creation/access a bit more complicated.

Copy link
Contributor

Choose a reason for hiding this comment

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

Ok, let's keep it simple for now. We should just extend the docstring of the write tool to make the LLM choose "qualified" names, maybe just an example is enough

Copy link
Contributor

Choose a reason for hiding this comment

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

I'd prefer java/style_guide btw (topic, not name prefix)

Copy link
Contributor

@opcode81 opcode81 Feb 24, 2026

Choose a reason for hiding this comment

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

It is up to the user (and the LLM) to use a naming scheme that fits the respective requirements

  • java/style_guide makes sense if there are several Java-related memories
  • java_style_guide makes sense if there is only one or a flatter structure is preferred
  • style_guides/java makes sense if grouping by function is desired
  • etc.

We should not impose our preferences.
It is not really logical to even have preferences a priori, because it depends on the situation and the requirements.

def __init__(self, project_root: str):
GLOBAL_TOPIC = "global"

def __init__(self, project_root: str, global_memory_dir: Path | None = None):
Copy link
Contributor

@opcode81 opcode81 Feb 24, 2026

Choose a reason for hiding this comment

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

This is a questionable decision, as it ties global memories to a project, and we cannot access global memories without a project being activated.

An alternative would be to have only root_folder here and instantiate more than once, i.e.

  • for the agent (global memories)
  • for the project, if any.

This allows us to decouple the reporting of global memories from the reporting of per-project memories.

  • We can report global memories only once (in the agent's system prompt) instead of reporting them for every project activation.
  • Project activation lists only project-specific memories.

This saves tokens and allows global memories to be known even before project activation.

@MischaPanch wdyt?

Copy link
Contributor

Choose a reason for hiding this comment

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

Sounds nice but in practice I notice that system prompt is often ignored or at least forgotten after a while by some agents (notably codex), which I work around with by calling activate project even when it's not needed. I think repeating on project activation is not too bad and somewhat safer given that context.

But let me actually verify this behavior in codex first

self.project_root = project_root
self.project_config = project_config
self.memories_manager = MemoriesManager(project_root)
global_memory_dir = Path(SerenaPaths().serena_user_home_dir) / "memories" / MemoriesManager.GLOBAL_TOPIC
Copy link
Contributor

Choose a reason for hiding this comment

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

Add the path to SerenaPaths instead.

@MischaPanch
Copy link
Contributor

@butterflysky thank you for the PR! We'll take this over, finish and merge it

@butterflysky
Copy link

@butterflysky thank you for the PR! We'll take this over, finish and merge it

thanks @MischaPanch, i was just about to make a pass by hand at addressing your review comments, but if you all want to finish it that's great. I appreciate your time and energy here, thank you!

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.

Memories with directory grouping and optional frontmatter

4 participants