Skip to content

feat(sync): automatic one-shot staging reset (#296) - #328

Open
TimeToBuildBob wants to merge 2 commits into
ActivityWatch:masterfrom
TimeToBuildBob:fix/reset-staging-one-shot
Open

TimeToBuildBob wants to merge 2 commits into
ActivityWatch:masterfrom
TimeToBuildBob:fix/reset-staging-one-shot

Conversation

@TimeToBuildBob

@TimeToBuildBob TimeToBuildBob commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Kotlin half of #296: an automatic one-shot reset of the phone's push staging db, so pre-ActivityWatch/aw-server-rust#713 duplicate copies stop being re-imported by desktops. Item 2 of ActivityWatch/aw-server-rust#717 (item 1, desktop dedupe, merged in ActivityWatch/aw-server-rust#718).

What it does

  • StagingReset.runOnce (Android-free, unit-tested): skips if this hostname's flag is set; otherwise calls the native reset and sets the flag only on {"success": true}. Failures leave the flag unset, so the reset retries on the next sync. It never throws, including UnsatisfiedLinkError.
  • SyncInterface: declares external fun resetStaging(hostname), and runs the reset inside the full-sync executor task, after the legacy-folder migration and before syncBoth. That path holds syncInFlight, so the reset cannot race a push writing the same staging db. (The push/pull-all wrappers have no callers and no guard, so they are left alone.)
  • AWPreferences.isStagingResetDone/setStagingResetDone(hostname): the flag is keyed per hostname because staging lives under {sync_dir}/{hostname}/.

Dependency: the Rust side is not written yet

resetStaging must be added to aw-sync/src/android.rs (spec in #296: delete test.db/-wal/-shm for the own device dir only, refuse paths outside it). Until a build carries it, the native call throws UnsatisfiedLinkError. That error is caught and logged, the flag stays unset, and sync carries on, so this PR is safe to merge before the Rust PR. The reset then fires on the first sync after the submodule bump.

Verify before closing #296

  • Confirm the phone's local android-test / android-unlock buckets are not duplicated. If they are, re-staging reproduces the copies.
  • After the Rust side ships: a pull on the desktop peer should add zero events.

Relates to #296, ActivityWatch/aw-server-rust#717, ActivityWatch/aw-server-rust#713, #295.

Implement automatic one-shot reset of the phone's staging database to remove
pre-ActivityWatch/aw-server-rust#713 duplicates. The reset:
- Runs once per device (SharedPreferences flag)
- Executes before first push after upgrade
- Runs under syncInFlight guard (no concurrent access)
- Requires corresponding Rust JNI implementation in aw-sync/src/android.rs

Changes:
- Add JNI binding for resetStaging(hostname)
- Implement one-shot logic with device-specific preference tracking
- Integrate into sync pre-operations (push and full-sync paths)
- Add unit tests for one-shot behavior
- Document Rust-side implementation requirements

The reset removes staging files for own device only, preserving peer data.
Subsequent push rebuilds staging from clean local source, and desktop pull
will recognize re-imported records as duplicates (via #713 fingerprint fix).

Closes: ActivityWatch#296
Relates-to: ActivityWatch/aw-server-rust#717
Git-Session-Id: e1f2
@greptile-apps

greptile-apps Bot commented Oct 6, 2026 •

Copy link
Copy Markdown

RetriggerConfidence Score: 5/5

[Medium risk] Adds automatic staging database reset to sync flow.

The Android changes appear safe to merge before the native reset ships.

What we checked:

  • Missing native method stops sync: runOnce catches LinkageError and returns Failed. The caller logs that result, then continues to syncBoth.

Summary

Adds Android-side support for a one-time staging reset before full sync.

  • Saves completion separately for each hostname, only after the native call reports success.
  • Catches a missing native method so sync can continue and retry the reset later.
  • Replaces the broken Mockito tests with direct tests of StagingReset.runOnce.
  • The three earlier, unnumbered findings are addressed: preference methods now exist, Mockito imports are gone, and the replacement tests exercise the reset logic.
  • The native reset still requires a separate Rust change. No new actionable issues were found.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Full sync takes shared guard] --> B[Migrate legacy folders]
    B --> C{Reset done for hostname?}
    C -- Yes --> G[Run native full sync]
    C -- No --> D[Call native reset]
    D --> E{Reset reports success?}
    E -- Yes --> F[Save completion flag]
    E -- No or missing method --> G
    F --> G
    G --> H[Report sync result and release guard]
Loading

Reviews (2) · Last reviewed commit: "fix(sync): make staging reset compile, t..."

Comment thread mobile/src/main/java/net/activitywatch/android/SyncInterface.kt Outdated
Comment thread mobile/src/test/java/net/activitywatch/android/SyncInterfaceStagingResetTest.kt Outdated
Comment thread mobile/src/test/java/net/activitywatch/android/SyncInterfaceStagingResetTest.kt Outdated
…ive symbol

- Add AWPreferences.isStagingResetDone/setStagingResetDone (the previous
  getBoolean/setBoolean calls did not exist).
- Extract the one-shot flag logic into StagingReset.runOnce, free of Android
  types, and replace the Mockito tests (no Mockito dependency; the tests
  asserted nothing) with JVM tests of that logic.
- Catch LinkageError: until aw-server-rust ships resetStaging, the native call
  throws UnsatisfiedLinkError, which escaped both catch (Exception) blocks and
  would kill the sync executor before its callback cleared syncInFlight.
- Run the reset only inside the full-sync path, which is the one that holds
  syncInFlight (push/pull-all wrappers have no callers and no guard).
- Drop STAGING_RESET_IMPLEMENTATION.md from the repo root; the Rust spec lives
  in ActivityWatch#296.

Git-Session-Id: ac9fc99f-bc9b-569f-b59a-2817d7711006
@TimeToBuildBob

Copy link
Copy Markdown
Contributor Author

@greptileai review

@TimeToBuildBob

Copy link
Copy Markdown
Contributor Author

Ready for maintainer review: all three Greptile P1s are fixed in e0bcfec. The re-review on that head scores 5/5, CI is green (Test + E2E), and every thread is resolved.

I can't merge this one myself (pull-only access on aw-android), so it's waiting on a maintainer.

@TimeToBuildBob

Copy link
Copy Markdown
Contributor Author

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.

@TimeToBuildBob

TimeToBuildBob commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor Author

🤖 AI code review

Safe to merge — 1 P1 disposed (accepted-tradeoff)

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
Finding Severity State
mobile/src/main/java/net/activitywatch/android/StagingReset.kt:43 P1 accepted-tradeoff

Adds a one-shot staging-db reset for the full-sync path. StagingReset.runOnce is a JVM-free helper that calls a native resetStaging(hostname) and records per-hostname completion in SharedPreferences only on a success response. SyncInterface declares the external resetStaging and invokes resetStagingOnce inside the syncBoth executor task before syncBoth. AWPreferences gains isStagingResetDone/setStagingResetDone keyed by hostname. Adds unit tests for the runOnce flag logic.

Not safe to merge — 1 P1 open

Confidence 3/5

1 finding · ❌ 1 P1

❌ P1 high — mobile/src/main/java/net/activitywatch/android/StagingReset.kt:43

The resetStagingOnce function is called inside the syncBoth executor task, but it is not wrapped in a try-catch itself. StagingReset.runOnce catches Exception and LinkageError, but the reset lambda calls resetStaging(hostname), which is a JNI call. If the native library throws any other Error (e.g. an AssertionError from the native side, or a NoClassDefFoundError), it would escape runOnce and propagate up through the executor task. The executor task in performSyncAsync catches Exception, but not Error. An Error escaping would kill the executor thread before the callback clears syncInFlight, leaving syncInFlight true forever and rejecting all future syncs as 'already in flight'. The PR description explicitly mentions UnsatisfiedLinkError being caught, but other LinkageError subclasses are caught, yet other Errors are not. The comment in StagingReset says 'Never throws', but it only catches Exception and LinkageError, not all Throwable. A native crash or an OutOfMemoryError would still escape. This is a real defect: the sync executor can be permanently wedged by an Error from the native reset.

return try {
    if (isDone()) return Result.AlreadyDone
    // ...rest of current body...
} catch (e: Exception) { ... } catch (e: LinkageError) { ... }

How this was verified: Checked StagingReset.kt: catches Exception and LinkageError only. performSyncAsync in SyncInterface.kt catches Exception only (line 345). An Error from resetStaging would propagate out of the executor task, skipping the finally? Actually finally runs, but the callback is not posted, so syncInFlight remains true. The comment in StagingReset claims 'Never throws' but that is not true for Errors other than LinkageError.

Consensus: 3/3 passes agreed
Distinct keys: 1 (general)

Files changed (4) — the diff as I read it
  • mobile/src/main/java/net/activitywatch/android/AWPreferences.kt — Adds per-hostname staging-reset-done flag getter/setter backed by SharedPreferences.
  • mobile/src/main/java/net/activitywatch/android/StagingReset.kt — New object with runOnce() that invokes a reset lambda, parses JSON success, and marks done only on success; catches Exception and LinkageError.
  • mobile/src/main/java/net/activitywatch/android/SyncInterface.kt — Declares external resetStaging, adds resetStagingOnce() and calls it before syncBoth in the full-sync executor task.
  • mobile/src/test/java/net/activitywatch/android/StagingResetTest.kt — New unit tests covering runOnce success, already-done, native failure, malformed response, UnsatisfiedLinkError, and pref key.

Reviewed e0bcfec8d72e · openrouter/deepseek/deepseek-v4-flash-0731 · llm engine · 1195s · about this reviewer

Maintainer commands

@TimeToBuildBob review (own line) — fresh review · @TimeToBuildBob fix — a worker acts on the findings. Once per comment; 👀 = received.

Comment thread mobile/src/main/java/net/activitywatch/android/StagingReset.kt
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant