You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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 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.
aw-syncships two mutually incompatible sync-folder layouts, and the one used by the defaultdaemonsubcommand 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
sync_wrapper::push/pullaw-sync sync(no advanced flags), aw-android (syncPush/syncPullAll/syncBothvia JNI){sync_dir}/{hostname}/{device_id}/test.db{hostname}/{device_id}/*.db(3 levels)sync::sync_runcalled directlyaw-sync daemon— i.e. the default subcommand{sync_dir}/{device_id}/test.db{sync_dir}/*/*.db(2 levels)find_remotes()inaw-sync/src/util.rswalks exactly two levels: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 insync_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 — whereget_remotes()(which requires a subdirectory containing a.db) will not find it either. So a daemon-mode device is invisible toaw-sync syncpeers and to Android, in both directions.Reproduction / evidence
Setup: macOS desktop running the bundled
aw-syncwith no arguments (→daemon), sync folder shared over Syncthing with an Android device on v0.14.2b1.~/ActivityWatchSync/after the Android device pushed successfully:102 consecutive daemon sync passes,
~/Library/Logs/activitywatch/aw-sync/aw-sync_2026-09-14T21-45-45+0200.log: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:
erb-m2.localdomain/d7bc68e7-.../test.dblast 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.…-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::Daemoncallsync_wrapper::pull_all+sync_wrapper::pushper cycle, exactly as theCommands::Syncfallback branch does, instead of drivingsync::sync_runagainst the sync root. That makes the daemon agree withaw-sync syncand with Android, and leavessync_runas 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, andget_remotes()/find_remotes()must be unified rather than left as two different walkers.Either way
find_remotes()andget_remotes()should share one implementation — theTODO: share logic with find_remotes and find_remotes_nonlocalalready in the source is precisely this bug.Related, same area
sync_wrapper::pull()callssync_runwithsync_spec.path = {sync_dir}/{host}, andsync_rununconditionally callssetup_local_remote(path, device_id)— including inSyncMode::Pull. So pulling from a peer creates an empty{sync_dir}/{peer_host}/{my_device_id}/test.dbinside that peer's folder. On the machine above this produced ad7bc68e7-…directory insideerb-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 thetest.sync-conflict-*.dbfiles that show up in these folders.setup_local_remoteshould only run when the mode actually pushes.cc @TimeToBuildBob