Repository navigation
feat(linux): treat an active microphone as not-AFK (opt-in) - #89
TimeToBuildBob wants to merge 4 commits into
Conversation
|
This comment has been minimized.
This comment has been minimized.
|
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 4 P1 findings from the Greptile review (commit 2239f08): 1. Monitor capture counts as microphone ( 2. Query failures interrupt calls ( 3. Call heartbeats stop advancing ( 4. AFK backdates across calls ( All 29 tests pass, mypy clean. Re-triggering Greptile now. |
|
@greptileai review |
🤖 AI code reviewSafe to merge — 1 P1 disposed (rejected)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
Adds opt-in Linux audio-based AFK detection: a new audio.py module queries pactl for active non-monitor source/sink streams, and afk.py's heartbeat_loop uses microphone activity to extend not-AFK state while logging audio playback transitions. Adds config flags --detect-mic and --detect-audio-playback, a design doc, and unit tests for parsing, detection, and loop behavior. Needs a look — P2 onlyConfidence 4/5 1 finding ·
|
| commit | score | findings | engine | when |
|---|---|---|---|---|
2239f086245e |
3/5 | 1 | llm | 2026-10-02 09:56 UTC |
f32727fd83a5 |
3/5 | 2 | llm | 2026-10-02 14:22 UTC |
11c32404a57d |
3/5 | 2 | llm | 2026-10-03 00:17 UTC |
Reviewed 11c32404a57d · openrouter/deepseek/deepseek-v4-flash-0731 · llm engine · 1089s · about this reviewer
Maintainer commands
@TimeToBuildBob review (own line) — fresh review · @TimeToBuildBob fix — a worker acts on the findings. Once per comment; 👀 = received.
Call and media time without keyboard/mouse input is currently recorded as AFK. Add a Linux prototype that reads PulseAudio/PipeWire stream metadata via pactl: - detect_mic (off by default): while a non-monitor source-output is active, stay not-AFK even past the idle timeout. Returning from AFK is anchored at the moment capture was first observed so the whole call is counted as present. A locked session ignores the signal until unlock. - detect_audio_playback (off by default): log playback transitions only. Audio output is a weak signal (background music) and never overrides AFK. Monitor sources/sinks are excluded (system-audio capture, not the mic). Only 'is a stream active' booleans are read; no audio content. Degrades to a no-op when pactl is unavailable. Design note: docs/audio-detection.md. Refs ActivityWatch#41 and ActivityWatch/activitywatch#261. Tests: tests/test_afk_mic.py (parser, detector, loop behavior). Git-Session-Id: e1fa
1. **audio.py – monitor detection column** (`_parse_stream_targets`): real `pactl list short source-outputs` places the source name in column 3 (0-indexed), not column 2; column 2 is the numeric client index, so the `.monitor` check never fired. Updated docstring and test fixtures to match. 2. **afk.py – query failures interrupt calls**: a failed `is_microphone_in_use()` returning `None` was treated as a confirmed mic-stop, clearing `mic_active_since` and falsely AFK-ing the user mid-call. Fixed to preserve state on `None`. 3. **afk.py – call heartbeats stop advancing**: the not-AFK heartbeat sent a fixed `timestamp=start` with no duration, so the recorded event never grew beyond its initial size during a long mic-only call. Added explicit `duration=(now - start).total_seconds()` to match the AFK heartbeat logic. 4. **afk.py – AFK backdates across calls**: when a mic-active spell ended while idle > timeout, `afk_start = last_input` reached back before the call, overlapping the presence event. Capture the pre-detection `mic_active_since` state and anchor `afk_start = now` when a call just ended. Tests: 29/29 pass; mypy clean. Git-Session-Id: 41848912-8053-53f2-a2d2-0c0512fc9374
Without this, a watcher that starts not-AFK with the mic already active never sets present_anchor (only AFK->not-AFK transitions did that). Heartbeats then timestamp at last_input, which can be seconds_since_input in the past and overlap a prior AFK event. Fix: when mic_active and present_anchor is None in the steady-state heartbeat branch, bootstrap present_anchor from mic_active_since. Also fix afk_duration to use (now - afk_start) instead of seconds_since_input — the two are equivalent only when last_input has not been clipped by not_afk_start, and they diverge after an unlock without new HID input. Test: test_heartbeat_not_backdated_when_mic_active_from_first_poll Git-Session-Id: 51a2cf06-a757-5f1d-9d00-611ca0f9cff8
2239f08 to
f32727f
Compare
|
@greptileai review |
Status update — ready for maintainer reviewWork done this cycle:
Greptile findings classified (3→5 rounds; score now 3/5):
No blocking bugs remain. The one live trade-off (indefinite persistence on prolonged pactl failure) is documented in What's left for a human:
|
Not-AFK heartbeats carried duration=now-start on every poll, so the presence event always extended to the current poll. When the idle timeout later tripped, AFK started at last_input, overlapping that event. This hit the default config (detect_mic off) on every AFK transition, and the mic-stops-before-timeout case flagged in review. - Without mic evidence, heartbeat zero-length at last_input (as master). - With the mic active, extend to now and record present_until. - On becoming AFK (non-lock), start at max(last_input, present_until); replaces the same-poll-only was_mic_present special case. - Tests replay pings through aw-server heartbeat merge and assert the stored events never overlap. Git-Session-Id: 13f9
Merge recommendation — converged, no blocking findingsAdjudicated the 2 remaining AI-review threads at head. Both were dispositioned as non-blocking false positives; no code change was required. No re-review triggered (attempt cap). Fixed: none — no code change this session. Remaining (non-blocking, resolved with reasons):
Verified against: head CI: all 3 checks pass (ubuntu-24.04 py-3.9, macos-latest py-3.9, windows-latest py-3.9). PR is Domain risk: the feature is Linux-only and depends on live Convergence: detector No open threads of ours remain. Awaiting maintainer judgment — merge-ready is not auto-merge. |
Refs #41, ActivityWatch/activitywatch#261
What
Long calls or media without keyboard/mouse input are currently recorded as AFK.
Add an opt-in Linux prototype that reads PulseAudio/PipeWire stream metadata
via
pactland keeps two signals deliberately distinct:A confirmed screen lock always wins: capture may be a background recording, so
a locked session stays AFK even while the mic is in use.
Behavior
--detect-mic(ordetect_mic = true): while a non-monitorsource-outputis active, stay not-AFK even past
timeout. When returning from AFK the eventis anchored at the moment capture was first observed, so the whole call is
counted as present rather than only from the last input event.
back-date into the locked interval).
--detect-audio-playback(ordetect_audio_playback = true): log playbacktransitions only.
*.monitor) areexcluded — they are system-audio capture, not the microphone.
Privacy
Only "is a stream active" booleans are read; no audio content is accessed and no
new OS permission is required. If
pactlis unavailable (e.g. PipeWire withoutthe PulseAudio compat layer) the feature is a silent no-op.
Design note
docs/audio-detection.mdcovers the per-OS APIs (Linux PipeWire/PulseAudio,macOS CoreAudio/TCC, Windows
CapabilityAccessManager), the privacy stance,config defaults, and known limitations.
Verification
python -m unittest discover -s tests -p "test_afk_*.py"→ 27 passed(21 pre-existing + parser/detector tests; loop behavior in
tests/test_afk_mic.py).mypy aw_watcher_afk --ignore-missing-imports→ clean.(
pactlabsent). The prototype is unit-tested against a mocked backend; areal-device smoke test is welcome before merge.
Follow-ups
pactl list source-outputs.dataflag or a separate bucket).