Skip to content

fix(capture): scope the own-staging guard to the image file-flavor only - #72

Merged
davidboulay merged 1 commit into
mainfrom
fix/staging-guard-overmatch
Jul 31, 2026
Merged

fix(capture): scope the own-staging guard to the image file-flavor only#72
davidboulay merged 1 commit into
mainfrom
fix/staging-guard-overmatch

Conversation

@davidboulay

Copy link
Copy Markdown
Owner

Symptom

Recovering a file from history played no copy sound and never synced to the paired device.

Cause

Two unrelated things staged into DATA_DIR/paste:

Writer What Should capture see it?
storage.paste_path() a recovered file, copied out under its original name so the paste isn't named after its sha256 yes — a genuine user clip
_stage_image() throwaway backing file for an image also offered as a file; its uri-list bounces back through wl-paste --watch no — pure echo

_is_own_staging matched the whole directory, so it dropped both:

file_paths = [p for p in clipboard.read_file_paths(types) if not _is_own_staging(p)]

With no file path surviving and no image/png on the clipboard, capture_current() fell through to new_id = None. _cmd_store (cli.py:77-85) then sent no _current, no refresh, no _broadcast, and never reached the sound.play() inside if new_id is not None.

The panel's storage.touch() still moved the entry to the front, so the recover looked like it worked. Measured: entry 8939 recovered at 22:10:13 while .last_sound still held 8946 from 22:10:00 — capture ran and stored nothing.

Fix

_stage_image writes to FLAVOR_DIR (paste/.image-flavor/); the guard matches only that. The echo it exists for is still suppressed; file recovers now capture normally, so the sound and the broadcast come back.

Both directories are named in config.py instead of being spelled out at three call sites — that duplication is what let them drift into one.

Re-capturing a recovered file is harmless: add_file_from_path dedupes on the content hash and returns the existing id (bumping created_at), so no duplicate entry.

Tests

scripts/capture_staging_test.py (new, wired into CI) pins the two apart — including the cross-module case that actually broke: whatever paste_path() hands the panel must survive capture's filter. Against the old guard:

FAIL: capture staging guard is wrong:
  recovered file must be captured, not ignored — this is the bug: no copy sound and no sync broadcast on a file recover
  paste_path output must not be treated as own staging — /tmp/…/paste/Report Q3.pdf

Full suite green locally: selector prototypes, mac pasteboard, capture staging, sync core, device-id drift, sync delivery.

Note

Existing files already staged in paste/ from before this change are unaffected — they're only ever overwritten per recover. Nothing migrates and nothing needs cleaning up.

Recovering a *file* from history was silent and didn't sync. Two unrelated
things staged into `DATA_DIR/paste`:

  paste_path()     a recovered file, copied out under its original name so the
                   paste isn't named after its sha256 — a genuine user clip
  _stage_image()   a throwaway backing file for an image also offered as a file,
                   whose uri-list bounces back through wl-paste --watch

`_is_own_staging` matched the whole directory, so it dropped both. With no file
path surviving the filter and no image flavor on the clipboard, capture_current()
fell through to `new_id = None`; `_cmd_store` then sent no `_current`, no
`refresh` and no `_broadcast`, and never reached the `sound.play()` inside
`if new_id is not None`. Meanwhile the panel's storage.touch() still moved the
entry to the front, so the recover looked like it had worked.

Measured: entry 8939 recovered at 22:10:13, `.last_sound` still holding 8946
from 22:10:00 — capture ran and stored nothing.

The echo it was written for is unaffected: _stage_image now writes to
FLAVOR_DIR (`paste/.image-flavor/`) and the guard matches only that. Both
directories are named in config.py rather than spelled out at three call sites,
which is what let them drift into one.

Re-capturing a recovered file is harmless: add_file_from_path dedupes on the
content hash and returns the existing id, so it bumps created_at rather than
filing a duplicate — the sound and the broadcast are exactly what we want back.

scripts/capture_staging_test.py (new, in CI) pins the two apart, including the
cross-module case that actually broke — paste_path()'s output must survive
capture's filter. It fails on the old guard with "recovered file must be
captured, not ignored".

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
@davidboulay
davidboulay merged commit c9be172 into main Jul 31, 2026
1 check 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.

1 participant