Skip to content

[Bug]: Stale legacy installs.json permanently triggers "conflicting plugin install metadata for: brave" #92811

Description

@fenglanhua

Bug type

Behavior bug (incorrect output/state without crash)

Beta release blocker

No

Summary

A stale legacy ~/.openclaw/plugins/installs.json generated during the 5.26 upgrade permanently conflicts with the current installed_plugin_index for brave due to an npm install-path layout change (flat npm/node_modules/ → per-project npm/projects//node_modules/), causing every doctor check to report "conflicting plugin install metadata for: brave" with no path to resolution.

Steps to reproduce

On an installation that has a legacy ~/.openclaw/plugins/installs.json generated by an older version (observed: 5.26, generatedAtMs: 1779887028060 = 2026-05-27), which records brave's installPath under the old flat npm layout: /home/michael/.openclaw/npm/node_modules/@openclaw/brave-plugin.
Upgrade to a version where npm plugin installs use the per-project layout (observed: 6.6), where brave's current installPath is /home/michael/.openclaw/npm/projects/openclaw-brave-plugin-11fe9e3aa3/node_modules/@openclaw/brave-plugin.
Run openclaw memory status --deep (or any other command that triggers a doctor check).
Observe the warning below on every invocation, including immediately after running openclaw plugins registry --refresh.

Expected behavior

Per the installed_plugin_index's own embedded text ("Use openclaw plugins registry --refresh ... instead"), running --refresh should resolve the conflict. Alternatively, per the code path in state-migrations-C5i4ZVgv.js (migrateLegacyInstalledPluginIndex → archiveLegacyInstalledPluginIndex), once the legacy install records are accounted for in the current index, ~/.openclaw/plugins/installs.json should be archived to installs.json.migrated, permanently ending the warning.

Actual behavior

The warning is emitted on every doctor-triggering command, across versions 5.x through 6.6:

Doctor warnings ─────────────────────────────────────────────────────────────────────────╮
│ - Left plugin install index in place because shared SQLite state has conflicting plugin │
│ install metadata for: brave │
├───────────────────────────────────────────────────────────────────────────────────────────╯
07:29:47 [state-migrations] Legacy state migration warnings:

  • Left plugin install index in place because shared SQLite state has conflicting plugin install metadata for: brave
    07:29:47 [state-migrations] Legacy state migration warnings:
  • Left plugin install index in place because shared SQLite state has conflicting plugin install metadata for: brave

openclaw plugins registry --refresh runs successfully ("Plugin registry refreshed: 69/96 enabled plugins indexed.") and re-emits the same warning during the refresh itself. Before/after --refresh, installed_plugin_index.policy_hash is unchanged (908f88d3d9108a63ffc5c6aa1905a7969227a8f3699f5db60ae2d334a0d27643); only generated_at_ms/updated_at_ms are bumped. ~/.openclaw/plugins/installs.json is never touched by --refresh and is never archived, because mergeLegacyInstalledPluginIndexRecords always returns brave in conflicts (the installPath mismatch is permanent), and archiveLegacyInstalledPluginIndex is only reached when conflicts.length === 0.

OpenClaw version

2026.6.6 (8c802aa)

Operating system

Rocky Linux (SELinux Enforcing)

Install method

npm global (via nvm, Node v24.14.1)

Model

N/A — this bug is unrelated to model invocation or routing.

Provider / routing chain

N/A — not applicable to this bug.

Additional provider/model setup details

No response

Logs, screenshots, and evidence

1. Original trigger

$ openclaw memory status --deep

OpenClaw 2026.6.6 (8c802aa) — Your AI assistant, now without the $3,499 headset.
│
◇  Doctor warnings ─────────────────────────────────────────────────────────────────────────╮
│  - Left plugin install index in place because shared SQLite state has conflicting plugin  │
│    install metadata for: brave                                                            │
├───────────────────────────────────────────────────────────────────────────────────────────╯
07:29:47 [state-migrations] Legacy state migration warnings:
- Left plugin install index in place because shared SQLite state has conflicting plugin install metadata for: brave
│
◇  Doctor warnings ─────────────────────────────────────────────────────────────────────────╮
│  - Left plugin install index in place because shared SQLite state has conflicting plugin  │
│    install metadata for: brave                                                            │
├───────────────────────────────────────────────────────────────────────────────────────────╯
│
◇
Memory Search (main)
Provider: gemini (requested: gemini)
Model: gemini-embedding-001
...
Indexed: 1434/1434 files · 22362 chunks
Dirty: no
...
Embeddings: ready

Memory indexing itself is healthy (Indexed: 1434/1434, Dirty: no, Embeddings: ready) — the warning is independent of memory functionality.

2. Current installed_plugin_index (SQLite, single record)

$ sqlite3 -header -line ~/.openclaw/state/openclaw.sqlite \
  "SELECT index_key, version, migration_version, refresh_reason, generated_at_ms, updated_at_ms FROM installed_plugin_index;"

        index_key = installed-plugin-index
          version = 1
migration_version = 1
   refresh_reason = source-changed
  generated_at_ms = 1781335238233
    updated_at_ms = 1781335238249

Only one record exists — rules out multiple conflicting index entries.

3. Legacy ~/.openclaw/plugins/installs.json

$ ls -la ~/.openclaw/plugins/installs.json
-rw-------. 1 user user 125095  5月 27 21:03 ~/.openclaw/plugins/installs.json

Header:

json{
  "version": 1,
  "hostContractVersion": "2026.5.26",
  "compatRegistryVersion": "dddff25fb468e3fe3b7747214c219a4c45f8e92330ae35978c429de3cce14f99",
  "migrationVersion": 1,
  "policyHash": "979d94e2400a282867c8617f9664ec8283815dfb1943c22af98b55b9d341910c",
  "generatedAtMs": 1779887028060,
  "refreshReason": "source-changed"
}

generatedAtMs: 1779887028060 = 2026-05-27T13:03:48Z, matching the 5.26 upgrade. File mtime (5月 27 21:03) confirms it has not been modified since.

4. Side-by-side brave install records

Legacy (~/.openclaw/plugins/installs.json, from 5.26):

json"brave": {
  "source": "npm",
  "spec": "@openclaw/brave-plugin",
  "installPath": "~/.openclaw/npm/node_modules/@openclaw/brave-plugin",
  "version": "2026.5.26",
  "resolvedName": "@openclaw/brave-plugin",
  "resolvedVersion": "2026.5.26",
  "resolvedSpec": "@openclaw/[email protected]",
  "integrity": "sha512-ezzo0YKbIHPvGOlTicLbxIPuTukP4bdDF+9B3R9cB7gTrzTR21J/IAANi8NGWNH41F9vNQkCGgYbVHiM+jmYUQ==",
  "shasum": "5198e5e76ebb1b26e3b91ccd45324f0615d7d267",
  "resolvedAt": "2026-05-27T13:03:44.736Z",
  "installedAt": "2026-05-27T13:03:47.378Z"
}

Current (installed_plugin_index in openclaw.sqlite, from 6.6):

json"brave": {
  "source": "npm",
  "spec": "@openclaw/[email protected]",
  "installPath": "~/.openclaw/npm/projects/openclaw-brave-plugin-11fe9e3aa3/node_modules/@openclaw/brave-plugin",
  "version": "2026.6.6",
  "resolvedName": "@openclaw/brave-plugin",
  "resolvedVersion": "2026.6.6",
  "resolvedSpec": "@openclaw/[email protected]",
  "integrity": "sha512-8XGcZH0YkcZkajol+JSSzqMVkVAcT48lRSDUP1POGcWk815oFGb/SW1TRbU+ZZP4FNQBDccyFv6PhKLKoP+j3w==",
  "shasum": "32c2830714827738919710b4e26ed4cff48d088a",
  "resolvedAt": "2026-06-13T07:20:34.572Z",
  "installedAt": "2026-06-13T07:20:37.359Z"
}

installPath, version, resolvedVersion, resolvedSpec, integrity, and shasum all differ due to the npm layout change between 5.26 and 6.6.

5. Comparison logic that fails (state-migrations-C5i4ZVgv.js)

jsfunction legacyInstallRecordCoveredByCurrent(currentRecord, legacyRecord) {
        if (currentRecord.source !== legacyRecord.source) return false;
        for (const key of Object.keys(legacyRecord).toSorted()) {
                const currentValue = readInstallRecordField(currentRecord, key);
                if (currentValue === readInstallRecordField(legacyRecord, key)) continue;
                if (key === "spec" && legacyInstallRecordHasCurrentResolvedIdentity({
                        currentRecord,
                        legacyRecord
                })) continue;
                if ((key === "resolvedAt" || key === "installedAt") && typeof currentValue === "string") continue;
                return false;
        }
        return true;
}

Only spec (via resolvedSpec identity), resolvedAt, and installedAt differences are tolerated. installPath is processed early (alphabetical key order), the values differ, and the function returns false — brave is added to conflicts.

6. Why this is a permanent loop (migrateLegacyInstalledPluginIndex, same file)

jsasync function migrateLegacyInstalledPluginIndex(params) {
        const sourcePath = resolveLegacyInstalledPluginIndexStorePath({ stateDir: params.stateDir });
        if (!fileExists(sourcePath)) return { changes: [], warnings: [] };
        ...
        const current = readPersistedInstalledPluginIndexSync(storeOptions);
        if (current && !legacyInstalledPluginIndexMatches(current, legacy)) {
                const merged = mergeLegacyInstalledPluginIndexRecords(current, legacy);
                if (merged.addedCount > 0) try {
                        writePersistedInstalledPluginIndexSync(merged.merged, storeOptions);
                        changes.push(`Merged ${merged.addedCount} legacy plugin install ${merged.addedCount === 1 ? "record" : "records"} → shared SQLite state`);
                } catch (err) { ... }
                if (merged.conflicts.length > 0) return {
                        changes,
                        warnings: [`Left plugin install index in place because shared SQLite state has conflicting plugin install metadata for: ${merged.conflicts.join(", ")}`]
                };
        }
        ...
        // archiveLegacyInstalledPluginIndex(...) only reached if merged.conflicts.length === 0
}

Because brave is always in conflicts, this function always returns early with the warning and never reaches archiveLegacyInstalledPluginIndex (the only code path that renames installs.json → installs.json.migrated). The same legacy file is therefore re-read and re-conflicts on every future doctor check.

7. --refresh is a no-op for this issue

$ openclaw plugins registry --refresh

OpenClaw 2026.6.6 (8c802aa) — Making 'I'll automate that later' happen now.

◇  Doctor warnings ─────────────────────────────────────────────────────────────────────────╮
│  - Left plugin install index in place because shared SQLite state has conflicting plugin  │
│    install metadata for: brave                                                            │
├───────────────────────────────────────────────────────────────────────────────────────────╯
07:45:44 [state-migrations] Legacy state migration warnings:
- Left plugin install index in place because shared SQLite state has conflicting plugin install metadata for: brave

◇  Doctor warnings ─────────────────────────────────────────────────────────────────────────╮
│  - Left plugin install index in place because shared SQLite state has conflicting plugin  │
│    install metadata for: brave                                                            │
├───────────────────────────────────────────────────────────────────────────────────────────╯
Plugin registry refreshed: 69/96 enabled plugins indexed.

policy_hash before and after: 908f88d3d9108a63ffc5c6aa1905a7969227a8f3699f5db60ae2d334a0d27643 (unchanged). generated_at_ms/updated_at_ms: 1781335238233/1781335238249 → 1781394344419/1781394344443 (bumped, confirming refresh ran, but the index content relevant to this conflict did not change).

8. Ruled-out alternative explanations


Only one installed_plugin_index record exists (not multiple conflicting indexes).
Only one brave plugin install directory exists on disk (~/.openclaw/npm/projects/openclaw-brave-plugin-11fe9e3aa3) — no stale old/new version directories coexisting.
plugin_state_entries (current openclaw.sqlite) and the legacy pre-migration plugin-state/state.sqlite.migrated both contain zero brave-related rows.
diagnostics_json in installed_plugin_index contains no brave-related entries.

Impact and severity

Affected: based on the code logic, any installation that (a) has a legacy ~/.openclaw/plugins/installs.json generated before the npm plugin-install layout changed from flat (npm/node_modules/) to per-project (npm/projects//node_modules/), and (b) has at least one plugin (here, brave) whose installPath therefore differs between the legacy file and the current index.
Severity: Low / cosmetic for this reporter — memory indexing and plugin functionality are unaffected (Indexed: 1434/1434, Dirty: no, Embeddings: ready, brave plugin enabled and contracted as webSearchProviders: ["brave"] in the current index). Pure warning noise.
Frequency: Always — 100% reproduction on every doctor-triggering command, confirmed across multiple invocations and immediately after --refresh.
Consequence: Persistent, unactionable warning in doctor output on every version going forward, with no available remediation (the suggested --refresh does not work).

Additional information

Last known good: NOT_ENOUGH_INFO (the warning has been present since at least the 5.26 → 6.6 transition for this reporter; no version without it has been confirmed). First known bad / first observed: 5.26 upgrade (2026-05-27), persisting through 6.6 (2026-06-13).

Suggested fixes

  1. In legacyInstallRecordCoveredByCurrent, tolerate differences in installPath (and version/resolvedVersion/resolvedSpec/integrity/shasum) when source matches and the package identity (resolvedName/packageName) is the same — a changed install path for the same package across a layout migration should not be treated as a conflict.
  2. When every entry in merged.conflicts corresponds to a package that does exist in current (i.e. only install-location/version drift, not a package present in legacy but missing from current), proceed to archiveLegacyInstalledPluginIndex anyway.
  3. Add a CLI escape hatch (e.g. openclaw plugins registry --refresh --force-archive-legacy) to explicitly archive plugins/installs.json regardless of conflicts.

Happy to provide additional diagnostics (full installed_plugin_index JSON dump, migration_runs/migration_sources table contents, gateway logs around the 6.6 upgrade timestamp 2026-06-13T07:20:3x) on request.

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Normal backlog priority with limited blast radius.bugSomething isn't workingimpact:otherThis issue has meaningful maintainer-visible impact outside the owned taxonomy.

    Type

    No type

    Fields

    Priority

    None yet

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions