fix(aw-sync): sanitize whitespace hostnames on import; make per-bucket errors non-fatal - #697
Conversation
|
@TimeToBuildBob The legacy-ID lookup is right — existing unsanitized → existing sanitized → create sanitized is exactly the resolution order #692's correction asked for, Blocking: the sanitizer must match aw-android's, or this defers the fork rather than closing itThis PR sanitizes with aw-android's Trace what happens the day ActivityWatch/aw-android#272 lands and the phone migrates its bucket
That is the discussions#1373 symptom again, just moved to the Android migration date — and by then every desktop that ran this PR has a Fix is small and only possible before merge: use Android's exact algorithm (lowercase, Small: per-peer isolation is still missing, and step 3 depends on it
for remote in selected {
pull_db(client, &remote.hostname, &remote.path)?;
}This PR makes bucket-level errors non-fatal but a peer that fails to open still aborts the pass and skips every peer after it. Plan step 3 (#693 minimal: open peers read-only, skip on Small: sanitize on the hostname, not only the IDThe branch condition is With the first item fixed I have nothing else on this one. |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #697 +/- ##
==========================================
+ Coverage 70.81% 79.32% +8.50%
==========================================
Files 51 74 +23
Lines 2916 7821 +4905
==========================================
+ Hits 2065 6204 +4139
- Misses 851 1617 +766 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
CI-green and mergeable — 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. |
|
Addressed all three. Blocking — sanitizer now matches aw-android. Shared Per-peer isolation. Sanitize on hostname, not only ID. Guard is Pushed as 67eeee7. The earlier "waiting on a maintainer click" note is superseded — this needed the review items first. CI is running. |
|
@TimeToBuildBob review |
|
All three items confirmed in 67eeee7. CI is green on all platforms. Blocking — sanitizer matches aw-android: Per-peer isolation: Sanitize on hostname not only ID: guard is One trivial nit (not blocking): in Ready to merge. |
|
P1 from the AI review: a hostname like 7d6cfd9 refuses that pull; per-bucket skip keeps the rest of the pass. Test: P2 (Locale.ROOT vs Unicode lowercase) left as-is — ASCII is what CI running on 7d6cfd9. Still pull-only here; needs a maintainer click once green. |
|
@TimeToBuildBob Now |
|
@greptileai review |
|
7d6cfd9 to
8037907
Compare
|
Rebased onto master ( Folded into this one push as asked:
Did not restack onto #678: hunks still do not overlap ( |
|
@TimeToBuildBob Rebase confirmed — on current master, sanitizer/per-peer Required 1 — sanitize always, not only on whitespace (
|
|
@ErikBjare Done on 1. Sanitize always. Lookup stays raw ID → sanitized ID; create under the sanitized ID when it differs. Tests: 2. Total failure returns Err. Left the origin-collision warn for ActivityWatch/activitywatch#302 as specified. |
|
Verified on |
|
Acknowledged. Head is still Order remains #700 → #678 → this. Post-merge rebase preview (not pushed — stacking now would dump #700/#678 into this PR):
Once those two land I will rebase, fold the deferred tautological-assertion cleanup, and this is your click. Resolved the three Greptile threads (two fixed in |
🤖 AI code reviewThis PR adds hostname sanitization to aw-sync's bucket import path, making per-bucket and per-peer failures non-fatal, and updates tests accordingly. It introduces a Safe to merge — no P0/P1 findingsConfidence 5/5 ✅ No thread-worthy findings. Advisory notes follow; they are retained without opening review threads. 1 advisory finding (summary-only, not scored)These P2 guard, heuristic, trade-off, or documentation claims are retained for judgment without opening review threads.
The test How this was verified: Read the test and the sanitizer logic. Consensus: 2/3 passes agreed — pass 2 looked and disagreed Files changed (5) — the diff as I read it
Reviewed Maintainer commands
|
|
@greptileai review |
fcd851c to
8efcf9c
Compare
|
Rebased onto master over #700 ( Conflict was the previewed adjacent insert in Folded the deferred one-line: a skipped Local: #678 still open and CLEAN; pairwise this vs #678 was clean before, so it should stay mergeable if that lands first. Still pull-only — needs a maintainer click after CI. Did not re-trigger Greptile (your 14:09 request was on |
The supervised daemon walks remotes inside sync_run. A single unreadable peer used `?` and skipped every peer after it, then exited non-zero and burned aw-qt/aw-tauri's restart budget (ActivityWatch#688). Partial failure is now warn+continue. Total failure (every peer failed) is still Err so a down destination is not reported as success. Git-Session-Id: fcff5116-c5d0-5209-9804-33fc54c5b5a5 ActivityWatch#697 isolates host-layout pull_all; this is the daemon path (ActivityWatch#685 parked).
|
@TimeToBuildBob Resolve conflicts |
…t errors non-fatal Two issues block pulling peers whose bucket hostname contains whitespace (e.g. "POCO F8 Ultra" from Android devices before aw-android#272): 1. `get_or_create_sync_bucket` derived the destination ID from the raw hostname. On aw-server-rust destinations, `create_bucket` rejects new buckets with whitespace hostnames (ActivityWatch#658), so the first pull of such a peer returns HTTP 400, which propagated as `?` and aborted the whole sync pass (every subsequent peer also skipped). Fix: before creating a new bucket, sanitize whitespace → `_` in both the bucket ID and the hostname field. The legacy (unsanitized) ID is checked first so any existing pre-sanitization import is reused, avoiding the full re-import fork (#1373). 2. A single bucket failure with `?` in `sync_datastores` aborted the entire pass. All remaining peers were skipped. Blast radius: daemon returns Err → exits → supervisor restarts → same failure → budget exhausted → ActivityWatch#688. Fix: per-bucket errors in `sync_datastores` are now non-fatal (warn + continue). A broken or invalid bucket is skipped; healthy buckets still sync. Updated two tests that relied on the old fatal-error behavior. Fixes ActivityWatch#692. Related: ActivityWatch#658, ActivityWatch#685, ActivityWatch#688, ActivityWatch/aw-android#272. Git-Session-Id: c8bb
Whitespace-only replace ("POCO F8 Ultra" → "POCO_F8_Ultra") would fork
the destination the day aw-android#272 migrates the phone's hostname
column to poco_f8_ultra. Use Android's exact algorithm (lowercase,
[^a-z0-9_-]+ → _, trim _) via a shared sanitize_hostname() that must
stay byte-identical to DeviceHostname.kt.
Also:
- Trigger sanitization when the source hostname has whitespace even if
the derived ID is already clean ($aw.sync.origin can be sanitized
while bucket.hostname is not; create_bucket would 400 on the field).
- pull_all: per-peer warn+continue so a failed open does not abort the
pass and skip every peer after it (ActivityWatch#688 / ActivityWatch#693).
Git-Session-Id: 2097767
Android maps empty/punctuation-only names to the "unknown" sentinel. A hostname like " * " contains whitespace so the sanitizer runs, then becomes "unknown", and get_or_create would create -synced-from-unknown — mixing every such remote into one destination. The hostname=="unknown" guard in sync_datastores never sees this because the source hostname is not yet the sentinel. Refuse on pull; per-bucket warn+continue skips the junk bucket and a healthy sibling still syncs. Git-Session-Id: 2679931
…l error Always run DeviceHostname.kt sanitization, not only on whitespace, so PIXEL8 and erb-m2.localdomain land on the same IDs Android's migration will use. Lookup stays raw then sanitized; create under the sanitized ID when it differs. sync_datastores and pull_all still skip individual buckets/peers, but return Err when every attempt fails so a down destination is not reported as success. Git-Session-Id: 82e7d9e5-0c8a-58c0-b129-a46f96ef6953
A failed sync_one can leave dest with a partial chunk. The per-bucket warn+continue now names that, so a skipped bucket is not mistaken for an untouched skip. Git-Session-Id: pm-697-rebase-2026-09-16
8efcf9c to
bff7a2a
Compare
|
Rebased onto Ordering noted: #697 → #703 → #702. Still pull-only — ready for a maintainer click once CI is green. |
|
Re-verified on |
|
Closing the loop on this dispatch — the rebase ask was delivered on Post-merge verification (not previously on this thread):
No further action from Bob on this PR: the sanitizer / per-peer- |
…e-import after Android hostname migration Closes ActivityWatch#707. When both the raw-ID and sanitized-ID lookups miss in `get_or_create_sync_bucket`, fall back to scanning the destination for a `-synced-from-` bucket whose base ID matches and whose `$aw.sync.origin` sanitizes to the same value. This recovers the case where a desktop imported an Android peer before ActivityWatch#697 landed (`…-synced-from-POCO F8 Ultra` with `$aw.sync.origin = "POCO F8 Ultra"`), then the phone ran ActivityWatch/aw-android#273 and its staging hostname became `poco_f8_ultra` (sanitized, no `$aw.sync.origin` on first-hand buckets). The two direct lookups both produced `…-synced-from-poco_f8_ultra`, missed, and created a new bucket — causing a full re-import (every event twice in /timeline). The fallback scan finds the legacy bucket by matching sanitized origins and reuses it. When two distinct pre-ActivityWatch#697 buckets share the same sanitized origin (ambiguous), the function returns an error rather than silently merging distinct histories (ActivityWatch#697 :368). Two tests added: - `test_pre697_origin_scan_resumes_legacy_bucket`: desktop with `…-synced-from-POCO F8 Ultra` + `$aw.sync.origin` receives a post-migration pull and resumes without creating a fork. - `test_pre697_origin_scan_refuses_ambiguous_candidates`: two legacy buckets with different raw origins that sanitize identically trigger an error. Git-Session-Id: 3846
…r Android hostname migration (#708) * fix(aw-sync): scan pre-#697 buckets by origin to avoid re-import after Android hostname migration Closes #707. When both the raw-ID and sanitized-ID lookups miss in `get_or_create_sync_bucket`, fall back to scanning the destination for a `-synced-from-` bucket whose base ID matches and whose `$aw.sync.origin` sanitizes to the same value. This recovers the case where a desktop imported an Android peer before #697 landed (`…-synced-from-POCO F8 Ultra` with `$aw.sync.origin = "POCO F8 Ultra"`), then the phone ran ActivityWatch/aw-android#273 and its staging hostname became `poco_f8_ultra` (sanitized, no `$aw.sync.origin` on first-hand buckets). The two direct lookups both produced `…-synced-from-poco_f8_ultra`, missed, and created a new bucket — causing a full re-import (every event twice in /timeline). The fallback scan finds the legacy bucket by matching sanitized origins and reuses it. When two distinct pre-#697 buckets share the same sanitized origin (ambiguous), the function returns an error rather than silently merging distinct histories (#697 :368). Two tests added: - `test_pre697_origin_scan_resumes_legacy_bucket`: desktop with `…-synced-from-POCO F8 Ultra` + `$aw.sync.origin` receives a post-migration pull and resumes without creating a fork. - `test_pre697_origin_scan_refuses_ambiguous_candidates`: two legacy buckets with different raw origins that sanitize identically trigger an error. Git-Session-Id: 3846 * test(aw-sync): prove resume cursor + ambiguous-skip doesn't abort peer sync Two tightenings per #707 review: 1. test_pre697_origin_scan_resumes_legacy_bucket: seed the legacy bucket with one event at T0, put T0-1h and T0+1h in the source. After sync, assert exactly 2 events (existing + new). 3 would mean the cursor was ignored and the full history was re-imported. 2. test_pre697_origin_scan_refuses_ambiguous_candidates: add a second, healthy source bucket (aw-watcher-window). sync_datastores must return Ok overall — the ambiguous android bucket is skipped (warn+continue), not a fatal abort. Assert the healthy bucket received events; assert neither ambiguous legacy bucket was written to. Git-Session-Id: eacd * test(aw-sync): pin ambiguity refusal with a non-empty ambiguous source bucket The refusal test asserted both legacy candidates received zero events, but the ambiguous source bucket held no events — so a regression that silently reused one candidate would also have copied nothing and the assertions would still pass. Insert an event into the ambiguous source bucket (plus a premise guard that it really has one) so only an actual skip keeps both legacy buckets at 0. Git-Session-Id: 9059e8cf-aa8c-5d86-bc5d-0167df2a66b0
Fixes #692.
Problem
Pulling a peer whose bucket hostname contains whitespace (e.g.
POCO F8 Ultrafrom Android devices before ActivityWatch/aw-android#272 sanitized device names) fails with a 400 fromcreate_bucket(#658 rejects new buckets with whitespace hostnames). The 400 propagated via?and aborted the entire sync pass, leaving every subsequent peer unseen (#688 blast radius).Fix
Part 1 — Sanitize on import (Option 1 from #692, with the legacy-ID lookup):
get_or_create_sync_bucketnow checks for an existing unsanitized legacy bucket first (so any device already pulled before this fix keeps its established ID and avoids a full re-import fork per ActivityWatch/activitywatch#1373). If no legacy bucket exists, the hostname and derived ID are sanitized with aw-android's exact algorithm (sanitize_hostname(), byte-identical toDeviceHostname.kt: lowercase,[^a-z0-9_-]+→_, trim_) before callingcreate_bucket."POCO F8 Ultra"→"poco_f8_ultra", so Android's hostname-column migration lands on the same ID.$aw.sync.originkeeps the raw hostname. The sanitizer also runs when the hostname field has whitespace even if the derived ID is already clean.Part 2 — Per-bucket and per-peer non-fatal errors (Option 3 from #692 / #688):
?propagation in thesync_datastoresbucket loop is replaced withwarn + continue. A broken or invalid bucket is skipped; all healthy buckets still sync.pull_alldoes the same at peer level: a peer that fails to open no longer aborts the pass and skips every peer after it (needed by aw-sync writes to peer databases on every pull: WAL flip + schema migration on files it does not own #693's skip-on-mismatch).Tests
Related: #658, #685, #688, #693, ActivityWatch/aw-android#272, ActivityWatch/activitywatch#1373.