Skip to content

fix(macos): keep foreground tracking when browser context fails - #146

Open
TimeToBuildBob wants to merge 6 commits into
ActivityWatch:masterfrom
TimeToBuildBob:fix/macos-foreground-reconciliation-rebase
Open

TimeToBuildBob wants to merge 6 commits into
ActivityWatch:masterfrom
TimeToBuildBob:fix/macos-foreground-reconciliation-rebase

Conversation

@TimeToBuildBob

Copy link
Copy Markdown
Contributor

Summary

Rebase of #141 (hawai-i) onto current master after #145. Original work is preserved and attributed (Co-Authored-By: hawai-i).

Browser URL/title enrichment is optional, but the macOS Swift watcher used to return without a heartbeat when Chrome/Safari ScriptingBridge failed. Heartbeat merging then extended the previous application across the real browser interval.

This keeps #141's PID-based foreground reconciliation:

  • emit a foreground heartbeat even when browser context acquisition fails
  • bind tracked app, AX observer, focused window, and emitted app identity to one PID
  • repair a missing/stale observer and reject stale AX callbacks
  • consume the activated NSRunningApplication from the workspace notification

Conflict with #145

#145 landed on master after #141 opened. The rebase keeps that work:

Unsafe as? String / as! AXUIElement casts from #141 are not carried forward.

Greptile on #141

P1 (incognito): ScriptingBridge is the only Chrome incognito detector. The original fallback sent AX-derived app + title when that lookup failed. The rebase still emits the Chrome app identity (so tracking stays coherent) but drops title and URL. Successful incognito detection still blanks both app and title.

P2 (tests): make test-swift now compiles with -target …-macosx$(MACOSX_DEPLOYMENT_TARGET) (12.0), matching the production helper.

Validation

Python tests: 77 passed. mypy clean. Swift helper/tests need Darwin CI (macos-latest).

Related

Rebase of ActivityWatch#141 (hawai-i) onto current
master after ActivityWatch#145.

Keep PID-based foreground reconciliation and emit a heartbeat when
Chrome/Safari ScriptingBridge fails, so merging does not extend the
previous application across the real browser interval.

Preserve ActivityWatch#145 AX safety (axString/axElement) and observer teardown
(unregister focused-window notification, track observedApp).

Chrome context-failure fallback keeps app identity for tracking but
drops title and URL: ScriptingBridge is the only incognito detector
(Greptile P1 on ActivityWatch#141). Swift state tests now compile with the macOS
12 deployment target (Greptile P2).

Co-Authored-By: hawai-i <32040032+hawai-i@users.noreply.github.com>
Git-Session-Id: 9615c06b-2a11-592d-b1ad-299a3433e323
Adding macos_state.swift made macos.swift a non-script compilation unit.
Top-level assignments (formatter options, encoder strategy, start()) are
illegal outside main.swift. Restore the ActivityWatch#141 closure inits and @main
wrapper that the rebase dropped.

Git-Session-Id: ca3f0209-55b1-58ce-8820-82fb6f5bc8e0
@TimeToBuildBob

Copy link
Copy Markdown
Contributor Author

macos-latest failed at make build-swift: compiling macos.swift together with macos_state.swift rejects top-level expressions (formatter.formatOptions = …, start(), RunLoop.main.run()).

The rebase onto master after #145 dropped the #141 @main wrapper and closure inits. Restored in 8ac81d3. CI is re-running.

@TimeToBuildBob

Copy link
Copy Markdown
Contributor Author

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.

@TimeToBuildBob

TimeToBuildBob commented Sep 16, 2026

Copy link
Copy Markdown
Contributor Author

🤖 AI code review

Safe to merge — 2 findings disposed (rejected; fixed in 90aad7f)

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
aw_watcher_window/macos.swift:211 P0 rejected — The verification step claimed "lines 192-209 contain top-level let main/formatter/encoder" and inferred "'main' attribut
aw_watcher_window/macos.swift:682 P1 fixed in 90aad7fbb413
aw_watcher_window/macos.swift:756 P2 superseded by latest review (not reproduced)
aw_watcher_window/macos.swift:819 P2 superseded by latest review (not reproduced)
aw_watcher_window/macos.swift:746 P2 accepted-tradeoff

Needs a look — P2 only

Confidence 4/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.

1 finding · ⚠️ 1 P2

⚠️ P2 mediumaw_watcher_window/macos.swift:746

In updateFocusedWindow, when a new focused window is registered, the code attempts AXObserverAddNotification for the window's title. If that call fails transiently (e.g., the window is not yet ready), it logs the error but continues: oldWindow is set to the new window and a heartbeat is emitted. On subsequent polls, refreshFocusedWindow calls updateFocusedWindow again; because oldWindow now equals the current window, windowChanged becomes false and the add is not re-attempted. Thus the title-change observer is never installed for that window, so live title/URL updates stop being received until the focused window changes or the AX observer is rebuilt. The watcher still sends heartbeats on each poll (every 10s) with the current title read via AX, so data is not lost but is delayed by up to 10 seconds, and tab switches that occur between polls are missed until the next poll.

// Release the observer we created but could not install.
      // AXObserverCreate returns a +1 retained object; use CFRelease to avoid leaking.
      CFRelease(newObserver)
      return

How this was verified: Checked the AXObserverCreate signature in the macOS SDK: it is a C function returning AXError, with the observer as an out-parameter, not marked CF_RETURNS_RETAINED. The code's own comment acknowledges the ARC ambiguity. The leak path is reached whenever addResult is neither success nor notificationAlreadyRegistered.

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

Previous review passes
commit score findings engine when
8ac81d39c2f8 4/5 2 llm 2026-09-16 08:53 UTC
9424b4f6a054 1/5 3 llm 2026-09-16 12:44 UTC

Reviewed 90aad7fbb413 · openrouter/deepseek/deepseek-v4-flash-0731 · llm (in-band) engine · 594s · 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 aw_watcher_window/macos.swift
Comment thread aw_watcher_window/macos.swift Outdated
PID-only AX guards accepted queued title-change events from a previous
window of the same app. Bind title callbacks to the focused window.

Safari ScriptingBridge failure kept the AX window title, unlike Chrome.
Use the same keep-app / drop-title-URL fallback so private pages cannot
leak via accessibility when context lookup fails.

Git-Session-Id: 9a7625ed-1cac-5d98-b0d9-013b84aae02d
Comment thread aw_watcher_window/macos.swift
Comment thread aw_watcher_window/macos.swift
rebuildObserver created an AXObserver then returned on
AXObserverAddNotification failure without adopting it, leaking the Mach
port. Adopt and tear down through the ActivityWatch#139 path. Bind foreground
identity in the caller so a failed install still emits a heartbeat.

Git-Session-Id: 28375f79-1abe-5518-8f8c-75fcc067315a
The add-failure path never installed the observer in the run loop, so
tearDownObserver's CFRunLoopRemoveSource was a no-op. Nil the managed
reference instead; CFRelease would double-free a Swift-ARC CF type.

Write the Swift state-test binary into a mktemp directory so make test
does not execute a predictable path in /tmp.

Git-Session-Id: 28375f79-1abe-5518-8f8c-75fcc067315a
Comment thread aw_watcher_window/macos.swift Outdated
@TimeToBuildBob

Copy link
Copy Markdown
Contributor Author

@greptileai review

@greptile-apps

greptile-apps Bot commented Sep 23, 2026

Copy link
Copy Markdown

RetriggerConfidence Score: 5/5

The PR appears safe to merge; no outstanding correctness, security, or repository-rule issue was identified.

Findings

  1. P2 Title registration is not retried

Summary

This PR makes the macOS native watcher keep foreground application tracking coherent when optional browser context acquisition fails.

  • Reconciles the foreground application, accessibility observer, focused window, and emitted identity by PID.
  • Repairs missing observers and rejects stale accessibility callbacks.
  • Emits privacy-safe browser fallback heartbeats without a title or URL when browser scripting fails.
  • Adds focused state tests and runs them on Darwin using the supported macOS 12 deployment target.
  • The previous title-notification retry finding is fully fixed: failed registration leaves the state retryable on the next poll.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Startup, activation, or poll] --> B[Select live foreground application]
    B --> C{PID changed or observer missing?}
    C -->|Yes| D[Rebuild AX observer]
    C -->|No| E[Reuse observer]
    D --> F[Read focused window]
    E --> F
    F --> G[Register or retry title notification]
    G --> H{Supported browser?}
    H -->|Chrome or Safari| I[Attempt browser context lookup]
    H -->|Firefox| J[Read URL from AX tree]
    H -->|Other app| K[Keep app and AX title]
    I -->|Success| L[Use browser title and URL]
    I -->|Failure| M[Keep app; blank title and URL]
    J --> N[Apply privacy filters]
    K --> N
    L --> N
    M --> N
    N --> O[Emit foreground heartbeat]
Loading

Reviews (2) · Last reviewed commit: "fix(macos): retry title-change observer ..."

Comment thread aw_watcher_window/macos.swift Outdated
If AXObserverAddNotification for kAXTitleChangedNotification fails once,
keep the focused window but leave registration retryable so the next poll
can install it. Poll heartbeats already carry the current AX title; this
restores live title-change callbacks after a transient add failure.

Git-Session-Id: 2b8b1429-6ecf-575e-b812-491b60c68d9e
@TimeToBuildBob

Copy link
Copy Markdown
Contributor Author

@greptileai review

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