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
- 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.
- 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.
- 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.
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:
07:29:47 [state-migrations] Legacy state migration warnings:
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
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
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.