feat(aw-sync): return and persist a SyncReport from each pass - #699
Conversation
Review guide (auto-posted)Structured warm-up for reviewers — what changed, what to run, where to look. Key files
Suggested verification
Known risks / watch points
Suggested review focus
Generated by |
|
@TimeToBuildBob Structure matches #695 point for point — Sequencing: this goes after the v0.14.x fixes (#698 → #697 → #678 → #693-minimal), not gating the cut. It is additive and larger (+743), so it gets a full read when it reaches the front rather than a structural pass now. Please keep it rebased as those land. One ask for when I read it properly: a test that a pass which finds zero peers produces a report saying so ( |
|
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #699 +/- ##
==========================================
+ Coverage 70.81% 78.80% +7.98%
==========================================
Files 51 73 +22
Lines 2916 7222 +4306
==========================================
+ Hits 2065 5691 +3626
- Misses 851 1531 +680 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
Added in 42946f6. A zero-peer pull now persists Will keep this rebased as #698 → #697 → #678 → #693-minimal land. Not gating the cut. Same commit also persists |
|
@TimeToBuildBob |
🤖 AI code reviewSafe to merge — 5 findings disposed (other; rejected; fixed in 91255d6)Updated after inline dispositions on finding threads — this is the current state; the verdict below is frozen at review time and is kept as the historical record of that pass. Finding disposition
This PR introduces a SyncReport type that is returned from sync_run, pull_all, push, and the JNI entry points, persisted to {data_dir}/aw-sync/last-sync-report.json, and printed by aw-sync status. It adds PeerOutcome::Skipped/Failed reporting for duplicate device_id and incompatible database versions, changes pull_discovery_warnings to accept Option<&str> for the local device id, and adds a close_opened_datastores helper for error paths. The diff also updates Cargo.toml to enable serde derive, adds tests for report round-tripping and event counts, and updates README/status output. Needs a look — P2 onlyConfidence 4/5 1 finding ·
|
| commit | score | findings | engine | when |
|---|---|---|---|---|
42946f6494d4 |
2/5 | 3 | llm | 2026-09-16 09:28 UTC |
49f0ce1726a2 |
2/5 | 3 | llm | 2026-09-16 12:44 UTC |
91255d66a998 |
4/5 | 3 | llm | 2026-09-16 13:22 UTC |
592c7b3ac0cb |
4/5 | 1 | llm | 2026-09-16 14:20 UTC |
a28035904771 |
3/5 | 2 | llm | 2026-09-16 15:28 UTC |
e4e92ecb53ab |
3/5 | 4 | llm | 2026-09-16 16:22 UTC |
Reviewed 2fab2f1452bf · openrouter/deepseek/deepseek-v4-flash-0731 · llm engine · 607s · about this reviewer
Maintainer commands
@TimeToBuildBob review (own line) — fresh review · @TimeToBuildBob fix — a worker acts on the findings. Once per comment; 👀 = received.
42946f6 to
49f0ce1
Compare
|
@greptileai review |
|
Rebased onto master (#698 landed): the Also fixed the two real findings from the latest review in 49f0ce1: the simple host-based path and the Android |
|
CI-green and mergeable (Greptile 5/5) — waiting only on a maintainer click. This PR is ready to merge, but the bot has pull-only access to this repo and can't self-merge — surfacing it here so it isn't lost. The monitoring loop will stop re-flagging it now that this note is posted. |
|
12:48 AI-review on
Zero-peer report test is still in (Erik's ask). Next rebase still waits on #700 / #678 / #697, not a merge click now. |
|
@greptileai review |
|
13:24 AI-review on
#700 landed; still waiting on #678 and #697 before the one rebase Erik asked for. Not mergeable yet ( |
|
15:38 in-band review on Pushed
Did not rebase. #678 and #697 are still open; one rebase after both land, as asked. In-band P1 "double-recorded failed peer" rejected: |
A successful pass used to return (). JNI, aw-sync status, and the daemon had nothing to report. SyncReport carries per-peer/per-bucket counts, including duplicate-device_id skips, and is persisted locally (not in the Syncthing folder). Closes ActivityWatch#695 Git-Session-Id: 94e08188-0a05-5fed-b8c0-fd327ae1f803
The ActivityWatch#682 failure mode was silence: a pass that found nobody logged warnings and threw them away. Capture them on the report so `peers: []` plus the diagnosis survive in last-sync-report.json. Also persist pull_all's aggregate on a later-peer failure, and use a unique temp file for the atomic write. Git-Session-Id: 2a2cd4fe-793a-5f03-bfbc-7da3c8ff9738
The simple host-based sync path and the Android syncBoth JNI entry both did `report.merge(push(...)?)` — the `?` discarded the merged pull report when push returned Err, so last-sync-report.json (and the JNI error payload) reflected only the push-phase report from inside sync_run. Persist the pull-phase result with a push warning before propagating. Rebased onto master (ActivityWatch#698 landed); conflict in sync_run resolved by keeping maybe_setup_local_remote + selection-based remote discovery. Git-Session-Id: 2a2cd4fe-793a-5f03-bfbc-7da3c8ff9738
The inner sync_run push-error path finished and persisted without a warning, so last-sync-report.json looked like a no-op success. Same string the simple-path and JNI callers already used. Git-Session-Id: 01a0aa45-67f2-7c32-9c50-447c12587192
Daemon and JNI log this string. A push abort already lived in `warnings` and the Display impl, but summary_message still said "Pushed 0 new events" — a success-like line for a failed pass. Git-Session-Id: 01a0aa92-68eb-7062-9ad7-aca64833f8b1
…nings
pull_all passed "" into pull_discovery_warnings, which wrapped it as
Some(""). That does not unclassify entries: leftover 2-level own staging
became a Peer and showed up as "peer db(s) that pull did not select".
Pass None instead, and skip 2-level leftovers from the missed-peer
warning when the local id is unknown. Do not call get_info() here —
reqwest's timeout is 120s; empty-dir diagnosis must stay a filesystem
check (ActivityWatch#682).
Git-Session-Id: 01a0aabd-c1af-70b2-807a-abd6ce4640cd
Datastore::close() stops the sqlite worker thread. The success path already did that; the new persist-then-return error paths skipped it, so a long-lived daemon could leak connections across failed passes. Git-Session-Id: 01a0aabd-c1af-70b2-807a-abd6ce4640cd
a280359 to
f8374cd
Compare
|
@greptileai review |
|
Rebased onto master (f8374cd) after #700/#697 landed. The sync.rs conflict was the read-only peer-open loop: |
…cReport open_peer_datastore returned Ok(None) on a user_version mismatch, so a peer rejected for an incompatible schema silently vanished from the report — an all-incompatible pass looked like a clean empty one in status and JNI (Greptile P1 on ActivityWatch#699). The mismatch reason is now carried back and each incompatible peer is recorded as skipped. Git-Session-Id: b1d605fd-5f81-5e6b-9293-7353ee952e7c
|
@greptileai review |
Simple host-based sync merges per-host pull reports. A later host's `?` dropped earlier hosts from last-sync-report.json, leaving only the failing host's inner persist. Catch, record, persist the aggregate, then propagate — same contract as the push-failure path. Git-Session-Id: ee496982-751b-50ab-a42d-742b7c76cf44
|
16:41 AI-review on
|
|
Local re-review on Finding threads all carry replies now; leaving resolution to the sweep on this head. |
|
Merge verified, not just "merged": full CI is green on the merge commit Downstream consumer is already wired: Deliberately unchanged, as scoped here: a per-peer failure still aborts the pass. That remainder is tracked at No further action needed from me on this PR. |
Rebased onto post-ActivityWatch#699/ActivityWatch#678 master (SyncReport/PeerReport world): open and pull failures per peer are now recorded on the report and skipped; only a total failure (every discovered peer failed) is Err, so one unreadable or failing peer no longer aborts the whole pass (ActivityWatch#688). Co-Authored-By: Bob Git-Session-Id: 2d23d1d2-e3df-5c27-97f3-ef94f342ad09
Rebased onto post-ActivityWatch#699/ActivityWatch#678 master (SyncReport/PeerReport world): open and pull failures per peer are now recorded on the report and skipped; only a total failure (every discovered peer failed) is Err, so one unreadable or failing peer no longer aborts the whole pass (ActivityWatch#688). Co-Authored-By: Bob Git-Session-Id: 2d23d1d2-e3df-5c27-97f3-ef94f342ad09
…ass (#703) * fix(aw-sync): skip broken peers in sync_run instead of aborting the pass Rebased onto post-#699/#678 master (SyncReport/PeerReport world): open and pull failures per peer are now recorded on the report and skipped; only a total failure (every discovered peer failed) is Err, so one unreadable or failing peer no longer aborts the whole pass (#688). Co-Authored-By: Bob Git-Session-Id: 2d23d1d2-e3df-5c27-97f3-ef94f342ad09 * test(aw-sync): make version-skip test hermetic against schema bumps Use NEWEST_DB_VERSION + 1000 instead of hardcoded 4 so the test cannot go stale when the supported schema version reaches the hardcoded value. Addresses our AI review P2 (fp 4bf1d7a1ac8b). Git-Session-Id: 515fe2f5-05bc-5268-82e6-bac7396140ff * fix(aw-sync): only open peer datastores for Pull/Both passes Push-only passes never read peer datastores, so opening them there was wasted work and made an all-unreadable-peer folder abort the pass before anything was pushed. Gate open_peer_datastores on Pull/Both; push mode now proceeds regardless of peer database state. Closes the Greptile P1 on 9b49d4a and the push-mode visibility P1 from the in-band review (with the gate, peer opens only ever run with record_peers=true, so incompatible peers are always on the report). Git-Session-Id: 63c563f9-1bf6-5fa9-a53b-c51caa2275d0
* feat(sync): record and show what each sync pass actually did SyncStatus was a timestamp and a boolean, so a pass that transferred a million events and one that transferred nothing both rendered as "succeeded" (#274). aw-sync returns a SyncReport across the JNI boundary now (ActivityWatch/aw-server-rust#699); the app was parsing only success/message/error out of it and discarding the aggregates. - SyncStatus carries events pulled/pushed, peers imported/skipped/failed, and warnings, with a hasReport marker so a run recorded by an older native lib still renders as before rather than as "pulled 0, pushed 0". - SyncStatus.fromJniResponse parses the payload in one place and never throws; a missing error field no longer propagates an exception. - Status persistence moved into performSyncAsync, the single choke point all four sync entry points pass through. Only the full-sync path used to record a status, so manual pull/push runs left the settings line stale. - formatSyncStatus appends the facts line and any warnings. Bumps the aw-server-rust submodule to pick up #699 (plus #705, whose port parameter the Kotlin declaration already matches, and #704 docs only). Git-Session-Id: 4b0fca5c-13ca-53f7-9910-98381b800dd3 * fix(sync): keep the native sync report when SAF mirroring fails The success status (with SyncReport counts/warnings) is persisted before SAF mirroring runs for full syncs. If mirroring then threw, the catch path replaced the persisted status with a report-less failure, erasing what the pass actually did. Keep the native status and record the mirror failure on top of it, preserving counts and warnings. Git-Session-Id: 029e7f5e-0490-56c7-ad5f-5d0a05d90ea0 * chore(deps): bump aw-server-rust to 3d3b9dd (#678) Pins the submodule at #678 (reconcile owner-originated event edits), which landed six minutes after the previous pin 37aa0b0 and is directly relevant to the sync reporting this PR consumes. Git-Session-Id: 38fdae10-a8ad-5df2-bf53-99533d65f74c * fix(sync): persist full-sync status only after SAF mirroring completes Greptile P1 (thread 4034960583): for a full sync with a configured SAF directory, the success status was persisted before mirroring finished, so the settings UI could show a completed sync while the mirror was still running. The persist now happens after the mirror for mirroring operations; non-mirroring operations keep the immediate persist. The catch-path report retention is unchanged. Git-Session-Id: 93c0a634-35df-50c7-a1d8-36d5fa94d03c * fix(sync): cap warnings after blank-filtering so trailing warnings survive A payload whose first five warning entries normalize to blank would drop any later meaningful warning. Filter blanks first, then cap. Git-Session-Id: 3794dab5-f699-5115-ade7-bcdd99be743d * fix(sync): key hasReport on any report field so push-only passes render counts Git-Session-Id: 3794dab5-f699-5115-ade7-bcdd99be743d * fix(sync): never let status persistence suppress the sync callback performSyncAsync posted its completion callback only after AWPreferences.setLastSyncStatus() returned. If persistence threw — in the try path or, worse, in the catch path's own persist — the callback was never posted. Callers use it to clear syncInFlight, so a single persistence failure would leave every later sync rejected as "already in flight" and the UI waiting. Route both persist calls through persistSyncStatus(), which logs and swallows the failure. A sync whose status could not be stored is not a failed sync, and the callback contract must not depend on storage health. Git-Session-Id: 6b9bee26-4b2b-533a-bb4e-97220cf3780c * fix(sync): key hasReport on count fields only, not warnings The marker decides whether the renderer appends the counts line. Including `warnings` in the key list let a payload that carried warnings but no count fields mark itself as report-bearing, so formatSyncStatus would render "pulled 0, pushed 0" — inventing a no-op pass out of a report that never supplied numbers. A pass that reports counts still marks itself through those keys, so push-only responses (events_pushed + peer counts, no events_pulled) are unaffected. Git-Session-Id: 6b9bee26-4b2b-533a-bb4e-97220cf3780c
Summary
A sync pass used to return
(). JNI always said"Successfully pulled from all hosts", the daemon had no status except dying, andaw-sync statuscould only describe the folder, not the last pass. That is why a daemon could run 102 empty cycles with nothing to look at (#682 / #684).This returns a
SyncReportfromsync_run/pull_all/push, persists the latest one locally (not in the Syncthing folder), and surfaces it in three places:aw-sync statusprints the last pass (peers imported/skipped/failed, event counts){success, message}object, plusevents_pulled,peers_skipped, etc. — Sync status is a timestamp and a boolean — the model cannot answer what synced, when next, or from whom aw-android#274 becomes a rendering jobstatuscan show what the last pass did even if the process has since exitedPeerOutcome::Skippedis how the duplicate-device_iddedupe from #686 reports itself, instead of a log line.Closes #695.
Persistence
{data_dir}/aw-sync/last-sync-report.json(override withAW_SYNC_LAST_REPORT). Atomic write. A persist failure is a warning, not a failed pass.Out of scope, as specified: per-device folder metadata ("which device is this peer", "when did it last push") — that is the manifest in ActivityWatch/activitywatch#302 / #691.
Does not change error policy: a per-peer failure still aborts the pass (#688). The partial report is persisted on that path so the failure is visible afterwards.
Test plan
cargo test -p aw-sync --lib --bins --tests(40 passed)cargo clippy --workspace -- -D warnings