Remediation mode restores nothing, and Fetch and Rebuild Everything fail while it is active
Split from #1200 at your request. That issue now covers only the queued results which do not resume at readiness; this one covers remediation mode: reflection within the modification-time limit, Fetch and Rebuild behaviour, and the conflict-resolution caveat.
This is a code-level report, based on reading the source and on unit tests, so it does not follow the issue template: there is no device debug info or LiveSync report to attach.
Revisions
- Found in a security fork of LiveSync 1.0.21 (upstream base f5f7aab11f03f62c6946d2fa296c50bb5df5b2a4) with Commonlib 0.1.19 (upstream base e8beaf017de243ee3d4beb998905a51de30ebb44), and confirmed there with unit tests.
- Re-checked against upstream
mainon 2026-09-17: LiveSync c60323e1 (1.0.29) and Commonlib 26efbb08 (0.1.26). The code paths below are the current ones. - Not reproduced on a released 1.0.28 or 1.0.29 build.
1. The mode restores nothing
While maxMTimeForReflectEvents is set, canProceedScan refuses every reconciliation scan (livesync-commonlib/src/serviceFeatures/offlineScanner.ts:695). That is intended. Readiness, however, depends on that scan: prepareDatabaseForUse treats the refusal as a failure and returns before markIsReady() (prepareDatabaseForUse.ts:57 and :81), so the application stays unready for as long as the mode is on.
Since 1d2077d3 the result processor waits for readiness (ReplicateResultProcessor.isSuspended, src/serviceFeatures/replication/ReplicateResultProcessor.ts:118). In the mode it therefore applies no received document at all, not even one modified before the limit, which is what the mode exists for. Before that commit the referenced-not-called check made the processor run regardless, which is why the mode appeared to work.
2. Fetch and Rebuild Everything fail in the mode
Both request the refused scan and treat each refusal as a failure, after the local database has already been emptied:
performFetchLocal→prepareLocalDatabaseForRebuildthrowsThe Vault could not be scanned for rebuild preparation.(Rebuilder.ts:341), aftersuspendReflectingDatabasehas persisted the suspension andresetLocalDatabasehas emptied the database.finishRebuild→resumeReflectingDatabasereturns false (Rebuilder.ts:607), so the fetch ends unfinalised with reflection left suspended.performRebuildEverythingfails the same way, also after the reset.
So a fetch started to recover an earlier state, the reason the mode exists, cannot complete.
3. Simple Fetch bypasses the restriction (plug-in)
After $fetchLocalDBFast(false), askAndPerformFastSetupOnScheduledFetchAll calls synchroniseAllFilesBetweenDBandStorage directly (src/serviceFeatures/redFlag.simpleFetch.ts:222), so in remediation mode it runs the full bidirectional scan which canProceedScan is meant to prevent. Depending on the selected handling it stores Vault files in the local database, writes documents modified after the limit to the Vault (the scanner never reads the limit), or deletes local files. Nothing leaves the device, because replication requires readiness, but the local database ends up holding the state the mode is meant to replace. The direct call was introduced in 7d2ba1b0 to pass the scan modes; nothing there mentions remediation mode, so the bypass does not look intentional.
Why simply marking the application ready is not a safe fix
Readiness opens isReplicationReady, so replication starts. Applying a document over a local file with different content preserves that content as a conflict revision (preserveUnsyncedStorageAsConflict), which would then be sent. The optional storage-to-database features on onDatabaseInitialised also reconcile without any modification-time limit. StorageEventManager.appendQueue keeps storage events out (StorageEventManager.ts:156), but these paths are not covered by the mode itself. An earlier revision of our fix did make start-up skip the scan and continue to readiness; review rejected it for exactly these reasons.
What we did in the fork
In Commonlib (0.1.19-security.10, kimjansheden/livesync-commonlib@ae10b28f):
- both prevented scans are skipped in this mode instead of failing;
- the files in storage are no longer stored in the database before fetching, neither through
createAllChunksnorprepareLocalDatabaseForRebuild, since staging them would publish the state being replaced and would runonDatabaseInitialisedwith its unrestricted reconciliation; - finalisation persists the resumed reflection settings but does not mark readiness while the mode is active, so the host stays as restricted as it is during an ordinary start in the mode;
- Rebuild Everything is refused while the mode is active, before the local database is reset;
- one
isRemediationModeActivehelper replaces the copies of the limit check incanProceedScan,StorageEventManager.appendQueueand the Rebuilder.
Start-up is deliberately unchanged: prepareDatabaseForUse still fails on the refused scan, the host stays unready, and the Started in remediation Mode! notice still reaches the user.
In the plug-in (kimjansheden/obsidian-livesync@5f90c322 and @ddd27382):
- the result processor checks readiness first, so behaviour outside the mode is unchanged; while the limit is configured it no longer waits for a readiness the mode prevents, but it does wait for a usable local database, because a document applied while
resetLocalDatabasehas removed its chunks is dropped afterFailed to gather contentwithout a retry; parseDocumentChangestill enforces the limit for every document, and storage events are still refused, so local changes are not sent;- a scheduled fetch leaves Simple Fetch while the mode is active, clears a remembered Simple Fetch choice and continues into the detailed flow, which already handles the mode. Skipping only the scan was tried first and rejected in review: Simple Fetch runs under the
keep-on-failurepolicy, which suspendssuspendParseReplicationResultwhile fetching, so a remediation Simple Fetch from Object Storage would have restored nothing, deleted the flag and reported success.
Remaining caveat
When a restored file conflicts with a differing local copy, automatic resolution by the newest file can take back part of the restore. We have not addressed that.
Minor, same area
parseDocumentChange logs the skipped modification time as docMtime * 1000 (ReplicateResultProcessor.ts:424) while both values are already in milliseconds, so the logged time is wrong.
Regression tests
All of these exist in the fork and are runnable there; each of the first three Commonlib tests fails against the current Rebuilder.ts.
Commonlib, src/serviceModules/Rebuilder.unit.spec.ts at kimjansheden/livesync-commonlib@ae10b28f (base e8beaf01):
finalises a rebuild in remediation mode and keeps the host restrictedfetches in remediation mode without storing the current files of the Vault firstrefuses to rebuild in remediation mode before the local database is resetsrc/common/utils.content.unit.spec.ts:isRemediationModeActivefor a configured, absent, zero and negative limit
Plug-in, src/modules/core/ReplicateResultProcessor.unit.spec.ts at kimjansheden/obsidian-livesync@5f90c322 (base f5f7aab1):
applies documents in remediation mode, which never reports readinessholds documents in remediation mode while the local database is being rebuilt
Plug-in, src/serviceFeatures/redFlag.unit.spec.ts at kimjansheden/obsidian-livesync@ddd27382: the scheduled-fetch cases which leave Simple Fetch and clear the remembered choice while the mode is active.
The fork's paths differ from current upstream (src/modules/core/ rather than src/serviceFeatures/replication/), so the tests need porting, which I am happy to do.
Offer
I can open focused PRs for both repositories, in the order that suits you: Commonlib first for the Fetch and finalisation behaviour, then the plug-in for result application and Simple Fetch, with the PRs linked to each other and to this issue.
Source: vrtmrz/obsidian-livesync