Skip to content

feat: exclusion-aware frame peek for filter-sensitive consumers#10

Open
divanshu-go wants to merge 1 commit into
screenpipe:mainfrom
divanshu-go:feat/exclusion-aware-peek-and-stream-hygiene
Open

feat: exclusion-aware frame peek for filter-sensitive consumers#10
divanshu-go wants to merge 1 commit into
screenpipe:mainfrom
divanshu-go:feat/exclusion-aware-peek-and-stream-hygiene

Conversation

@divanshu-go

@divanshu-go divanshu-go commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

What

Adds a peek API that makes the persistent stream's latched frame safely consumable by filter-sensitive callers (screenpipe's visual-change checks), and fixes a filter-update staleness bug that also affects capture().

before (per visual check, ~every 3-5s per monitor):
  capture_monitor_sync → spawn_blocking → manager lock → maybe create stream
                       → maybe update filter → clone latch under global lock

after:
  Monitor::peek_image_scaled_excluding → manager lock (validate only)
                                       → clone latch OUTSIDE the lock
  (falls back to capture when no stream / filter or resolution mismatch)

Peek API

  • peek_latest_frame_matching(monitor_id, width, height, ids) — returns the latch only when the running stream matches what a capture call with those parameters would use: same resolution (capture recreates on any width/height change) and same exclusion filter (the sorted comparison capture() uses). On mismatch the caller falls back to a capture, which installs the requested parameters. A returned frame is therefore always interchangeable with a captured one — no stream creation, no filter churn, no blocking-thread hop, same ≤1-frame-interval latch staleness.
  • Monitor::peek_image_excluding / peek_image_scaled_excluding compute target dims with the same scaled_dims logic as the corresponding capture_image* methods, so callers can't drift.
  • Per-stream filter epoch: the matching peek validates under the manager lock, clones the frame after releasing it, and rejects if the epoch moved — a concurrent capture swapping the filter mid-read can't hand back pixels the validation never saw.

Filter-update correctness (pre-existing bug, also hardens capture())

update_exclusions() installed the new ContentFilter but left the latched frame — composited under the old filter — in place, so for up to one frame interval both capture() and peeks served pixels the new exclusion list claims are filtered out (e.g. a freshly-ignored password manager). The latch is now dropped after a successful filter install; consumers wait ≤1 frame interval for the first post-update delivery.

Lock hygiene

All latch reads (peeks and capture()'s fast paths) clone the multi-MB frame buffer outside the global manager lock — a copy on one monitor no longer stalls captures, filter updates, or peeks on other monitors.

Add a peek API that makes the persistent stream's latched frame safely
consumable by filter-sensitive callers (screenpipe's visual-change
checks), and fix a filter-update staleness bug that also affects
capture().

## Peek API

- peek_latest_frame_matching(monitor_id, width, height, ids): returns
  the latched frame only when the running stream matches what a capture
  call with those parameters would use — same resolution (capture
  recreates the stream on any width/height change) and same exclusion
  filter (the sorted comparison capture() uses to decide on a filter
  update). On mismatch the caller falls back to a capture call, which
  installs the requested parameters. A returned frame is therefore
  always interchangeable with a captured one: no stream creation, no
  filter churn, no blocking-thread hop, same ≤1-frame-interval latch
  staleness.
- Monitor::peek_image_excluding / peek_image_scaled_excluding compute
  the target dimensions with the same scaled_dims logic as the
  corresponding capture_image* methods, so callers can't drift.
- Each stream carries a filter epoch (bumped on every filter change).
  The matching peek validates under the manager lock, clones the frame
  after releasing it, and rejects the frame if the epoch moved — a
  concurrent capture swapping the filter mid-read can't hand back
  pixels the validation never saw.

## Filter-update correctness (also hardens capture())

update_exclusions() now drops the latched frame after installing the
new ContentFilter: the latch was composited under the OLD filter, so
serving it would hand callers pixels the new exclusion list claims are
filtered out (e.g. a freshly-ignored password manager). Consumers wait
≤1 frame interval for the first post-update delivery instead.

## Lock hygiene

All latch reads (peeks and capture()'s fast paths) clone the multi-MB
frame buffer OUTSIDE the global manager lock, so a copy on one monitor
no longer stalls captures, filter updates, or peeks on others.

Removes the now-dead MonitorStream::latest_frame / wait_first_frame.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@divanshu-go divanshu-go force-pushed the feat/exclusion-aware-peek-and-stream-hygiene branch from a6beca7 to c29e363 Compare July 5, 2026 08:15
@divanshu-go divanshu-go changed the title feat: exclusion-aware frame peek + stream config hygiene feat: exclusion-aware frame peek for filter-sensitive consumers Jul 5, 2026
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