Skip to content

aw-sync: daemon (the default subcommand) never pulls — two incompatible sync-folder layouts #682

Description

@ErikBjare

aw-sync ships two mutually incompatible sync-folder layouts, and the one used by the default daemon subcommand can never see remotes written by the other one.

Net effect: running bare aw-sync (which is what the bundled binary and aw-qt do) is push-only, into a directory nothing else reads. It silently never pulls.

The two layouts

code path used by writes scans
sync_wrapper::push / pull aw-sync sync (no advanced flags), aw-android (syncPush/syncPullAll/syncBoth via JNI) {sync_dir}/{hostname}/{device_id}/test.db {hostname}/{device_id}/*.db (3 levels)
sync::sync_run called directly aw-sync daemon — i.e. the default subcommand {sync_dir}/{device_id}/test.db {sync_dir}/*/*.db (2 levels)

find_remotes() in aw-sync/src/util.rs walks exactly two levels:

fs::read_dir(sync_directory)?
    .filter(|p| p.is_dir())                 // {sync_dir}/{hostname}
    .flat_map(|d| fs::read_dir(d).unwrap()) // {sync_dir}/{hostname}/{device_id}  <- a dir, not a .db
    .filter(|path| path.extension() == "db")

Real peer databases sit one level deeper, so the filter drops every one of them. find_remotes_nonlocal() returns an empty vec, and the pull loop in sync_run() iterates over nothing.

Meanwhile setup_local_remote(sync_spec.path, device_id) stages the local push at {sync_dir}/{device_id}/test.db — at the folder root, with no hostname level — where get_remotes() (which requires a subdirectory containing a .db) will not find it either. So a daemon-mode device is invisible to aw-sync sync peers and to Android, in both directions.

Reproduction / evidence

Setup: macOS desktop running the bundled aw-sync with no arguments (→ daemon), sync folder shared over Syncthing with an Android device on v0.14.2b1.

~/ActivityWatchSync/ after the Android device pushed successfully:

POCO F8 Ultra/41662faa-.../test.db      9.4 MB    <- 3-level, from aw-android
poco_f8_ultra/41662faa-.../test.db      273 MB    <- 3-level, from aw-android
erb-m2.localdomain/d7bc68e7-.../test.db 
d7bc68e7-.../test.db                    1.19 GB   <- 2-level, from the local daemon

102 consecutive daemon sync passes, ~/Library/Logs/activitywatch/aw-sync/aw-sync_2026-09-14T21-45-45+0200.log:

    102 Pulling...
    102 Pushing...
      0 Found N remote db files      <- never logged, not once

The Android buckets never appear locally. The only reason this is not more widely reported is that the failure is completely silent — Pulling... is logged unconditionally before the (empty) loop.

Two further consequences on the same machine:

  • The local host-layout staging copy at erb-m2.localdomain/d7bc68e7-.../test.db last received an event on 2025-01-22. Everything since has gone into the root-level 2-level db, so this desktop has not published anything its peers can read for ~8 months.
  • That root db is 1.19 GB and Syncthing replicates it to every device, where nothing reads it. It also still contains …-synced-from-… buckets (re-exported peer data) predating fix(sync): never re-sync buckets synced from another host #648 — that fix stopped new ones being created but nothing cleans up existing ones.

Suggested fix

Collapse to one layout. Cheapest correct change: have Commands::Daemon call sync_wrapper::pull_all + sync_wrapper::push per cycle, exactly as the Commands::Sync fallback branch does, instead of driving sync::sync_run against the sync root. That makes the daemon agree with aw-sync sync and with Android, and leaves sync_run as the per-directory primitive it already is.

If the 2-level {device_id}/ scheme is preferred long-term (it is arguably the better one — see the identity discussion in the linked issues), then it needs a migration for existing folders plus a matching change in aw-android, and get_remotes()/find_remotes() must be unified rather than left as two different walkers.

Either way find_remotes() and get_remotes() should share one implementation — the TODO: share logic with find_remotes and find_remotes_nonlocal already in the source is precisely this bug.

Related, same area

sync_wrapper::pull() calls sync_run with sync_spec.path = {sync_dir}/{host}, and sync_run unconditionally calls setup_local_remote(path, device_id) — including in SyncMode::Pull. So pulling from a peer creates an empty {sync_dir}/{peer_host}/{my_device_id}/test.db inside that peer's folder. On the machine above this produced a d7bc68e7-… directory inside erb-laptop2-arch/, tekla-air-m1/, erb-pn50/, steamdeck/ and others.

This breaks the "each device only writes to files in the sync folder they own" invariant the whole design rests on (stated in aw-sync/README.md), and is a plausible source of the test.sync-conflict-*.db files that show up in these folders. setup_local_remote should only run when the mode actually pushes.

cc @TimeToBuildBob

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions