Bug type
Behavior bug (incorrect output/state without crash)
Beta release blocker
No
Summary
The macOS app still persists its config-corruption baseline to the retired legacy path ~/.openclaw/logs/config-health.json (rewritten atomically every ~2–3s while the app runs), while Node core ≥2026.6.x owns config-health state in the shared SQLite config_health_entries table — so migrateLegacyConfigHealth reports a conflict warning on every CLI/gateway bootstrap on any machine running the Mac app, and doctor --fix can never clear it.
Steps to reproduce
- Run OpenClaw.app (macOS app, 2026.6.10) alongside openclaw 2026.6.11 CLI/gateway (npm global) on macOS.
- Run any CLI command (e.g.
openclaw status) or start the gateway.
- Observe on every bootstrap:
[state-migrations] Legacy state migration warnings: - Left legacy config health state in place because 1 entry conflicts with shared SQLite state: /Users/…/.openclaw/logs/config-health.json
- Delete the JSON while the app runs → it reappears within ~3s (live inode churn observable via
stat).
Expected behavior
After core moved config-health to SQLite (src/config/io.health-state.ts), no component writes the legacy JSON path; the migration archives the file once and never warns again — per repo policy that legacy state files are migration debt with a single owner.
Actual behavior
The Swift writer still exists at repo HEAD: apps/macos/Sources/OpenClaw/OpenClawConfigFile.swift:8,480-511. The migration's conflict check (configHealthComparable, src/infra/state-migrations.ts:2090-2195; identical in installed dist state-migrations-CDz8cSvR.js:1706-1772) compares string-serialized tuples that can never match across the two writers: Foundation JSONSerialization key order + escaped slashes, Swift ctimeMs=creationDate vs Node ctime, rolling observedAt refreshed every write, and the Swift side never writes lastPromotedGood. Substantively both stores agree (identical config SHA-256 / bytes / inode) — a pure serialization/ownership mismatch, permanent while the app runs.
OpenClaw version
CLI/gateway 2026.6.11 (e085fa1); Mac app 2026.6.10; Swift writer confirmed present at repo HEAD (2026-06-30)
Operating system
macOS 15.6 (Darwin 24.6.0)
Install method
npm global (Homebrew node) + Mac app
Model
n/a — state-migration bug, model-independent
Provider / routing chain
n/a
Logs
[state-migrations] Legacy state migration warnings:
- Left legacy config health state in place because 1 entry conflicts with shared SQLite state: /Users/zach/.openclaw/logs/config-health.json
Evidence the Mac app is the writer: JSON exhibits Foundation formatting (" : " separators, \/ escapes, sorted keys); the string config-health.json is present in /Applications/OpenClaw.app/Contents/MacOS/OpenClaw; file mtime/inode churn every ~3s while the app runs and stops when it quits.
Impact and severity
Additional information
Workaround: quit the Mac app and delete/rename the JSON — the warning clears until the app relaunches. Suggested fix direction: the Mac app should stop persisting the baseline to the legacy path (or write via gateway/SQLite), letting the existing migration archive the file once.
Bug type
Behavior bug (incorrect output/state without crash)
Beta release blocker
No
Summary
The macOS app still persists its config-corruption baseline to the retired legacy path
~/.openclaw/logs/config-health.json(rewritten atomically every ~2–3s while the app runs), while Node core ≥2026.6.x owns config-health state in the shared SQLiteconfig_health_entriestable — somigrateLegacyConfigHealthreports a conflict warning on every CLI/gateway bootstrap on any machine running the Mac app, anddoctor --fixcan never clear it.Steps to reproduce
openclaw status) or start the gateway.[state-migrations] Legacy state migration warnings: - Left legacy config health state in place because 1 entry conflicts with shared SQLite state: /Users/…/.openclaw/logs/config-health.jsonstat).Expected behavior
After core moved config-health to SQLite (
src/config/io.health-state.ts), no component writes the legacy JSON path; the migration archives the file once and never warns again — per repo policy that legacy state files are migration debt with a single owner.Actual behavior
The Swift writer still exists at repo HEAD:
apps/macos/Sources/OpenClaw/OpenClawConfigFile.swift:8,480-511. The migration's conflict check (configHealthComparable,src/infra/state-migrations.ts:2090-2195; identical in installed diststate-migrations-CDz8cSvR.js:1706-1772) compares string-serialized tuples that can never match across the two writers: FoundationJSONSerializationkey order + escaped slashes, SwiftctimeMs=creationDate vs Nodectime, rollingobservedAtrefreshed every write, and the Swift side never writeslastPromotedGood. Substantively both stores agree (identical config SHA-256 / bytes / inode) — a pure serialization/ownership mismatch, permanent while the app runs.OpenClaw version
CLI/gateway 2026.6.11 (e085fa1); Mac app 2026.6.10; Swift writer confirmed present at repo HEAD (2026-06-30)
Operating system
macOS 15.6 (Darwin 24.6.0)
Install method
npm global (Homebrew node) + Mac app
Model
n/a — state-migration bug, model-independent
Provider / routing chain
n/a
Logs
[state-migrations] Legacy state migration warnings: - Left legacy config health state in place because 1 entry conflicts with shared SQLite state: /Users/zach/.openclaw/logs/config-health.jsonEvidence the Mac app is the writer: JSON exhibits Foundation formatting (
" : "separators,\/escapes, sorted keys); the stringconfig-health.jsonis present in/Applications/OpenClaw.app/Contents/MacOS/OpenClaw; file mtime/inode churn every ~3s while the app runs and stops when it quits.Impact and severity
openclaw doctor --fix. #90213 tracks the same recurring[state-migrations]symptom for other legacy sources (plugin install index, etc.), but this case has a distinct diagnosed cause — an active second writer — so filing separately per one-issue-per-report.Additional information
Workaround: quit the Mac app and delete/rename the JSON — the warning clears until the app relaunches. Suggested fix direction: the Mac app should stop persisting the baseline to the legacy path (or write via gateway/SQLite), letting the existing migration archive the file once.