feat(aw-sync): add dedupe subcommand for pre-#713 -synced-from- duplicates - #718
Conversation
…d-from- duplicates aw-sync sync/daemon pull passes before ActivityWatch#711/ActivityWatch#713 re-imported the resume-boundary event on every pass, so -synced-from- buckets that pulled before that fix carry hundreds of exact-duplicate copies of the same event. Add `aw-sync dedupe [--bucket ...] [--dry-run]`: for every -synced-from- bucket (optionally filtered), collapse events with identical (timestamp, duration, data) down to the lowest-id (first-imported) copy, reporting counts per bucket. Never touches first-hand (non-synced) buckets. Android staging db cleanup (the phone's own test.db before push) is a separate follow-up on the aw-android side. Refs ActivityWatch#717 Git-Session-Id: eb7a54b0-3d37-51f0-acba-6c4a02daea1e
|
|
@TimeToBuildBob Correctness is right: only Two practical asks before this runs on a real instance:
Document the one caveat in the README line: over HTTP timestamps are ms-precision, so two genuinely distinct events with identical data in the same millisecond would be collapsed — practically nonexistent, but this command deletes, so say it. LGTM with those. I will run it on erb-m2 once Erik says so; the dry-run numbers above are the before-picture. |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #718 +/- ##
==========================================
+ Coverage 70.81% 79.71% +8.89%
==========================================
Files 51 75 +24
Lines 2916 8325 +5409
==========================================
+ Hits 2065 6636 +4571
- Misses 851 1689 +838 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
@TimeToBuildBob address |
…ess log, README caveat - Replace unbounded get_events with paginated fetch (PAGE_SIZE=5000, boundary-safe same-timestamp pop) so large buckets (1.5M events) don't OOM or hang the dry-run - Log per-bucket deletion progress at start and every 10k deletes - Add --help note that large cleanups take a long time (individual HTTP deletes) - README: add caveat that HTTP timestamp precision is ms, not ns — two genuinely distinct events with identical (timestamp-ms, duration, data) within the same millisecond would be collapsed (practically impossible for real AW data) - Fix test assertion: paginated test expected [10,11] but lowest-id is keeper, so duplicates are [11,12]; fix comments to match Closes the two practical asks from ErikBjare's review on ActivityWatch#718. Git-Session-Id: c85b
|
Addressed in 4c40f27:
Also fixed a test assertion bug caught during the test run: |
|
@greptileai review |
🤖 AI code reviewSafe to merge — 1 P1 disposed (fixed in de0c8a5)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
Adds a Not safe to merge — 1 P1 openConfidence 3/5
2 findings · ❌ 1 P1 ·
|
| commit | score | findings | engine | when |
|---|---|---|---|---|
4c40f27d6781 |
3/5 | 1 | llm | 2026-09-18 18:02 UTC |
Reviewed 262e3569ee89 · openrouter/deepseek/deepseek-v4-flash-0731 · llm engine · 685s · about this reviewer
Maintainer commands
@TimeToBuildBob review (own line) — fresh review · @TimeToBuildBob fix — a worker acts on the findings. Once per comment; 👀 = received.
…lipping P1 — Boundary Fetch Changes Durations (Greptile #4049200869): When the pagination boundary fell mid-timestamp-tie, re-fetching those events with end=boundary_ts caused the server to clip their durations to zero (the server clips event end-times at the query boundary). This made two distinct events with the same timestamp but different durations appear identical, risking permanent deletion of a non-duplicate event. Fix: pop boundary events into a `carry` buffer and advance fetch_end to boundary_ts - 1ns. The carried events (with their true, unclipped durations) are prepended to the next iteration's batch alongside newly fetched older events. No server re-fetch with a clipping end-time is needed. P1 — Large Timestamp Ties Are Skipped (Greptile #4049200874): When an entire page shares one timestamp, remaining events at that timestamp beyond PAGE_SIZE can't be fetched without offset support. Added a warning so operators know a second pass may be needed in this (unlikely) case. P2 — Timestamp Precision Warning Was Incorrect (Greptile #4049200879): The README caveat incorrectly stated the HTTP API returns ms-precision timestamps. The server retains ns precision through storage, serialization and deserialization. Updated the caveat to describe the actual dedup key (exact timestamp + duration + data match) without the inaccurate ms claim. Also: update fake_fetch_page in tests to simulate server-side duration clipping so the regression test can catch this class of bug. New test `carry_preserves_duration_across_page_boundary` exercises the P1 fix directly — two events with the same timestamp but different durations must not be flagged as duplicates across a page boundary. Git-Session-Id: 2515863d-bf36-563a-a4a0-130e48543412
|
@greptileai review |
Convergence adjudication — 2026-09-18Head: Fixed
Remaining (non-blocking, dismissed)
CIAll checks green: Android ✅, macOS ✅, ubuntu ✅, windows ✅, clippy ✅, format ✅, Greptile CI check ✅, codecov ✅. Domain riskThe dedupe command is destructive and irreversible — events deleted cannot be recovered without re-syncing from the source device. The Convergence
Not requesting auto-merge — this is a destructive tool targeting real data; maintainer review of the final code is warranted. |
…ies can't split duplicate groups A full page cut through a duplicate run left the remainder unfetched (next page started at cut_ts - 1ns), so runs straddling a page edge were silently under-cleaned. Cut pages at the PAGE_SIZE-th event's timestamp and grow the fetch limit until an older event proves the run complete; also covers runs larger than a page. The unit-test fake now follows the real server ordering/limit/clipping. Refusal for unnamed buckets moves after the empty-target check so a fresh install gets a clean no-op. Git-Session-Id: 6f002d6d-0117-5806-a4c0-de3cc45f0101
|
Follow-up on "address" — the two asks (paged fetch, progress/runtime, README caveat) landed in 4c40f27, but re-reading the pagination against your real numbers I found my last adjudication was wrong, so here is the actual fix. Correction. I called the pagination undercount resolved by the carry-buffer commit (262e356) and dismissed the tie case as ">5000 events at one nanosecond". Both wrong. Any duplicate run that straddles a page edge was cut: only the part inside the page was carried, and the next fetch started at Fix (de0c8a5). A page is now cut at the timestamp of its Also: the "refusing to delete without Tests. The test fake previously sorted by id and never modelled the real straddle. It now follows the server contract ( Still worth your eyes before the real delete run: it's destructive, and the durations of events crossing a page's |
Closes item 1 of #717.
What
Adds
aw-sync dedupe [--bucket ...] [--dry-run]: for every-synced-from-bucket (destination buckets pull created), collapse events that are exact
duplicates — same
(timestamp, duration, data), usingEvent's ownPartialEq— down to the lowest-id (first-imported) copy. Reportsper-bucket counts. Never touches a host's own first-hand buckets (only
-synced-from-ids are ever candidates — same markersync::is_synced_bucketalready uses elsewhere).
Why
#711/#713 stopped new pull passes from re-importing the resume-boundary
event, but didn't touch what already accumulated on destinations that
pulled before that fix (e.g. 550 copies of one stopwatch event on erb-m2).
This is the one-off cleanup for that legacy state, over the local aw-server
HTTP API (
get_events/delete_event), so it works against a runningserver without touching the sqlite files directly.
Not in this PR
Item 2 of #717 (resetting/deduping the Android staging db,
test.db,before push) is aw-android-side work and out of scope here — noted in the
issue.
Testing
cargo test -p aw-sync --features cli dedupe— 3 new unit tests on thepure grouping logic (
find_duplicate_ids): keeps lowest id per group,unaffected by input order (get_events returns newest-first), no
duplicates → no-op.
cargo clippy -p aw-sync --features cli --all-targets— clean (no newwarnings).
cargo fmt -p aw-sync -- --check— clean.