Remediation mode restores nothing, and Fetch and Rebuild Everything fail while it is active

Author: kimjanshedenCreated Sep 17, 2026Updated Sep 17, 2026

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 main on 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:

  • performFetchLocalprepareLocalDatabaseForRebuild throws The Vault could not be scanned for rebuild preparation. (Rebuilder.ts:341), after suspendReflectingDatabase has persisted the suspension and resetLocalDatabase has emptied the database.
  • finishRebuildresumeReflectingDatabase returns false (Rebuilder.ts:607), so the fetch ends unfinalised with reflection left suspended.
  • performRebuildEverything fails 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 createAllChunks nor prepareLocalDatabaseForRebuild, since staging them would publish the state being replaced and would run onDatabaseInitialised with 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 isRemediationModeActive helper replaces the copies of the limit check in canProceedScan, StorageEventManager.appendQueue and 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 resetLocalDatabase has removed its chunks is dropped after Failed to gather content without a retry;
  • parseDocumentChange still 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-failure policy, which suspends suspendParseReplicationResult while 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 restricted
  • fetches in remediation mode without storing the current files of the Vault first
  • refuses to rebuild in remediation mode before the local database is reset
  • src/common/utils.content.unit.spec.ts: isRemediationModeActive for 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 readiness
  • holds 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