Skip to content

fix: add durable Hermes provider link#2051

Open
RerankerGuo wants to merge 16 commits into
MemTensor:dev-v2.0.23from
RerankerGuo:fix/durable-hermes-provider-link
Open

fix: add durable Hermes provider link#2051
RerankerGuo wants to merge 16 commits into
MemTensor:dev-v2.0.23from
RerankerGuo:fix/durable-hermes-provider-link

Conversation

@RerankerGuo

Copy link
Copy Markdown

Description

Fixes #1744.

Hermes provider installation now writes two provider links: the checkout-local plugins/memory/memtensor path used by the current runtime, plus a user-level ~/.hermes/plugins/memory/memtensor link that survives Hermes checkout replacement during upgrades. The adapter-specific installer and Windows installer use the same durable location, and the provider manifest copy remains unchanged.

A small install regression test checks the Unix, adapter, and PowerShell installers so future changes keep both provider paths wired.

Related Issue (Required): Fixes #1744

Type of change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Refactor (does not change functionality, e.g. code style improvements, linting)
  • Documentation update

How Has This Been Tested?

  • Unit Test
  • Test Script Or Test Steps (please provide)
  • Pipeline Automated API Test (please provide)

Commands run:

cd apps/memos-local-plugin && npm test -- --run tests/unit/install/hermes-provider-link.test.ts
cd apps/memos-local-plugin && npm run lint
make format

Results:

  • npm test -- --run tests/unit/install/hermes-provider-link.test.ts: 1 file passed, 3 tests passed.
  • npm run lint: tsc -p tsconfig.json --noEmit completed successfully.
  • make format: blocked locally because poetry is not installed (make: poetry: Command not found).

Checklist

  • I have performed a self-review of my own code | 我已自行检查了自己的代码
  • I have commented my code in hard-to-understand areas | 我已在难以理解的地方对代码进行了注释
  • I have added tests that prove my fix is effective or that my feature works | 我已添加测试以证明我的修复有效或功能正常
  • I have created related documentation issue/PR in MemOS-Docs (if applicable) | 我已在 MemOS-Docs 中创建了相关的文档 issue/PR(如果适用)
  • I have linked the issue to this PR (if applicable) | 我已将 issue 链接到此 PR(如果适用)
  • I have mentioned the person who will review this PR | 我已提及将审查此 PR 的人

Reviewer Checklist

harvey_xiang and others added 14 commits April 23, 2026 19:38
## Description

Please include a summary of the change, the problem it solves, the
implementation approach, and relevant context. List any dependencies
required for this change.

Related Issue (Required):  Fixes #issue_number

## Type of change

Please delete options that are not relevant.

- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to not work as expected)
- [ ] Refactor (does not change functionality, e.g. code style
improvements, linting)
- [ ] Documentation update

## How Has This Been Tested?

Please describe the tests that you ran to verify your changes. Provide
instructions so we can reproduce. Please also list any relevant details
for your test configuration

- [ ] Unit Test
- [ ] Test Script Or Test Steps (please provide)
- [ ] Pipeline Automated API Test (please provide)

## Checklist

- [ ] I have performed a self-review of my own code | 我已自行检查了自己的代码
- [ ] I have commented my code in hard-to-understand areas |
我已在难以理解的地方对代码进行了注释
- [ ] I have added tests that prove my fix is effective or that my
feature works | 我已添加测试以证明我的修复有效或功能正常
- [ ] I have created related documentation issue/PR in
[MemOS-Docs](https://github.com/MemTensor/MemOS-Docs) (if applicable) |
我已在 [MemOS-Docs](https://github.com/MemTensor/MemOS-Docs) 中创建了相关的文档
issue/PR(如果适用)
- [ ] I have linked the issue to this PR (if applicable) | 我已将 issue
链接到此 PR(如果适用)
- [ ] I have mentioned the person who will review this PR | 我已提及将审查此 PR
的人

## Reviewer Checklist
- [ ] closes #xxxx (Replace xxxx with the GitHub issue number)
- [ ] Made sure Checks passed
- [ ] Tests have been provided
Translated an untranslated JSON `description` field in
`docs/cn/open_source/modules/dream.md` from English to Chinese.
@RerankerGuo RerankerGuo marked this pull request as ready for review July 8, 2026 05:42
@RerankerGuo

Copy link
Copy Markdown
Author

This draft is now Ready for review. Cloud test-engine rerun on dev-v2.0.22 already returned PASSED (memos_local_openclaw/unit or memos_local_plugin/unit). Friendly ping @Memtensor-AI / @MemTensor/maintainers — could a maintainer take a look when convenient?

@RerankerGuo

Copy link
Copy Markdown
Author

Hi @Memtensor-AI / @CarltonXiang — friendly ping on this draft.

Quick observation: this PR still has base branch main, but MemTensor/MemOS's current merge flow is into dev-v2.0.22 (every recent PR #2065 / #2058 / #2047 / #2046 / #2042 / #2034 / #2033 / #2026 lands on dev-v2.0.22). The bot already retargeted the base SHA for testing and cloud test-engine returned PASSED against dev-v2.0.22 on every rerun.

Could a maintainer please change the base branch from maindev-v2.0.22 (Edit → base branch dropdown) so this can enter the normal merge queue? The head commit is a single focused change and does not depend on any main-only commits.

Thanks!

@RerankerGuo

Copy link
Copy Markdown
Author

Hi @syzsunshine219 — looks like you've merged 35 of the last 50 PRs on this repo. Friendly ping on this one.

This draft (and the other 21 OPEN PRs I have here) is small, single-commit, has cloud test-engine PASSED, and is fully mergeable. The only thing blocking it is that the base branch dropdown still says main — every recent merge (e.g. #2038, #2037, #2034, #2032) landed on dev-v2.0.22.

When you have a moment, would you mind one of:

  1. Flipping the base branch on this PR from maindev-v2.0.22 (Edit → base branch dropdown), or
  2. Telling me which base branch you'd prefer and I'll re-push accordingly.

Both fix and feature PRs in this batch are independent — each can be reviewed/merged on its own. Happy to rebase / split / drop any that don't fit the direction.

Thanks!

@RerankerGuo RerankerGuo force-pushed the fix/durable-hermes-provider-link branch from db2deea to edfe99c Compare July 8, 2026 07:45
@Memtensor-AI Memtensor-AI changed the base branch from main to dev-v2.0.23 July 8, 2026 11:25
@Memtensor-AI

Copy link
Copy Markdown
Collaborator

🤖 Open Code Review

Target: PR #2051
Task: manual-ocr:MemTensor/MemOS#2051
Base: main
Head: fix/durable-hermes-provider-link

🔍 OpenCodeReview found 9 issue(s) in this PR.

⚠️ 1 warning(s) occurred during review.


1. apps/memos-local-plugin/install.ps1 (L446-L450)

The UserPluginDir (%LOCALAPPDATA%\hermes\plugins\memory) is always created unconditionally before entering the loop, even when creating a junction to it might fail or be unwanted. More critically, notice that the first target in $ProviderTargets is $PluginDir\memtensor (the system/checkout-local plugin path), while the second is $UserPluginDir\memtensor. However, $PluginDir itself was resolved either as %LOCALAPPDATA%\hermes\hermes-agent\plugins\memory or via a Python fallback. If the Python fallback resolves $PluginDir to the same path as $UserPluginDir (both under %LOCALAPPDATA%\hermes), the two entries in $ProviderTargets could potentially overlap or conflict. More importantly, if New-Item -ItemType Junction for the first target fails (e.g., due to permission issues on the Hermes installation path), the script continues into the second iteration without any error handling — the | Out-Null silently swallows errors. Consider adding -ErrorAction Stop or wrapping in a try/catch to surface junction creation failures.

💡 Suggested Change

Before:

    foreach ($Target in $ProviderTargets) {
        if (Test-Path $Target) { Remove-Item -Recurse -Force $Target }
        New-Item -ItemType Junction -Path $Target -Value (Join-Path $AdapterDir "memos_provider") | Out-Null
        Write-Success "Linked -> $Target"
    }

After:

    foreach ($Target in $ProviderTargets) {
        if (Test-Path $Target) { Remove-Item -Recurse -Force $Target }
        try {
            New-Item -ItemType Junction -Path $Target -Value (Join-Path $AdapterDir "memos_provider") -ErrorAction Stop | Out-Null
            Write-Success "Linked -> $Target"
        } catch {
            Write-Warning "Failed to create junction at $Target : $_"
        }
    }

2. apps/memos-local-plugin/install.ps1 (L451)

Copy-Item for plugin.yaml is executed after the loop, but it copies into memos_provider\plugin.yaml (the junction source directory), which is correct. However, this copy was already placed there before the loop's junctions are created in the old code — in the new code its position is unchanged (after the loop). This is fine functionally, but notice that Copy-Item runs even if both junction creations failed silently (due to Out-Null). If the memos_provider directory does not exist or is inaccessible and the junctions fail quietly, this copy will also fail quietly (due to -ErrorAction SilentlyContinue). The combination of two silent failure modes means the overall state after a failed install may be entirely invisible to the user.

💡 Suggested Change

Before:

    Copy-Item -Path (Join-Path $AdapterDir "plugin.yaml") -Destination (Join-Path $AdapterDir "memos_provider\plugin.yaml") -ErrorAction SilentlyContinue

After:

    Copy-Item -Path (Join-Path $AdapterDir "plugin.yaml") -Destination (Join-Path $AdapterDir "memos_provider\plugin.yaml") -ErrorAction SilentlyContinue
    if (-not (Test-Path (Join-Path $AdapterDir "memos_provider\plugin.yaml"))) {
        Write-Warning "plugin.yaml copy may have failed; verify $AdapterDir\memos_provider\plugin.yaml exists."
    }

3. apps/memos-local-plugin/install.ps1 (L440-L441)

The UserPluginDir is always created unconditionally (New-Item -ItemType Directory -Path $UserPluginDir -Force), even if $PluginDir detection succeeded and the user's Hermes install is entirely self-contained. If a future Hermes version uses a different user-level plugin discovery path, this will silently create a stale directory that is never cleaned up. Consider at minimum logging the creation: Write-Host "Ensuring user plugin dir: $UserPluginDir".

💡 Suggested Change

Before:

    $UserPluginDir = Join-Path $env:LOCALAPPDATA "hermes\plugins\memory"
    New-Item -ItemType Directory -Path $UserPluginDir -Force | Out-Null

After:

    $UserPluginDir = Join-Path $env:LOCALAPPDATA "hermes\plugins\memory"
    New-Item -ItemType Directory -Path $UserPluginDir -Force | Out-Null
    Write-Host "Ensuring user plugin dir: $UserPluginDir"

4. apps/memos-local-plugin/adapters/hermes/install.hermes.sh (L55)

Unlike the HERMES_PLUGINS_DIR branch and the equivalent code in install.sh, this unconditional user-level symlink creation does not clean up a pre-existing non-symlink entry at the target path before calling ln -sfn. The -f flag will silently replace an existing symlink but will fail (or misbehave) if $USER_HERMES_PLUGINS_DIR/memtensor is a regular file or directory left by a previous manual installation. The parallel code in install.sh explicitly guards this with:

if [[ -L "${target}" ]]; then rm "${target}"
elif [[ -e "${target}" ]]; then rm -rf "${target}"
fi

Consider applying the same guard here for consistency and robustness.

💡 Suggested Change

Before:

ln -sfn "$PREFIX/adapters/hermes/memos_provider" "$USER_HERMES_PLUGINS_DIR/memtensor"

After:

USER_HERMES_PLUGINS_DIR="${HOME}/.hermes/plugins/memory"
mkdir -p "$USER_HERMES_PLUGINS_DIR"
USER_TARGET="$USER_HERMES_PLUGINS_DIR/memtensor"
if [[ -L "$USER_TARGET" ]]; then rm "$USER_TARGET"
elif [[ -e "$USER_TARGET" ]]; then rm -rf "$USER_TARGET"
fi
ln -sfn "$PREFIX/adapters/hermes/memos_provider" "$USER_TARGET"
log "Linked Python provider → $USER_TARGET"

5. apps/memos-local-plugin/adapters/hermes/install.hermes.sh (L53-L58)

The new unconditional block runs before set -euo pipefail has a chance to surface errors cleanly if $HOME is unset or empty. While HOME is usually set, it is not validated by the script (only AGENT, PREFIX, and HOME_DIR are checked at the top). If HOME is empty, mkdir -p "/.hermes/plugins/memory" would attempt to write to the filesystem root and ln would create /memtensor, both of which are dangerous. Consider adding a guard:

: "${HOME:?HOME must be set}"
💡 Suggested Change

Before:

USER_HERMES_PLUGINS_DIR="${HOME}/.hermes/plugins/memory"
mkdir -p "$USER_HERMES_PLUGINS_DIR"
ln -sfn "$PREFIX/adapters/hermes/memos_provider" "$USER_HERMES_PLUGINS_DIR/memtensor"
log "Linked Python provider → $USER_HERMES_PLUGINS_DIR/memtensor"

if [[ -n "${HERMES_PLUGINS_DIR:-}" ]]; then

After:

: "${HOME:?HOME must be set for user-level plugin directory creation}"
USER_HERMES_PLUGINS_DIR="${HOME}/.hermes/plugins/memory"
mkdir -p "$USER_HERMES_PLUGINS_DIR"

6. apps/memos-local-plugin/adapters/hermes/install.hermes.sh (L60)

The symlink name was silently renamed from memos_provider to memtensor. If any existing Hermes configuration (e.g. plugin.yaml or user config) references the old name memos_provider, those references will break silently after re-running the installer over an existing installation. The rename should be documented or a migration/cleanup step for the old memos_provider symlink should be added.

💡 Suggested Change

Before:

  ln -sfn "$PREFIX/adapters/hermes/memos_provider" "$HERMES_PLUGINS_DIR/memtensor"

After:

# Clean up legacy symlink name if it still exists
LEGACY_TARGET="$HERMES_PLUGINS_DIR/memos_provider"
if [[ -L "$LEGACY_TARGET" ]]; then
  rm "$LEGACY_TARGET"
  log "Removed legacy symlink $LEGACY_TARGET"
fi
ln -sfn "$PREFIX/adapters/hermes/memos_provider" "$HERMES_PLUGINS_DIR/memtensor"

7. apps/memos-local-plugin/install.sh (L742-L746)

The plugin_dir is discovered via Python import introspection or a hardcoded fallback path. If plugin_dir and user_plugin_dir resolve to the same directory (e.g., ~/.hermes/hermes-agent/plugins/memory vs ~/.hermes/plugins/memory are identical in some installs), the loop will attempt to create two symlinks at the exact same path — the first succeeds, the second will fail because the symlink already exists (it was just created). While the two paths look different here, no guard exists if a future environment collapses them. More critically, both ln -s calls are not using -f (force) or any idempotency flag — so if rm of a symlink failed silently (e.g., a race condition), the subsequent ln -s would fail with File exists, aborting the entire installer with a non-zero exit.

Suggestion: Use ln -sf (or ln -sfn as done in install.hermes.sh) instead of the rm + ln -s two-step. This is atomic, idempotent, and consistent with the companion script.

💡 Suggested Change

Before:

  for target in "${provider_targets[@]}"; do
    if [[ -L "${target}" ]]; then rm "${target}"
    elif [[ -e "${target}" ]]; then rm -rf "${target}"
    fi
    ln -s "${adapter_dir}/memos_provider" "${target}"

After:

  for target in "${provider_targets[@]}"; do
    if [[ -e "${target}" && ! -L "${target}" ]]; then rm -rf "${target}"; fi
    ln -sfn "${adapter_dir}/memos_provider" "${target}"

8. apps/memos-local-plugin/install.sh (L749)

The cp of plugin.yaml is performed after the loop that creates both symlinks. Since the second symlink (user_plugin_dir/memtensor) points to the same memos_provider directory as the first, the cp will correctly populate it for both targets. However, the order dependency is subtle and fragile: if someone reorders the loop or the cp line, the second symlink could end up pointing to a directory without plugin.yaml. Consider moving the cp before the loop (so the provider directory is fully prepared before being linked), making the relationship explicit and order-independent.

💡 Suggested Change

Before:

  cp "${adapter_dir}/plugin.yaml" "${adapter_dir}/memos_provider/plugin.yaml" 2>/dev/null || true

After:

  # Ensure the provider directory is fully populated before symlinking
  cp "${adapter_dir}/plugin.yaml" "${adapter_dir}/memos_provider/plugin.yaml" 2>/dev/null || true
  local provider_targets=(
    "${plugin_dir}/memtensor"
    "${user_plugin_dir}/memtensor"
  )
  local target
  for target in "${provider_targets[@]}"; do
    ...

9. apps/memos-local-plugin/install.sh (L743-L746)

The rm + ln -s two-step is not atomic. If rm of an existing symlink/directory fails silently (e.g., due to a race condition or permission issue in certain environments), the subsequent ln -s will fail with File exists, causing the installer to abort. The companion script install.hermes.sh correctly uses ln -sfn (force + no-dereference) which is atomic and idempotent. This script should be consistent.

Suggestion: Replace the rm + ln -s pattern with ln -sfn to match install.hermes.sh.

💡 Suggested Change

Before:

    if [[ -L "${target}" ]]; then rm "${target}"
    elif [[ -e "${target}" ]]; then rm -rf "${target}"
    fi
    ln -s "${adapter_dir}/memos_provider" "${target}"

After:

    if [[ -e "${target}" && ! -L "${target}" ]]; then rm -rf "${target}"; fi
    ln -sfn "${adapter_dir}/memos_provider" "${target}"

🧹 Filtered 1 low-confidence OCR finding(s) before posting/fix-loop (duplicate: 1).

Generated by cloud-assistant via Open Code Review.

@Memtensor-AI Memtensor-AI added the area:plugin Plugin, adapter, bridge, or apps layer | 插件、适配器、桥接层或 apps 目录 label Jul 8, 2026
@Memtensor-AI Memtensor-AI requested a review from bittergreen July 8, 2026 11:43
@Memtensor-AI Memtensor-AI added the status:in-progress Someone or AI is working on it | 人工或 AI 正在处理 label Jul 8, 2026
@Memtensor-AI

Memtensor-AI commented Jul 8, 2026

Copy link
Copy Markdown
Collaborator

Routing this PR to @syzsunshine219 for review/follow-up.

@Memtensor-AI

Copy link
Copy Markdown
Collaborator

✅ Automated Test Results: PASSED

All tests passed (3/3 executed). memos_local_plugin/unit: 3/3. Duration: 2s [advisory, non-gating] AI-generated tests on branch test/auto-gen-82389cfa2d92b165-20260708212013: 67/67 passed — these do NOT affect the PR verdict; review the branch manually.

Branch: fix/durable-hermes-provider-link

OCR review on MemTensor#2051 surfaced 9 robustness issues across the PowerShell
and Unix Hermes installers. This patch addresses every one of them:

PowerShell install.ps1:
  - Surface junction-creation failures via try/catch + -ErrorAction Stop
    instead of silently piping to Out-Null.
  - Log a warning if the plugin.yaml copy may have failed.
  - Log a message when the user-level plugin dir is auto-created.

Unix adapter install.hermes.sh:
  - Validate HOME is set before using it (avoid writing to filesystem
    root if HOME is empty).
  - Remove a pre-existing non-symlink entry at the user-level target
    before ln -sfn, matching install.sh semantics.
  - Clean up the legacy 'memos_provider' symlink name so re-running the
    installer doesn't leave a dangling reference.

Unix install.sh:
  - Move the cp of plugin.yaml BEFORE the symlink loop so the second
    symlink (user-level) already points at a fully populated provider
    directory.
  - Switch the rm + ln -s pattern to atomic ln -sfn (matching the
    adapter script) so a failed rm no longer aborts the install with
    'File exists'.

Adds four regression tests that lock the new behaviors in place.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:plugin Plugin, adapter, bridge, or apps layer | 插件、适配器、桥接层或 apps 目录 status:in-progress Someone or AI is working on it | 人工或 AI 正在处理

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix: Hermes provider symlink breaks after upgrade and bridge health can timeout during startup recovery

5 participants