Skip to content

feat: add user_mapper.py for cross-channel memory unification - #19163

Open
Cyrene963 wants to merge 1 commit into
NousResearch:mainfrom
Cyrene963:feat/cross-channel-memory-mapper
Open

feat: add user_mapper.py for cross-channel memory unification#19163
Cyrene963 wants to merge 1 commit into
NousResearch:mainfrom
Cyrene963:feat/cross-channel-memory-mapper

Conversation

@Cyrene963

Copy link
Copy Markdown

Summary

Addresses community feedback on multi-user isolation design: same user on different channels (CLI, Telegram, Discord) gets separate memory banks.

Solution

user_mapper.py — a filesystem utility that unifies a user's memories across channels via symlinks:

memories/{chat_id}/ → memories/user_{username}/

Commands

# Map a channel to a user
python3 user_mapper.py map --chat-id 7359770766 --user nitrogen

# Migrate existing data + map
python3 user_mapper.py migrate --chat-id 7359770766 --user nitrogen

# List all mappings
python3 user_mapper.py list

# Resolve a chat_id
python3 user_mapper.py resolve --chat-id 7359770766

# Remove mapping
python3 user_mapper.py unmap --chat-id 7359770766

Relationship to PR #17989

PR Problem Solved Layer
#17989 Different users can't see each other's data Code (MemoryStore, session_search)
This PR Same user shares memory across channels Filesystem (symlinks)

Complementary, not competing. #17989 is the foundation; this builds on it.

How It Works

  1. Creates memories/user_{username}/ as the real data directory
  2. Symlinks memories/{chat_id}/user_{username}/
  3. Symlinks global MEMORY.md/USER.mduser_{username}/ (for CLI)
  4. Hermes follows symlinks transparently — zero code changes

Limitations

  • Global MEMORY.md symlink means all non-Telegram sessions (CLI, cron) share the same user's memory. Fine for single-user setups; multi-user CLI needs code-level user_id support.
  • This is a companion tool, not a replacement for proper cross-channel identity in Hermes core.

Test Results

All 5 tests pass locally:

  • ✅ CLI reads correct memory via global symlink
  • ✅ Telegram reads correct memory via chat_id symlink
  • ✅ Different user remains isolated
  • ✅ Write consistency across all paths
  • ✅ user_mapper CLI commands work

@alt-glitch alt-glitch added type/feature New feature or request P3 Low — cosmetic, nice to have comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint comp/cli CLI entry point, hermes_cli/, setup wizard comp/gateway Gateway runner, session dispatch, delivery comp/plugins Plugin system and bundled plugins tool/memory Memory tool and memory providers tool/skills Skills system (list, view, manage) labels May 3, 2026
@Cyrene963
Cyrene963 force-pushed the feat/cross-channel-memory-mapper branch from 1231b79 to 500d2b0 Compare May 3, 2026 09:56
@alt-glitch

Copy link
Copy Markdown
Collaborator

Related to #9308 — both address cross-channel identity/memory unification. #9308 approaches via Honcho owner identity at gateway layer; this PR uses filesystem symlinks as a companion tool.

1 similar comment
@alt-glitch

Copy link
Copy Markdown
Collaborator

Related to #9308 — both address cross-channel identity/memory unification. #9308 approaches via Honcho owner identity at gateway layer; this PR uses filesystem symlinks as a companion tool.

@Cyrene963

Copy link
Copy Markdown
Author

Thanks for pointing out #9308! You're right that both address cross-channel identity, but they solve it at different scopes:

#9308 (Honcho gateway layer):

  • Auto-detects the bot owner via DM + home channel match
  • Works at the Honcho memory plugin level
  • Only covers the owner — other users still get fragmented memories

#19163 (filesystem layer):

The gap in #9308: if a non-owner user (e.g., a classmate sharing the bot) uses both Telegram and CLI, their memories are still split. #9308's _is_owner_source check intentionally skips non-owner users.

Proposed approach:

I'll update #19163 to also support automatic owner detection (symlink the global MEMORY.md/USER.md to the owner's user directory on first setup), reducing the manual config burden.

@Cyrene963
Cyrene963 force-pushed the feat/cross-channel-memory-mapper branch from 500d2b0 to 9ea7ce0 Compare May 3, 2026 10:18
@ether-btc

Copy link
Copy Markdown
Contributor

The user_mapper.py approach is architecturally sound — unified identity mapping across channels prevents the fragmentation problem where the same user appears as different IDs on Telegram vs Discord.

Questions before merge:

  1. Location: scripts/user_mapper.py is an odd home for a runtime component (and this is a net-new addition, not an edit). If this runs at agent startup or on each message, it belongs in agent/ or tools/. scripts/ typically implies standalone utilities. What's the invocation path?

  2. Persistence: Does the mapper state survive session restarts? If channels are added incrementally, the mapper needs to be persisted (DB, file). If it's recomputed on each startup from some canonical source, that should be documented.

  3. Conflict resolution: When two channels map to the same user_id but have conflicting display names or metadata, which wins? There should be an explicit merge strategy.

The core logic looks solid. The file is well-structured with clear docstrings. Resolving the location and persistence questions would unblock merge.

@Cyrene963

Copy link
Copy Markdown
Author

Thanks for the thorough review, @ether-btc! Good questions. Here's the breakdown:

1. Location: scripts/ vs agent/ vs tools/

user_mapper.py is a standalone CLI utility, not a runtime component. It's invoked manually during setup:

python3 user_mapper.py auto-setup      # one-time owner detection
python3 user_mapper.py map --chat-id X --user Y  # manual mapping

It does not run at agent startup or on each message. Once the symlinks are created, Hermes reads memories/{chat_id}/ and follows the symlink transparently — zero runtime code changes. This is why scripts/ is the correct home (same pattern as scripts/release.py, scripts/run_tests.sh).

That said, if the maintainers prefer tools/ for discoverability, I'm happy to move it.

2. Persistence

The mapper state persists across restarts via two mechanisms:

  • Mapping file: ~/.hermes/user_mapping.json — JSON dict of {chat_id: username}. Loaded on each CLI invocation, saved on map/unmap commands.
  • Symlinks: Created on the filesystem (~/.hermes/memories/{chat_id}/ → user_{username}/). These survive restarts, reboots, and hermes update. They're just regular filesystem symlinks.

The mapping is not recomputed on startup — it's explicitly managed via CLI commands. Once set, it persists until manually unmapped.

3. Conflict Resolution

With the symlink approach, there are no metadata conflicts to resolve — the design sidesteps the problem entirely:

  • The canonical key is username (not display name, not metadata)
  • All channels mapped to the same user point to the same directory (user_{username}/)
  • Data naturally merges because it's literally the same filesystem directory
  • Display names and per-channel metadata are not stored in the mapping file — only the {chat_id → username} mapping exists

If two channels map to the same user but have conflicting data (e.g., different USER.md content), the second map command doesn't overwrite — it symlinks to the existing directory. The first channel's data wins (it's already there). The migrate command handles moving existing data into the user directory with explicit confirmation.

Summary: The mapper is intentionally minimal — a CLI tool + JSON file + symlinks. No runtime code, no database, no conflict resolution logic needed because the filesystem handles it.

@Cyrene963
Cyrene963 force-pushed the feat/cross-channel-memory-mapper branch from 9ea7ce0 to 4cf1e75 Compare May 7, 2026 13:44
…cation

Adds  command that:
1. Reads config.yaml to find Telegram home channel chat_id
2. Falls back to largest memory directory detection
3. Automatically migrates data and creates symlinks
4. Sets up global MEMORY.md/USER.md for CLI access

Addresses feedback on NousResearch#9308 overlap:
- NousResearch#9308 auto-detects owner at gateway layer (Honcho only)
- This provides auto-setup for owner + manual mapping for any user
- Works with all memory providers, not just Honcho

Commands: auto-setup, map, unmap, list, resolve, migrate
@Cyrene963
Cyrene963 force-pushed the feat/cross-channel-memory-mapper branch from 4cf1e75 to eba9d52 Compare May 7, 2026 14:54

@teknium1 teknium1 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks for tackling cross-channel continuity. Current main shows that this implementation needs a redesign before it can provide the stated behavior.

Problems

  • scripts/user_mapper.py:61 assumes Hermes reads memories/<chat_id>/; built-in memory instead always reads root MEMORY.md and USER.md from get_hermes_home() / "memories" (tools/memory_tool.py:55-57, :185-189). The per-chat links are inert, while the global links redirect all built-in-memory sessions in the profile.
  • Provider memory is identity-based: agent/agent_init.py:1384-1410 forwards runtime user IDs and chat metadata. Filesystem links cannot unify those providers. Honcho already documents many-to-one userPeerAliases mapping (plugins/memory/honcho/README.md:145-167).
  • The script hardcodes Path.home() / ".hermes" (scripts/user_mapper.py:79-81), bypassing HERMES_HOME/profile isolation (hermes_constants.py:55-110).
  • scripts/user_mapper.py:414-427 skips colliding files then deletes the source directory, which can lose skipped data.

Suggested changes

  • Re-scope this around provider-consumable canonical identity mapping, using the existing Honcho mapping where applicable, and add hermetic HERMES_HOME tests for migration/conflict cases.

Automated hermes-sweeper review.

Comment thread scripts/user_mapper.py
"""Get the directory Hermes expects for a chat_id."""
return MEMORIES_DIR / chat_id


Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This bypasses get_hermes_home(), so it targets the wrong state for named profiles and custom HERMES_HOME deployments. Use the profile-aware resolver rather than hardcoding Path.home() / '.hermes'.

Comment thread scripts/user_mapper.py
print(__doc__)
sys.exit(1)

cmd = sys.argv[1]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

A destination collision is skipped here, but the source directory is unconditionally removed at line 427. That silently deletes every skipped source item; require an explicit conflict policy that preserves or aborts on conflicting data.

@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-platform-windows Sweeper risk: may break or behave differently on native Windows sweeper:blast-contained Sweeper blast radius: contained — one narrow path / opt-in / few users labels Jul 12, 2026
@alt-glitch alt-glitch removed comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint comp/cli CLI entry point, hermes_cli/, setup wizard comp/gateway Gateway runner, session dispatch, delivery comp/plugins Plugin system and bundled plugins tool/skills Skills system (list, view, manage) 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 labels Jul 18, 2026
@alt-glitch alt-glitch added needs-decision Awaiting maintainer decision before any implementation and removed sweeper:risk-platform-windows Sweeper risk: may break or behave differently on native Windows labels Jul 18, 2026
@teknium1 teknium1 added the area/memory Memory subsystem: store, providers, sync, background reviews label Jul 19, 2026
ether-btc pushed a commit to ether-btc/aeon that referenced this pull request Jul 27, 2026
The github-filing-registry.json was carrying 4 entries pointing at PRs
that no longer exist (404 Not Found from GitHub API) or that the token
could no longer access (401 Bad credentials). These were filed in
April-May 2026 against:
- ch0udry/hermes-rtk-optimizer#1
- NousResearch/hermes-agent#19163 (and 2 others)
- wysie/hermes-omni-plugin#1

Sync now correctly reports them as 'unknown' and they're removed from
the registry. Also: skills/github-filing-registry/filing-registry cmd_sync()
now captures gh CLI exit code separately so 'unknown' is set when the API
fails (not just when the jq parse fails).

Co-authored-by: housekeeping-session-2026-07-26
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 needs-decision Awaiting maintainer decision before any implementation P3 Low — cosmetic, nice to have sweeper:blast-contained Sweeper blast radius: contained — one narrow path / opt-in / few users 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.

4 participants