fix(doctor): recover legacy cron archive across devices#98217
Conversation
|
Codex review: needs maintainer review before merge. Reviewed July 1, 2026, 1:33 AM ET / 05:33 UTC. Summary PR surface: Source +562, Tests +556. Total +1118 across 7 files. Reproducibility: yes. from source: current main catches legacy cron archive rename failures and still emits the migration success note, matching the repeated doctor --fix loop described in the PR. I did not run a live repro in this read-only review, but the PR includes real-kernel and Crabbox proof. Review metrics: 1 noteworthy metric.
Stored data model Merge readiness Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch. Rank-up moves:
Risk before merge
Maintainer options:
Next step before merge
Security Review detailsBest possible solution: Keep runtime cron storage SQLite-only and let doctor own legacy JSON cleanup with explicit archive outcomes, durable receipts, rollback coverage, and maintainer-accepted upgrade semantics. Do we have a high-confidence way to reproduce the issue? Yes, from source: current main catches legacy cron archive rename failures and still emits the migration success note, matching the repeated doctor --fix loop described in the PR. I did not run a live repro in this read-only review, but the PR includes real-kernel and Crabbox proof. Is this the best way to solve the issue? Yes, subject to maintainer acceptance of the compatibility-sensitive cleanup semantics. The fix belongs in the doctor legacy cron migration path and preserves the repository rule that runtime cron storage remains SQLite-only. AGENTS.md: found and applied where relevant. Codex review notes: model internal, reasoning high; reviewed against c230ab3c924e. Label changesLabel justifications:
Evidence reviewedPR surface: Source +562, Tests +556. Total +1118 across 7 files. View PR surface stats
What I checked:
Likely related people:
What the crustacean ranks mean
Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics. How this review workflow works
|
Dependency graph guard clearedThis PR no longer has blocked dependency graph changes. A future dependency graph change requires a fresh
|
|
/allow-dependencies-change exact head 11e77bf; GraphQL parent artifact only The GraphQL-authored commit retains the contributor head The real |
|
Maintainer hardening is complete on exact head What changed:
Proof:
The earlier broad |
|
/allow-dependencies-change exact head e504c45; old GraphQL ancestry artifact only Remote tree
The dependency-guard failure is solely caused by the old GraphQL ancestry merge base |
|
Merged via squash.
|
What Problem This Solves
Fixes an issue where self-hosters running
openclaw doctor --fixsee "Cron store migrated to SQLite" reported as finished, yet the legacycron/jobs.json(andcron/jobs-state.json) file is never archived — so every lateropenclaw doctorrun re-detects the same legacy storage and re-offers the migration. This happens when archiving the legacy file fails: the rename used to swallow every error, so a cross-device move (EXDEV, the common case when the cron store sits on a Docker bind mount) or any other rename failure silently no-op'd while doctor still claimed success.Why This Change Was Made
archiveLegacyCronFilekept the fastfs.renamepath, but added a copy+unlink fallback for cross-device (EXDEV) moves so the legacy file is actually archived, and now returns a closed result instead of discarding errors.archiveLegacyCronStoreForMigrationaggregates per-file outcomes, and doctor reports honestly: the existing "Cron store migrated to SQLite" success only when the legacy files are archived, otherwise a warning naming the leftover file, the reason, and theopenclaw doctor --fixretry. The scope is deliberately narrow — recover the archive failure and stop the false success. SQLite migration semantics and scheduler behavior are unchanged; no new config or runtime surfaces.User Impact
doctor --fixno longer loops on the same migration every run.<path>:<reason>. Remove it manually or rerunopenclaw doctor --fixto retry." — instead of falsely reporting the migration as done.Evidence
Base
upstream/main6cb82eaab8. Focused suites, types, lint, and format all green, plus two real-kernel captures: (A) a real cross-deviceEXDEVrename proving the copy+unlink recovery mechanism, and (B) the real exported archive function returning an honest failure on a real kernel rename error. Both run against the real OS with no mocks; the production wrapper'sEXDEVbranch itself is covered by the deterministic unit tests below.(A) Real cross-device
EXDEV+ copy+unlink recovery (real kernel, no mocks). Genuinefs.renamefrom tmpfs (/dev/shm) to ext4 returns the realEXDEVerrno that the code branches on; the same copy+unlink the fix uses then recovers the move:(B) Real production function against a bind-mounted store file (real kernel
EBUSY, no mocks). This exercises the honest-failure half on the real code path. The legacy store file is turned into its own mountpoint withmount --bindinside an unprivileged user+mount namespace — the same kernel primitive Docker uses for a single-file-v host/jobs.json:container/cron/jobs.jsonbind mount. Renaming a bind-mounted file to a sibling returnsEBUSY, so the unmodified exportedarchiveLegacyCronStoreForMigration(run vianode --import tsx) gets a genuine kernel rename failure and reports it instead of claiming success — the legacy file remains, nothing is mislabeled as migrated:EBUSYhere (bind-mounted file) andEXDEV(cross-device / Docker Desktop FUSE) are both real archive-failure modes; the fix recoversEXDEVvia copy+unlink and reports everything else honestly rather than swallowing it.Deterministic unit tests (
src/commands/doctor/cron/index.test.ts) drive the realmaybeRepairLegacyCronStorerepair path, spying onlyfs.rename/fs.copyFileto inject the cross-device failures:falls back to copy+unlink when renaming the legacy cron store fails with EXDEV— archive completes via copy+unlink, original removed, success reported, and a second doctor pass no longer re-detects the store (loop broken).warns honestly without a false success when archiving the legacy cron store fails entirely— renameEXDEV+ copyEACCES; legacy file remains, a warning surfaces, no "Cron store migrated to SQLite" success note.archives the legacy cron store and its state file on a successful rename— rename happy path unchanged; both files archived, no warning.A same-directory
EXDEV(what Docker Desktop's virtiofs/gRPC-FUSE bind mounts return for in-place rename) needs a FUSE/virtiofs mount that this sandbox cannot provide (no FUSE dev headers). The deterministic injected-EXDEVtest is therefore the strongest available proof that the production wrapper takes the copy+unlink branch; capture (A) independently proves that branch's mechanism against the real kernel, and capture (B) proves the honest-failure half on the real exported function with a real kernel rename error. Together they cover every link without overclaiming a live FUSE reproduction.