Keep unresolved conflicts visible after Not now, reopen them only on explicit requests, and clear stale warning and dialogue state when a replicated resolution arrives. Add typed provisional English messages, revision-tree regressions, specifications, and a focused Real Obsidian E2E.
Obsidian's mobile storage layer forwards mtime/ctime to Capacitor's
Filesystem.setTimes, whose native binding casts the value to a Java Long.
A non-integer (float) timestamp makes that cast throw (ClassCastException:
Double cannot be cast to Long), which crashes the app on launch as soon as
such a document is replicated in.
Float timestamps can reach the database from any client that stores
fs.Stats.mtimeMs without flooring. Coerce mtime/ctime to integer ms at the
Obsidian vault and storage adapter write boundary, so a float already
present in the mesh can't crash the app regardless of where it came from.
The truly central choke point is dbToStorage in livesync-commonlib; a
matching guard there would cover the CLI and webapp too. This change
protects the platform that actually crashes.
Claude-Session: https://claude.ai/code/session_0123E9jVQrsgu3zb82Csuwhi
CLIWatchAdapter._toNodeFile passed chokidar's raw stats.ctimeMs/mtimeMs
straight through. On Linux those carry sub-millisecond precision (e.g.
1778511180024.462), so watch-mode file changes wrote non-integer
timestamps into the database.
The earlier floor fix (3f7bb047) covered the scan/stat adapters but missed
this watch path, which is the one the daemon actually uses at runtime.
Mobile clients then crash on such a document because Capacitor's
Filesystem.setTimes casts the value to a Java Long (ClassCastException:
Double cannot be cast to Long).
Floor ctimeMs/mtimeMs here, with a null guard so a partial stat still
falls back to Date.now(). Adds a regression test that a fractional stat is
floored.
Claude-Session: https://claude.ai/code/session_0123E9jVQrsgu3zb82Csuwhi