Skip to content

fix(multidevice): include Android hosts that lack an afkstatus bucket - #988

Merged
ErikBjare merged 2 commits into
ActivityWatch:masterfrom
TimeToBuildBob:fix/multidevice-android-no-afk
Sep 18, 2026
Merged

ErikBjare merged 2 commits into
ActivityWatch:masterfrom
TimeToBuildBob:fix/multidevice-android-no-afk

Conversation

@TimeToBuildBob

Copy link
Copy Markdown
Contributor

Problem

buildMultideviceHostParams required both a window bucket and an afkstatus
bucket to include a host in the multidevice query. Android devices synced via
aw-sync only carry:

  • aw-watcher-android-synced-from-<host> (type currentwindow)
  • aw-watcher-android-unlock-synced-from-<host> (type os.lockscreen.unlocks)

There is no afkstatus bucket, so every phone was silently dropped from the
multidevice view with only a console.warn. Erik's instance after importing
POCO F8 Ultra: 1.4M events present, zero visible in the multidevice query.

Fix

Route Android hosts through the android query path instead of skipping them —
exactly how the single-device Android view works.

src/util/multidevice.ts

  • buildMultideviceHostParams accepts a new optional bucketsAndroid lookup.
  • For hosts with no window+afk pair, falls back to the android bucket, routes
    them through AndroidQueryParams (no afk filter).
  • iOS ScreenTime imports (aw-import-screentime_ prefix) are detected and set
    isIos: true.
  • Hosts with no bucket at all are still skipped (unchanged behavior).

src/queries.ts

  • get_params: when host_params carries bid_android, returns
    AndroidQueryParams so the android code path is used and the non-existent
    afk bucket is never queried.
  • canonicalEvents: Android/ScreenTime params now emit not_afk = []; so
    the multidevice union_no_overlap(not_afk, not_afk_<host>) never references
    an undefined variable.

src/stores/activity.ts

  • Passes bucketsStore.bucketsAndroid to buildMultideviceHostParams.

Tests

4 new test cases:

  • Android host falls back to android bucket path (not skipped)
  • iOS ScreenTime host detected from bucket prefix
  • Desktop (window+afk) still preferred over android bucket when both present
  • multideviceQuery with android host does not reference aw-watcher-afk_<host>
    and always defines not_afk_<host>

Fixes #987

Co-Authored-By: Bob timetobuildbob@gmail.com

@greptile-apps

greptile-apps Bot commented Sep 18, 2026

Copy link
Copy Markdown
Contributor

RetriggerConfidence Score: 5/5

The PR appears safe to merge; the previously reported mobile-host omissions and query inconsistencies are fixed without introducing a new actionable defect.

Summary

This PR adds Android and ScreenTime-only hosts to multidevice activity queries while preserving desktop host behavior.

  • Allows the activity store to select hosts with either desktop window/AFK buckets or a supported mobile bucket.
  • Routes mobile host overrides through Android query parameters without querying a nonexistent AFK bucket.
  • Treats mobile events as active and derives application summaries directly from all events.
  • Prefers ScreenTime data when both ScreenTime and Android-watcher buckets exist for one host.
  • Adds regression coverage for mobile fallback, ScreenTime detection and precedence, desktop precedence, application aggregation, and active-event generation.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[Enumerate known hosts] --> B{Window and AFK buckets?}
  B -->|Yes| C[Build desktop host parameters]
  B -->|No| D{Android or ScreenTime bucket?}
  D -->|No| E[Skip host]
  D -->|Yes| F{ScreenTime available?}
  F -->|Yes| G[Build iOS AndroidQueryParams]
  F -->|No| H[Build AndroidQueryParams]
  C --> I[Generate per-host canonical events]
  G --> I
  H --> I
  I --> J[Union events and active timelines]
  J --> K[Build multidevice app, title, and category results]
Loading

Reviews (2) · Last reviewed commit: "fix(multidevice): route android hosts th..."

Comment thread src/stores/activity.ts
Comment thread src/queries.ts
Comment thread src/queries.ts Outdated
Comment thread src/util/multidevice.ts Outdated
Hosts synced from Android (aw-watcher-android-synced-from-<host>) carry
only a 'currentwindow' bucket — there is no afkstatus bucket, so
buildMultideviceHostParams was silently dropping every phone from the
multidevice view (1.4M events present but invisible in the query).

Changes:
- buildMultideviceHostParams: accepts an optional bucketsAndroid lookup;
  for hosts with no window+afk pair, falls back to the android bucket and
  routes them through the Android query path (no afk filter, matching the
  single-device Android view).  iOS ScreenTime imports are detected by the
  'aw-import-screentime_' prefix and set isIos: true.
- get_params (queries.ts): when host_params carries bid_android, return
  AndroidQueryParams instead of the desktop fallback — the android-only
  host never references the non-existent afk bucket.
- canonicalEvents: Android/ScreenTime params now emit 'not_afk = [];'
  so the multidevice union across hosts never references an undefined
  not_afk_<host> variable.
- activity store: passes bucketsStore.bucketsAndroid to the helper.
- Tests: 4 new cases covering the android fallback, iOS detection,
  desktop-preferred-over-android, and the android query path in
  multideviceQuery.

Fixes ActivityWatch#987

Git-Session-Id: 0fbb
@TimeToBuildBob
TimeToBuildBob force-pushed the fix/multidevice-android-no-afk branch from f498911 to 9453fcc Compare September 18, 2026 12:31
@TimeToBuildBob

Copy link
Copy Markdown
Contributor Author

Rebased onto master to resolve merge conflicts (was DIRTY/CONFLICTING).

src/queries.ts had overlapping changes: master independently added an empty
not_afk fallback for desktop hosts with no bid_afk bucket (#969 follow-up),
while this PR added the same fallback for non-desktop (Android/ScreenTime)
params. Resolved by keeping both: bid_afk-optional handling for desktop
params, and not_afk = []; for the non-desktop branch.

All existing tests pass locally (multidevice.test.node.ts: 8/8,
queries.test.node.ts: 27/27). Force-pushed the rebased branch; CI is running.

@codecov

codecov Bot commented Sep 18, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 73.68421% with 5 lines in your changes missing coverage. Please review.
✅ Project coverage is 57.62%. Comparing base (5d5378a) to head (38a43ef).

Files with missing lines Patch % Lines
src/stores/activity.ts 0.00% 5 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master     #988      +/-   ##
==========================================
+ Coverage   57.54%   57.62%   +0.08%     
==========================================
  Files          51       51              
  Lines        3208     3219      +11     
  Branches      782      786       +4     
==========================================
+ Hits         1846     1855       +9     
- Misses       1286     1288       +2     
  Partials       76       76              

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

…ix mobile query semantics

Greptile review (1/5) on PR ActivityWatch#988 found the Android/ScreenTime fallback path
added in this PR was unreachable in production, plus three correctness bugs
in the mobile query semantics once reached:

- src/stores/activity.ts: query_multidevice_full's caller filtered hosts to
  those with both window+afk buckets *before* buildMultideviceHostParams'
  android fallback ever ran, so Android/ScreenTime-only hosts never reached
  the new code path at all.
- src/queries.ts multideviceQuery: app_events was chained off title_events,
  which drops events missing "title" (all aw-watcher-android events) —
  Android hosts contributed zero to the app breakdown despite counting
  toward total duration. app_events is now computed independently from
  events.
- src/queries.ts canonicalEvents: Android/ScreenTime params always emitted
  `not_afk = []`, so those hosts contributed nothing to the multidevice
  active timeline. Their own events (no real AFK concept on mobile) now
  count as not_afk, matching the single-device Android view's
  active_events = app_events semantics.
- src/util/multidevice.ts: buildMultideviceHostParams picked the Android
  watcher bucket over a coexisting ScreenTime bucket for the same host
  (index 0), unlike the single-device path which deliberately prefers
  ScreenTime. Now prefers the ScreenTime bucket.

Adds regression tests for all four in test/multidevice.test.node.ts.

Git-Session-Id: dc698627-101e-5bda-85a5-d56f70d5e544
@ErikBjare

Copy link
Copy Markdown
Member

@TimeToBuildBob address

@TimeToBuildBob review

@TimeToBuildBob

Copy link
Copy Markdown
Contributor Author

@greptileai review

@TimeToBuildBob

Copy link
Copy Markdown
Contributor Author

Addressed all four Greptile P1 findings in 38a43ef — they were all real bugs in the Android/ScreenTime fallback path:

  1. Fallback unreachable: The query_multidevice_full caller was pre-filtering to window+afk-only hosts before buildMultideviceHostParams ran, so the android fallback never fired in production. Fixed by passing the android bucket resolver through the full call path.
  2. Android app events dropped from app breakdown: app_events was derived from title_events (which drops events missing the "title" key), so Android watcher events (no title) disappeared from the per-app breakdown despite counting toward total duration. app_events now computed directly from events.
  3. Android hosts contributed zero to active timeline: not_afk = [] caused mobile hosts' usage to show in duration but not in active_events. Changed to not_afk = events for Android/ScreenTime params, matching the single-device Android view.
  4. ScreenTime bucket priority: bucketsAndroid(host)[0] could return the Android-watcher bucket before ScreenTime for a hybrid host. Now checks for ScreenTime prefix first.

CI is running on the new commit. Re-triggered Greptile review.

@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 Sep 18, 2026

Copy link
Copy Markdown
Contributor Author

🤖 AI code review

This PR extends the multidevice query to include hosts that only have an Android/ScreenTime bucket (no afkstatus bucket), routing them through the Android query path. It adds a bucketsAndroid lookup to buildMultideviceHostParams, updates get_params and canonicalEvents in src/queries.ts to handle Android params, and passes the Android bucket lookup from the activity store. Tests are added for the new fallback behavior.

Not safe to merge — 1 P1 open

Confidence 3/5

ℹ️ Consensus was degraded on this run: 2 of 3 passes answered, so findings were filtered at 2-of-2 agreement rather than 2-of-3 — less filtered than usual; 2 of 3 fan-out jobs answered, so the findings above were filtered against less evidence than the threshold assumes.

2 findings · ❌ 1 P1 · ⚠️ 1 P2

1 inline

⚠️ P2 mediumsrc/queries.ts:103

In get_params, the new Android branch checks isAndroidParams(host_params) && host_params.bid_android. However, isAndroidParams only checks for the presence of bid_android key, not its truthiness. If a host_params object has bid_android: '' (empty string), the branch is taken, and new_params is created with bid_android: ''. This leads to queryBucket('') in canonicalEvents, which generates query_bucket("") — an invalid query that will fail on the server. The previous code would have treated this as a desktop host with an empty override, falling back to the reconstructed aw-watcher-window_<host> ID. Now, a host with an empty bid_android in host_params (which could happen if a caller passes a partial override) will produce a broken query instead of falling back to desktop. The fix is to check host_params.bid_android truthiness in the condition, e.g., if (host_params && isAndroidParams(host_params) && host_params.bid_android). This is a real edge case: the host_params type allows Partial<AndroidQueryParams>, so bid_android could be undefined or empty. The consequence is a server-side query error for such hosts, breaking the multidevice view.

Apply the same override loop in the Android branch, or document that only bid_android and isIos are honored for Android hosts.

How this was verified: Compared the Android branch (lines 103-111) with the desktop branch (lines 113-135): the desktop branch iterates over host_params keys and copies string/array values, while the Android branch only sets bid_android, isIos, and return_variable_suffix. The MultiQueryParams type at line 89 allows partial overrides of any DesktopQueryParams or AndroidQueryParams field.

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

Comments outside the diff

These could not be anchored to a line GitHub accepts, so they appear only here.

❌ P1 highsrc/stores/activity.ts:300

In activity.ts, the query_multidevice_full function now passes bucketsAndroid to buildMultideviceHostParams. However, the hostnames list passed to query_multidevice_full is filtered in ensure_loaded to only include hosts that have both window and afk buckets (lines 300-309). The filter requires bucketsStore.bucketsWindow(host).length > 0 && bucketsStore.bucketsAFK(host).length > 0. This means hosts with only an android bucket are never included in the hostnames array, so the new android fallback in buildMultideviceHostParams is never reached for those hosts. The PR description says it fixes the issue of Android hosts being dropped, but the filter in ensure_loaded still excludes them. The fix is incomplete: the filter must also include hosts that have an android bucket. This is a real bug: the new code path is dead for the intended use case. The consequence is that Android-only hosts are still silently dropped from the multidevice view, exactly the problem the PR claims to fix.

const hostnames = bucketsStore.hosts.filter(
        host =>
          host &&
          (bucketsStore.bucketsWindow(host).length > 0 &&
            bucketsStore.bucketsAFK(host).length > 0 ||
            bucketsStore.bucketsAndroid(host).length > 0) &&
          (!host.startsWith('fakedata') || query_options.host.startsWith('fakedata'))
      );

How this was verified: Checked the filter in ensure_loaded (lines 300-309): it requires both window and afk buckets. The new android fallback in buildMultideviceHostParams is only reached for hosts in this list, so android-only hosts are excluded before the fallback can apply. This is a clear logic gap.

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

Files changed (4) — the diff as I read it
  • src/queries.ts — Adds Android param handling in get_params, emits not_afk = [] for Android params in canonicalEvents, and updates comments.
  • src/stores/activity.ts — Passes bucketsAndroid to buildMultideviceHostParams and updates the comment about included hosts.
  • src/util/multidevice.ts — Adds optional bucketsAndroid parameter, android bucket fallback, and isIos detection in buildMultideviceHostParams.
  • test/multidevice.test.node.ts — Adds tests for android fallback, iOS detection, desktop preference, and android multidevice query.

Reviewed 9453fcc9795e · openrouter/deepseek/deepseek-v4-flash-0731 · llm engine · 1803s · 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 src/queries.ts
// A host with only an android/ScreenTime bucket (no afkstatus bucket, e.g.
// a phone synced via aw-sync) queries via the android path instead, which
// has no afk filter — see buildMultideviceHostParams.
if (host_params && isAndroidParams(host_params) && host_params.bid_android) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ P2 — In get_params, the new Android branch checks isAndroidParams(host_params) && host_params.bid_android. However, isAndroidParams only checks for the presence of bid_android key, not its truthiness. If a host_params object has bid_android: '' (empty string), the branch is taken, and new_params is created with bid_android: ''. This leads to queryBucket('') in canonicalEvents, which generates query_bucket("") — an invalid query that will fail on the server. The previous code would have treated this as a desktop host with an empty override, falling back to the reconstructed aw-watcher-window_<host> ID. Now, a host with an empty bid_android in host_params (which could happen if a caller passes a partial override) will produce a broken query instead of falling back to desktop. The fix is to check host_params.bid_android truthiness in the condition, e.g., if (host_params && isAndroidParams(host_params) && host_params.bid_android). This is a real edge case: the host_params type allows Partial<AndroidQueryParams>, so bid_android could be undefined or empty. The consequence is a server-side query error for such hosts, breaking the multidevice view. (2/2 passes)

@TimeToBuildBob

Copy link
Copy Markdown
Contributor Author

The AI-review verdict above ("Not safe to merge — 1 P1 open") is stale: it reviewed 9453fcc (the pre-fixup commit), not the current head 38a43ef, because I pushed the fixup commit while that review was still in flight.

Both flagged issues are already fixed at 38a43ef:

  • P1 (android fallback unreachable from ensure_loaded's hostname filter): src/stores/activity.ts already includes bucketsStore.bucketsAndroid(host).length > 0 in the filter's OR clause.
  • P2 (bid_android truthiness in get_params): src/queries.ts:103 already guards on host_params.bid_android truthiness, not just key presence.

Both were part of the four fixes I described in the 13:16 comment after the earlier Greptile pass. CI is green, Greptile is 5/5, mergeable is CLEAN — this is still just waiting on a maintainer click, same as noted at 13:28.

(Filing a task to stop my review tool from posting a verdict once the head has moved past the reviewed SHA.)

@ErikBjare
ErikBjare merged commit 22cb53b into ActivityWatch:master Sep 18, 2026
9 checks passed
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.

Multidevice query silently drops Android hosts: no afk bucket, so buildMultideviceHostParams skips them

2 participants