Skip to content

feat(memory): auto-migrate shared root memories to per-user namespace dirs - #11448

Open
Laurc2004 wants to merge 3 commits into
NousResearch:mainfrom
Laurc2004:fix/memory-migration-from-shared-to-namespaced
Open

feat(memory): auto-migrate shared root memories to per-user namespace dirs#11448
Laurc2004 wants to merge 3 commits into
NousResearch:mainfrom
Laurc2004:fix/memory-migration-from-shared-to-namespaced

Conversation

@Laurc2004

Copy link
Copy Markdown

Summary

When per-user memory isolation is enabled, users with existing shared memories (MEMORY.md / USER.md in ~/.hermes/memories/ root) would lose their memories because the new namespaced directory starts empty.

This PR adds automatic migration: on first load for a new namespaced user, shared root memory files are automatically copied into their per-user directory.

Changes

tools/memory_tool.py

  • Add _migrate_from_shared() — helper that copies MEMORY.md and USER.md from the shared root to a namespaced user directory, only if the target files do not already exist (no overwrite)
  • Fix load_from_disk() in MemoryStore.__init__ area — the original definition used get_memory_dir() without the namespace parameter, which would break per-user isolation. Fixed to use get_memory_dir(self.namespace) and call migration.
  • Remove duplicate load_from_disk() — the second definition shadowed the first, making the fix in the first ineffective
  • Fix regex escaping in get_memory_dir() (@.+@\\.+ for proper literal dot matching)

tests/tools/test_memory_tool.py

  • TestMemoryMigrationFromSharedRoot — new test class with 3 tests:
    • test_migration_copies_shared_files_to_new_namespace — verifies happy path
    • test_migration_does_not_overwrite_existing_user_files — verifies no data loss
    • test_migration_does_nothing_for_empty_namespace — verifies CLI/global sessions unaffected

Testing

All 37 tests in test_memory_tool.py pass, including:

  • 3 new migration tests
  • Existing namespace isolation test
  • All existing MemoryStore and memory_tool dispatcher tests

…ponse

The stream consumer conflated 'some content was streamed' with 'the
final response was delivered', causing the gateway to skip independent
delivery when only tool-progress messages had been sent.

Fixes in stream_consumer.py:
- Line 325 (overflow split + got_done): set _final_response_sent = True
  directly after successfully sending chunks, instead of copying
  _already_sent (which was True from earlier tool-progress edits)
- Lines 417-418 (CancelledError): only set _final_response_sent = True
  when accumulated content and an active message existed — not merely
  because _already_sent was set by earlier edits
- got_done with empty accumulated: set _final_response_sent = True when
  _last_sent_text already delivered the final answer via a prior edit
- _send_fallback_final: set _final_response_sent = True after sending

Fix in gateway/run.py:
- Propagate final_response_sent from stream consumer to agent result dict
  so the gateway can distinguish 'stream sent tool progress' from
  'stream sent the final answer'

Closes NousResearch#10748
- Add namespace parameter to MemoryStore (platform:user_id format)
- Update get_memory_dir to accept namespace and resolve separate paths
- Pass namespace from AIAgent using platform:user_id derivation
- Add TestMemoryStoreNamespaceIsolation to verify cross-user separation
- Fix monkeypatch signature mismatches for new get_memory_dir(ns) API
- Remove erroneous @staticmethod from _path_for instance method

Different platform/user combinations now have completely separate
MEMORY.md and USER.md files, preventing cross-user memory leakage.
CLI/global sessions retain shared access via empty namespace.
… dirs

When a namespaced user (e.g. telegram:123456) loads memory for the first
time, automatically copy shared root MEMORY.md/USER.md into their
per-user directory. This prevents memory loss when enabling per-user
isolation.

- Add _migrate_from_shared() helper that copies shared root files only
  if the target namespace dir doesn't already have them
- Fix duplicate load_from_disk() definition in MemoryStore (the second
  definition shadowed the first but the first used get_memory_dir()
  without namespace, breaking per-user isolation)
- Fix regex escaping in get_memory_dir() (\w\-_@.\+ → \w\-_@\.\+)
- Add 3 new tests covering migration happy-path, no-overwrite, and
  empty-namespace no-op
@Laurc2004

Copy link
Copy Markdown
Author

相关issue引用:

此PR通过per-user namespace实现了内存隔离,解决了群聊中用户身份混淆的问题,并为后续的session搜索隔离和多Agent架构提供了基础。

@teknium1

Copy link
Copy Markdown
Contributor

Thanks for tackling a real multi-user memory boundary.

Problems

  • Current main still has the built-in-memory leak, but its construction moved: agent/agent_init.py:1341-1345 creates MemoryStore without gateway identity. The PR's run_agent.py initialization hunk is stale, and GitHub reports this branch as conflicting.
  • Copying shared MEMORY.md / USER.md into every first-seen platform:user_id namespace cannot safely migrate ownership: the shared files have no user provenance. It can replicate one user's historical profile to unrelated gateway users.
  • The gateway approval fallback remains global: gateway/slash_commands.py:2765-2789 calls load_on_disk_store() without source identity, while tools/memory_tool.py:791-820 accepts no scope. Approved staged writes would bypass the proposed namespace.
  • The PR also includes unrelated streaming-delivery changes. Current final-delivery reconciliation is now implemented at gateway/run.py:19323-19341 and gateway/run.py:19904-19952; review and salvage that work separately.

Suggested changes

  • Rehome scope construction in agent/agent_init.py and thread one validated scope through all direct-store and approval paths.
  • Use an explicit, owner-qualified migration policy rather than copying an unscoped USER.md to every user.
  • Add temp-HERMES_HOME integration tests for two gateway users and staged-write approval.

This is an automated hermes-sweeper review; maintainers will make the final call.

@teknium1 teknium1 added sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages sweeper:blast-broad Sweeper blast radius: broad — a core path most sessions hit labels Jul 12, 2026
@teknium1 teknium1 added the area/memory Memory subsystem: store, providers, sync, background reviews label Jul 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/memory Memory subsystem: store, providers, sync, background reviews P2 Medium — degraded but workaround exists sweeper:blast-broad Sweeper blast radius: broad — a core path most sessions hit sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data tool/memory Memory tool and memory providers type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants