Skip to content

fix(transform): handle zero-duration base and sub events in merge_subwatcher_fields#141

Open
fanxing11 wants to merge 2 commits into
ActivityWatch:masterfrom
fanxing11:fix/merge-subwatcher-zero-duration
Open

fix(transform): handle zero-duration base and sub events in merge_subwatcher_fields#141
fanxing11 wants to merge 2 commits into
ActivityWatch:masterfrom
fanxing11:fix/merge-subwatcher-zero-duration

Conversation

@fanxing11

Copy link
Copy Markdown
Contributor

Found two related bugs in merge_subwatcher_fields while poking at the new transform. Both stem from how the boundary/intersection logic deals with zero-length Timeslots.

Bug 1 — zero-duration base event is silently dropped

base = [Event(timestamp=t0, duration=timedelta(0), data={'app':'x'})]
sub  = [Event(timestamp=t0-timedelta(seconds=5), duration=timedelta(seconds=10), data={'project':'P'})]
merge_subwatcher_fields(base, sub, ['project'])
# -> []   (event vanishes)

When the base is instantaneous and does overlap a sub, boundary_points collapses to a single point, so zip(boundary_points, boundary_points[1:]) is empty and nothing is emitted. The no-overlap fast path further up returns the base unchanged, so the symptom only shows up when there's a sub.

Fix: dedicated branch for instant bases that picks the active sub via the same "latest sub wins" rule and returns a single enriched zero-duration event.

Bug 2 — instantaneous sub event "colors" the whole base

base = [Event(timestamp=t0, duration=timedelta(seconds=10), data={'app':'x'})]
sub  = [Event(timestamp=t0+timedelta(seconds=3), duration=timedelta(0), data={'project':'P'})]
merge_subwatcher_fields(base, sub, ['project'])
# -> [Event(t=0, dur=10s, data={'app':'x', 'project':'P'})]

Expected: the sub is active for zero time → no slice should be enriched.

Root cause: Timeslot([0,3]).intersection(Timeslot([3,3])) returns a zero-length but truthy Timeslot. The segment loop only checked if not segment_period.intersection(sub_period): continue, so every segment adjacent to the instant matched the sub. Then the dedup pass merged the (now identical) segments back into one fully-colored event.

Fix: ignore zero-length intersections both when collecting overlapping subs for a non-instant base and inside the per-segment matcher. ip.duration > 0 is the right rule — a sub that was active for zero time can't enrich any slice. (Instant base is still handled correctly, via the dedicated branch above.)

Tests

Added three:

  • test_merge_subwatcher_fields_zero_duration_base_event_preserved — covers bug 1
  • test_merge_subwatcher_fields_zero_duration_base_event_no_overlap_preserved — locks in the existing no-overlap fast path so future refactors don't regress it
  • test_merge_subwatcher_fields_zero_duration_sub_does_not_color_base — covers bug 2

All 30 test_transforms tests pass; ruff clean.

Real-world relevance

Most watchers heartbeat with a nonzero pulsetime, so canonical queries probably don't hit either case today. But the function is exposed in query2 and via the Python API, and instantaneous events are a perfectly valid shape (manual API posts, edge transformations, future watchers reporting markers). Wanted to lock down the contract before this transform shows up in more pipelines.

…watcher_fields

Two related bugs in the boundary/intersection logic:

1. A zero-duration base event with an overlapping sub was silently dropped.
   base_period.duration == 0 makes boundary_points collapse to a single
   element, so the segment loop produces no output. Now preserved as a
   single zero-duration event enriched from the active sub.

2. A zero-duration sub event (instantaneous marker whose timestamp falls
   inside a base) was coloring the entire base event. Timeslot.intersection
   returns a zero-length but truthy result when a segment's boundary
   coincides with the instant, so every adjacent segment matched the sub
   and they merged back into one fully-enriched event. Now zero-length
   intersections on a non-instant base are ignored — a sub that was active
   for zero time can't enrich any slice of the base.

Added 3 tests covering: zero-duration base + overlapping sub, zero-duration
base + no overlap (locks in fast path), zero-duration sub + non-instant
base.
@greptile-apps

greptile-apps Bot commented Jun 26, 2026

Copy link
Copy Markdown

Greptile Summary

This PR fixes zero-duration interval handling in merge_subwatcher_fields. The main changes are:

  • Adds a dedicated path for zero-duration base events.
  • Ignores zero-duration sub intersections for durationful base slices.
  • Adds tests for instant bases, no-overlap instant bases, and instant sub events.

Confidence Score: 5/5

This looks safe to merge.

  • No blocking issues found in the changed code.

Important Files Changed

Filename Overview
aw_transform/merge_subwatcher_fields.py Updates overlap collection and segment matching so zero-duration bases are preserved while zero-duration sub events do not color durationful bases.
tests/test_transforms.py Adds focused tests for the new zero-duration base and sub-event behavior.

Reviews (1): Last reviewed commit: "fix(transform): handle zero-duration bas..." | Re-trigger Greptile

The new instant-base branch reused 'best_sub' / 'best_sub_period' as plain
local names, then the segment loop further down redeclares them with
Optional[] type annotations. mypy treats this as 'Name already defined'
even though control flow makes them disjoint. Renaming the instant-base
variables to 'instant_best_*' satisfies mypy and makes the intent clearer.
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