Skip to content

Prevent SMB photo deletes from hanging - #1446

Merged
github-actions[bot] merged 16 commits into
mainfrom
investigate-slow-photo-delete
Aug 10, 2026
Merged

Prevent SMB photo deletes from hanging#1446
github-actions[bot] merged 16 commits into
mainfrom
investigate-slow-photo-delete

Conversation

@jss367

@jss367 jss367 commented Aug 9, 2026

Copy link
Copy Markdown
Owner

Vireo could hang indefinitely while moving photos to Trash on an unhealthy macOS SMB mount because os.replace ran inside the server worker.
This routes network-volume deletions through the existing time-bounded Finder subprocess, fails closed when mount discovery fails, retains catalog rows on timeout or error, and preserves the atomic rename fast path for local removable volumes.
It also reports file-level batch-delete progress and adds regression coverage for mount classification, bounded routing, failure behavior, and the progress callback.
Validation: 42 delete API tests, 13 focused Trash tests, and 32 duplicate-cleanup tests pass; the full app module has 426 passes and one unrelated local ExifTool availability failure.

Summary by CodeRabbit

  • Bug Fixes

    • Improved trash handling for files on macOS network volumes by routing them through Finder.
    • More reliably distinguishes moved, missing, and failed items, including files that are already missing.
    • Prevents unsafe deletion when network-volume status cannot be confirmed.
    • Handles custom and symlinked network mount points more reliably.
    • Improves duplicate-file cleanup for network-stored files and removes corresponding orphaned records.
  • Improvements

    • Added per-item progress reporting for trash and permanent deletion operations.
    • Provides clearer outcomes when files disappear or network mounts change during processing.

@coderabbitai

coderabbitai Bot commented Aug 9, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@jss367, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 2 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 046cf80a-7c4c-4da5-8391-5b4ccdd77fd8

📥 Commits

Reviewing files that changed from the base of the PR and between b2c5e2c and 508773e.

📒 Files selected for processing (3)
  • vireo/app.py
  • vireo/tests/test_app.py
  • vireo/tests/test_duplicates_api.py
📝 Walkthrough

Walkthrough

The trash workflow detects macOS network volumes, routes them through Finder, reconciles per-file results, validates detached mounts, and reports progress. Duplicate cleanup now handles network paths and already-missing files explicitly. Tests cover routing, Finder outcomes, mount changes, and callback compatibility.

Changes

macOS trash workflow

Layer / File(s) Summary
Network-volume detection and routing
vireo/app.py, vireo/tests/test_app.py
The application parses bounded mount output, normalizes mount points, expands symlink prefixes, fails closed when discovery is unreliable, and routes network paths directly to Finder.
Finder per-path outcome handling
vireo/app.py, vireo/tests/test_app.py
Finder processes each path independently and returns moved, missing, or error. The caller validates outcomes and rechecks mount state before accepting missing network files.
Trash integration and progress reporting
vireo/app.py, vireo/tests/test_delete_api.py, vireo/tests/test_duplicates_api.py
Trash operations report per-path progress and already-missing paths. Duplicate cleanup bypasses local preflight for network paths and reports missing losers explicitly. Test helpers accept progress callbacks.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant DiskOperation
  participant _trash_paths
  participant mount
  participant Finder
  DiskOperation->>_trash_paths: process paths with progress callback
  _trash_paths->>mount: discover mounted volumes
  mount-->>_trash_paths: classify network paths
  _trash_paths->>Finder: process network-volume paths
  Finder-->>_trash_paths: return per-path statuses
  _trash_paths-->>DiskOperation: report progress and missing paths
Loading

Possibly related PRs

  • jss367/vireo#976: Modifies the same Finder-based trash fallback and platform-specific routing.
  • jss367/vireo#1412: Modifies network-volume deletion, Finder handling, and _trash_paths failure outcomes.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the primary change: preventing SMB photo deletions from hanging.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch investigate-slow-photo-delete

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5237465148

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread vireo/app.py Outdated
)
report_processed(filepath)
continue
if _path_on_network_volume(filepath, network_roots):

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Move network classification ahead of SMB preflight I/O

When the SMB share is already unhealthy, _snapshot_parent_device() and os.path.isfile() run for every path before this network-volume branch is reached. Both issue in-process filesystem calls against the share and can block indefinitely during SMB reconnection, so a delete can still hang before reaching the time-bounded Finder subprocess. Classify network candidates first and avoid all source and parent stat calls for them.

Useful? React with 👍 / 👎.

Comment thread vireo/app.py Outdated
Comment on lines +1727 to +1729
volume_root = _volume_root_for_path(filepath)
if volume_root is None:
return False

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Honor network mounts outside /Volumes

When an SMB/NFS share is mounted at a custom mount point such as /Users/me/mnt/photos, _network_volume_roots() discovers that root, but this early /Volumes check returns False without comparing it. Such paths therefore fall through to send2trash() in the server worker and retain the unbounded-I/O behavior this routing is meant to prevent. Compare paths against discovered network roots regardless of their mount-point prefix.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 25b772f698

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread vireo/app.py Outdated
Comment on lines +2080 to +2084
# After an error or timeout, retain every catalog row even if
# Finder may have moved a prefix of the batch. Rechecking those
# paths here would put the worker straight back into potentially
# unbounded SMB stat calls; a retry safely reconciles files that
# were already moved.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Reconcile paths already moved by a timed-out Finder batch

When Finder moves a prefix of an SMB batch and then times out, retaining every row makes the next retry include those now-missing paths. Because _trash_via_finder() has no per-item error handling, Finder errors on the first already-missing item and aborts the repeat loop before later files are attempted; network candidates also skip the absence preflight, so subsequent retries can remain stuck indefinitely. The bounded Finder operation needs to report/reconcile individual outcomes or tolerate already-missing items during retries.

Useful? React with 👍 / 👎.

@jss367 jss367 added the claude-agent PR managed by Claude PR Agent label Aug 9, 2026 — with Claude

jss367 commented Aug 9, 2026

Copy link
Copy Markdown
Owner Author

Both Codex findings are addressed by 25b772f:

  • P1 (network classification ahead of preflight I/O) — _trash_paths now partitions network paths into finder_candidates before _snapshot_parent_device / os.path.isfile run, so a stalled SMB mount can no longer pin a worker on the in-process stat that used to precede the network-volume branch.
  • P2 (mounts outside /Volumes) — _path_on_network_volume no longer early-returns False when _volume_root_for_path is None; it now compares against the discovered network roots directly, so SMB/NFS mounts at custom mount points (e.g. /Users/me/mnt/photos) still route through the bounded Finder subprocess. Fail-closed behavior is preserved for /Volumes paths when the mount table can't be read.

Related trash/network tests pass locally (10/10 in vireo/tests/test_app.py). No additional changes from the agent.


Generated by Claude Code

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e8fd1f55cf

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread vireo/app.py
Comment on lines +1903 to +1905
"-e", (
"if (not sourceExists) and parentExists then "
"set statusValue to \"missing\""

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Revalidate the mount before accepting “missing”

When a mounted volume disconnects but leaves its mount-point directory on the underlying filesystem, Finder observes sourceExists = false and parentExists = true, so this marks the path successful and the caller removes its catalog row even though the photo reappears after remounting. The previous post-Finder _path_confirmed_gone(..., expected_parent_dev) check explicitly rejected this device-change case; preserve equivalent mount-identity validation before accepting a missing outcome.

Useful? React with 👍 / 👎.

Comment thread vireo/app.py
Comment on lines +2031 to +2033
for filepath in ordered:
if _path_on_network_volume(filepath, network_roots):
finder_candidates.append(filepath)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Classify duplicate-cleanup paths before statting them

The separate /api/duplicates/delete-loser-files endpoint is fresh evidence beyond the earlier helper-level preflight issue: it still calls os.path.isfile(filepath) at line 22186 before invoking _trash_paths. For an unhealthy SMB share, that call can block indefinitely, so this new bounded network routing is never reached during duplicate cleanup; a failed stat can also be mistaken for a missing file and cause the row to be deleted. Route or classify these candidates before that endpoint performs filesystem I/O.

Useful? React with 👍 / 👎.

jss367 and others added 2 commits August 10, 2026 00:39
Addresses two P1 Codex review comments on the SMB-safe trash routing:

1. _trash_via_finder previously trusted its own "missing" outcome
   whenever Finder reported sourceExists=false and parentExists=true.
   An unmounted network volume leaves its mount-point directory in
   place on the underlying local FS, so parentExists=true does not
   prove the volume is still there — accepting missing would prune
   the catalog row for a photo that reappears on remount. Split its
   return shape into (moved, missing, failures) so the caller can
   revalidate mount identity. In _trash_paths, network-classified
   paths are rechecked against a fresh mount table (bounded by the
   mount subprocess timeout) and local fallbacks reuse the existing
   parent-device snapshot; a mount that disappeared or a stat we
   cannot perform is refused as unreachable.

2. /api/duplicates/delete-loser-files called os.path.isfile on every
   candidate before delegating to _trash_paths. On an unhealthy SMB
   share that stat can block indefinitely inside the request thread,
   never reaching the network-aware routing; a stat failure was also
   misread as "already missing" and dropped the DB row. Classify
   paths via the mount table first and hand network candidates
   straight through — local paths keep the existing preflight so
   manually-cleaned local losers still surface as
   "file already missing".

Regression coverage:
- _trash_paths rejects Finder's "missing" outcome when the mount
  table no longer lists the network root, when the recheck fails
  outright, and (for local fallbacks) when the parent's st_dev has
  drifted; accepts it when the mount is still healthy.
- /api/duplicates/delete-loser-files never calls os.path.isfile for
  a network-classified path.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b292a15bc6

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread vireo/app.py
Comment on lines +22245 to +22248
# the file is truly gone, Finder will report "missing" and
# the bounded mount-identity revalidation there decides
# whether to accept it as end-state or preserve the row.
trash_candidates.append((pid, filepath))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Report already-missing network losers as skipped

When a loser is already absent from a still-mounted network share, Finder returns missing and _trash_paths includes the path in successful_paths while leaving trashed at zero. This branch therefore deletes the catalog row but returns trashed: 0 with empty skipped and failed lists; trashOneLoserFile has no matching UI branch, so the card remains disabled with “Moving to Trash...” even though cleanup completed. Preserve the existing file already missing outcome for this network path, or otherwise return an explicit terminal result.

Useful? React with 👍 / 👎.

Comment thread vireo/app.py Outdated
Comment on lines +2126 to +2128
if filepath in network_finder_candidates:
current_roots = _network_volume_roots()
if current_roots is None:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reuse one mount-table recheck per Finder batch

When a retry contains many paths already moved by an earlier timed-out Finder call, every path is returned as missing and this invokes a new mount subprocess for each one. A full 20-item batch can consequently spend up to another 100 seconds in the five-second mount-query timeout, and large cleanups spawn one process per missing file, undermining the bounded batch behavior this change introduces. Query the current roots once per Finder batch and reuse that result for all missing outcomes.

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@vireo/app.py`:
- Around line 10644-10663: Update the permanent-delete branch of operate to
classify each path with _path_on_network_volume using _network_volume_roots
before any os.stat, os.path.isfile, or os.remove call. Skip network paths
entirely (fail closed), while preserving the existing permanent-delete behavior
for local paths and its progress accounting.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 03bb468e-84d0-48c1-97d9-5c77dd01dbf2

📥 Commits

Reviewing files that changed from the base of the PR and between 486d85d and 225e7e5.

📒 Files selected for processing (3)
  • vireo/app.py
  • vireo/tests/test_app.py
  • vireo/tests/test_delete_api.py

Comment thread vireo/app.py
Addresses two P2 Codex review comments on the SMB-safe trash routing:

1. When every duplicate loser is already absent on a still-mounted
   network share, `_trash_paths` places each path in `successful` but
   leaves `moved` at zero, and the duplicate-cleanup endpoint answered
   `{trashed: 0, skipped: [], failed: []}` — a silent success the
   frontend `trashOneLoserFile` has no matching branch for, so the card
   stays disabled on "Moving to Trash..." forever. `_trash_paths` now
   accepts an opt-in `already_missing_out` set that is populated for
   every path treated as successful because the requested end state
   already held (local preflight found it absent, or Finder reported it
   missing on a still-mounted volume). The delete-loser-files endpoint
   passes this in and surfaces those paths as
   `{reason: "file already missing"}` in `skipped`, matching the
   existing local-branch wording so the UI's terminal branch fires and
   the card clears.

2. `_finder_missing_is_trustworthy` previously called
   `_network_volume_roots()` for every missing path, spawning one
   `mount` subprocess per path. A retry containing 20 already-moved
   paths could therefore burn up to `_MOUNT_QUERY_TIMEOUT_SECS × 20`
   seconds in the worst case, undoing the bounded-batch guarantee the
   Finder routing is meant to give. The recheck now runs at most once
   per Finder batch (and only when the batch actually contains a
   network path with a `missing` outcome); the shared roots are passed
   into the trust check for every missing path in that batch.

Regression coverage:
- `_trash_paths` populates `already_missing_out` with the Finder
  `missing` paths on a still-mounted volume while leaving actually
  moved paths out of it.
- `_trash_paths` calls `_network_volume_roots` at most twice for a
  20-path all-missing batch (initial classification + one batch
  recheck), never one call per path.
- `/api/duplicates/delete-loser-files` returns the network loser as
  `skipped` with reason "file already missing" (and drops the orphan
  row) when the file is already absent on the still-mounted share.

jss367 commented Aug 9, 2026

Copy link
Copy Markdown
Owner Author

Both P2 Codex findings on b292a15b are addressed by 7ed82b68:

  • P2 (already-missing network losers) — _trash_paths gained an opt-in already_missing_out set; the delete-loser-files endpoint passes it in and surfaces those paths as {reason: "file already missing"} in skipped, so a network loser that is already gone on a still-mounted share now returns the same terminal outcome the local branch has always returned. trashOneLoserFile's existing "file already missing" branch fires and the card clears instead of sitting on "Moving to Trash..." forever.
  • P2 (per-path mount subprocesses) — _finder_missing_is_trustworthy no longer calls _network_volume_roots() itself; the batch loop queries the mount table at most once per Finder batch (and only when the batch actually contains a network missing outcome) and passes the shared roots into the trust check for every path. A 20-item all-missing retry now takes one mount subprocess, not twenty.

Regression coverage:

  • test_trash_paths_reuses_one_mount_recheck_per_finder_batch asserts the mount table is queried only twice for a 20-path all-missing Finder batch (initial classification + single per-batch recheck).
  • test_trash_paths_exposes_already_missing_paths_via_out_parameter asserts the new set is populated only for paths whose end state already held, not for paths actually moved.
  • test_delete_loser_files_reports_already_missing_network_losers_as_skipped asserts the endpoint surfaces the network-already-missing case as {"reason": "file already missing"} and still drops the orphan DB row.

Related trash/duplicates suites pass locally (19/19 trash tests in vireo/tests/test_app.py, all vireo/tests/test_duplicates_api.py and vireo/tests/test_delete_api.py). The pre-existing unrelated exiftool-availability failure in test_failed_import_does_not_chain is not caused by this change (verified against b292a15b).


Generated by Claude Code

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7ed82b68e5

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread vireo/app.py
Comment on lines +1734 to +1738
normalized = os.path.normpath(os.path.abspath(filepath))
for root in network_roots:
try:
if os.path.commonpath((normalized, root)) == root:
return True

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Keep symlink-backed network paths off local I/O

On macOS, when a catalog folder is a symlink such as /Users/me/photos -> /Volumes/NAS/photos, abspath preserves the symlink spelling, so this comparison does not recognize the path as belonging to the discovered NAS root. Vireo supports directly selected symlink roots in scanner.py:1584-1598; once the NAS is unhealthy, _trash_paths therefore classifies such photos as local and can block indefinitely in _snapshot_parent_device() or os.path.isfile() before reaching the bounded Finder subprocess.

Useful? React with 👍 / 👎.

Comment thread vireo/app.py Outdated
# confirm the volume is still mounted, so refuse to trust
# "missing" and let the caller retry.
return False
return _path_on_network_volume(filepath, current_network_roots)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Reject missing results after a same-root remount

If Finder reports missing while an SMB volume is detached, but the volume reconnects at the same mount point before this recheck, _path_on_network_volume() returns true solely because that root is listed again and the caller deletes the catalog row even though the photo has reappeared. Fresh evidence in this revision is that the new validation checks only lexical mount-root membership, not the original mount instance or the path's current absence; preserve mount identity or perform a bounded existence recheck before accepting this result.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 80e1c3924e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread vireo/app.py
Comment on lines 22437 to 22440
trashed, successful_paths, trash_failures = _trash_paths(
[filepath for _pid, filepath in trash_candidates],
already_missing_out=already_missing_paths,
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve the endpoint's network classification

When a share uses a custom mount point and the endpoint's first mount query succeeds but _trash_paths' immediate second query times out or fails, the helper receives network_roots=None; _path_on_network_volume then fails closed only for /Volumes paths and reclassifies the already-known custom share as local, allowing _snapshot_parent_device or os.path.isfile to block indefinitely on the unhealthy share. Fresh evidence beyond the earlier duplicate-cleanup preflight concern is that this call discards the successful classification made at lines 22392-22415 instead of passing it into _trash_paths.

Useful? React with 👍 / 👎.

Comment thread vireo/app.py Outdated
Comment on lines +2245 to +2247
return (
_path_on_network_volume(filepath, current_network_roots)
and filepath in confirmed_network_missing

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Do not treat repeated Finder absence as mount liveness

When an SMB mount remains listed while its server is unreachable, Finder can report the source as absent while cached parent metadata still reports the parent as present; _missing_paths_via_finder merely repeats the same Finder exists query, so two false-negative checks put the path in confirmed_network_missing and this condition accepts it as successful, causing callers to remove the catalog row even though the photo reappears when connectivity returns. Fresh evidence beyond the earlier detach/remount concerns is that the new confirmation adds no independent reachability or mount-identity signal.

Useful? React with 👍 / 👎.

jss367 and others added 2 commits August 10, 2026 01:41
…robe

Addresses two P1 findings from the Codex review on commit 80e1c39:

1. **Preserve endpoint's network_roots into `_trash_paths`.** The
   duplicates delete-loser-files endpoint stats the mount table once
   (line 22392) and classifies custom-mount shares
   (`/Users/me/mnt/photos`) as network-backed. It then discarded that
   result and let `_trash_paths` re-query the table — if that second
   query timed out or failed, the helper's `_path_on_network_volume`
   fallback only catches `/Volumes` paths, so an already-known custom
   mount would be silently reclassified as local and trigger the
   unbounded in-process I/O this whole routing exists to prevent.
   Thread `network_roots` through as a parameter and pass the
   endpoint's successful result.

2. **Add an independent reachability signal before accepting Finder
   "missing" outcomes.** An SMB mount can stay listed in the kernel
   mount table while its server is unreachable; both Finder queries
   (`_trash_via_finder` and `_missing_paths_via_finder`) share the
   same exists-cache, so two false negatives can put a path in
   `confirmed_network_missing` even though the photo will reappear on
   reconnect. Add `_network_root_reachable`, a bounded out-of-process
   `stat` probe on the mount root that is independent of Finder's
   cache and cannot hang the caller (subprocess is killed on timeout).
   Require it, in addition to a still-listed mount and a Finder
   confirmation, before accepting a network path as trustworthy-gone.
   Runs once per relevant mount root per Finder batch to preserve the
   bounded-batch behaviour.

Tests: adds coverage for the reachability probe (bounded subprocess,
timeout fail-closed, platform gating), for `_trash_paths` rejecting
missing when the mount root is unreachable, for the endpoint plumbing
its own `network_roots` through, and for `_trash_paths` honouring a
caller-supplied classification without re-querying. Existing tests
that rely on missing outcomes being accepted are updated to mock the
new probe.

Full suite: 2094 passed, 16 skipped, 1 unrelated failure
(`test_failed_import_does_not_chain` — missing local exiftool binary,
same as noted in the PR description).

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (5)
vireo/app.py (1)

1981-1982: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Align the empty-input return with the new tuple shape.

The docstring and the terminal return now yield (moved_paths, missing_paths, failures), where the first element is a set. The empty-input branch still returns 0 for that element. The caller at Line 2257 computes len(finder_moved_paths), which raises TypeError on the int form. _chunked never yields an empty batch today, so this is currently unreachable from _trash_paths, but the contract mismatch will break the next direct caller.

♻️ Proposed fix
     if not filepaths:
-        return 0, set(), []
+        return set(), set(), []
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@vireo/app.py` around lines 1981 - 1982, Update the empty-input branch in the
function containing the `filepaths` check to return an empty set as
`moved_paths`, matching the documented and terminal `(moved_paths,
missing_paths, failures)` tuple shape. Preserve the existing empty values for
missing paths and failures, and ensure callers such as the `finder_moved_paths`
length calculation can handle the result.
vireo/tests/test_duplicates_api.py (2)

502-506: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Replace the and expression statement with an if.

Line 505 uses a boolean expression for control flow. An explicit if states the intent and avoids a lint warning for a useless expression.

♻️ Proposed change
     def record_trash(paths, progress_callback=None, already_missing_out=None):
         trash_calls.append(list(paths))
         for path in paths:
-            real_isfile(path) and os.remove(path)
+            if real_isfile(path):
+                os.remove(path)
         return len(paths), set(paths), []
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@vireo/tests/test_duplicates_api.py` around lines 502 - 506, Update
record_trash so the real_isfile(path) check and os.remove(path) call use an
explicit if statement instead of a boolean-expression statement, preserving the
existing deletion behavior for each path.

572-581: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Also assert the trashed counter.

The test proves the skipped reason but leaves body["trashed"] unchecked. A regression that counts an already-missing loser as trashed would still pass. Add the counter assertion so the user-visible count stays correct.

♻️ Proposed change
     assert resp.status_code == 200
     body = resp.get_json()
+    assert body["trashed"] == 0
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@vireo/tests/test_duplicates_api.py` around lines 572 - 581, Extend the
duplicate-file API test assertions after validating body["skipped"] to also
verify body["trashed"] has the expected count for an already-missing loser,
while preserving the existing empty body["failed"] assertion.
vireo/tests/test_app.py (2)

4636-4670: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Derive the batch expectation from _FINDER_TRASH_BATCH_SIZE.

The test hard-codes 20 paths and expects exactly 2 mount queries. That holds only while _FINDER_TRASH_BATCH_SIZE >= 20. If the constant is lowered, _chunked yields more batches and the assertion fails for a reason unrelated to the behaviour under test.

♻️ Proposed change to bind the test to the constant
-    photos = [str(volume / f"bird-{i:02d}.NEF") for i in range(20)]
+    batch_size = app_module._FINDER_TRASH_BATCH_SIZE
+    photos = [str(volume / f"bird-{i:02d}.NEF") for i in range(batch_size)]
-    assert len(mount_call_paths) == 2
+    # One classification call at the top of _trash_paths + one recheck for
+    # the single Finder batch. Never one-per-path.
+    assert len(mount_call_paths) == 2
+    assert len(photos) > 2
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@vireo/tests/test_app.py` around lines 4636 - 4670, Update the test around
_trash_paths to generate photos using _FINDER_TRASH_BATCH_SIZE and assert the
expected mount-query count based on the resulting Finder batch count, rather
than hard-coding 20 paths and 2 calls. Preserve the expectation of one initial
classification query plus one query per Finder batch.

4593-4600: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

The _Shifted stat stub exposes only two attributes.

stat_with_shifted_dev returns an object with st_dev and st_mode only. Any future access to another field, for example st_ino or st_nlink, raises AttributeError instead of failing the assertion clearly. Copy the real result and override st_dev instead, for example with a small os.stat_result-backed wrapper or unittest.mock.Mock(spec=result, st_dev=baseline_dev + 1).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@vireo/tests/test_app.py` around lines 4593 - 4600, Update
stat_with_shifted_dev so the shifted result preserves the full real_stat result
interface while overriding only st_dev; use an os.stat_result-backed wrapper or
a spec-constrained mock based on result, and retain the original st_mode and all
other stat attributes.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@vireo/app.py`:
- Around line 1981-1982: Update the empty-input branch in the function
containing the `filepaths` check to return an empty set as `moved_paths`,
matching the documented and terminal `(moved_paths, missing_paths, failures)`
tuple shape. Preserve the existing empty values for missing paths and failures,
and ensure callers such as the `finder_moved_paths` length calculation can
handle the result.

In `@vireo/tests/test_app.py`:
- Around line 4636-4670: Update the test around _trash_paths to generate photos
using _FINDER_TRASH_BATCH_SIZE and assert the expected mount-query count based
on the resulting Finder batch count, rather than hard-coding 20 paths and 2
calls. Preserve the expectation of one initial classification query plus one
query per Finder batch.
- Around line 4593-4600: Update stat_with_shifted_dev so the shifted result
preserves the full real_stat result interface while overriding only st_dev; use
an os.stat_result-backed wrapper or a spec-constrained mock based on result, and
retain the original st_mode and all other stat attributes.

In `@vireo/tests/test_duplicates_api.py`:
- Around line 502-506: Update record_trash so the real_isfile(path) check and
os.remove(path) call use an explicit if statement instead of a
boolean-expression statement, preserving the existing deletion behavior for each
path.
- Around line 572-581: Extend the duplicate-file API test assertions after
validating body["skipped"] to also verify body["trashed"] has the expected count
for an already-missing loser, while preserving the existing empty body["failed"]
assertion.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: f52609f4-8c4d-42f3-bb83-ee29a48d8cff

📥 Commits

Reviewing files that changed from the base of the PR and between 225e7e5 and b2c5e2c.

📒 Files selected for processing (3)
  • vireo/app.py
  • vireo/tests/test_app.py
  • vireo/tests/test_duplicates_api.py

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e4fcf678bf

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread vireo/app.py Outdated
# respond to a bounded out-of-process stat, treat "missing"
# as a false negative from an unreachable server rather than
# as evidence the photo is gone.
return _path_on_network_volume(filepath, reachable_network_roots)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Probe the deepest matching network mount

When network mounts are nested (for example, an unavailable SMB mount at /Volumes/NAS/archive beneath a reachable mount at /Volumes/NAS), this accepts the path if any reachable ancestor root matches it. Because relevant_roots also selects an arbitrary first match from a set, the outer root may be the only root probed; it can therefore validate Finder's false missing result for the unavailable inner mount, causing the catalog row to be removed before the photo reappears. Associate each path with its deepest matching mount and require that exact root to be reachable.

Useful? React with 👍 / 👎.

@github-actions
github-actions Bot enabled auto-merge (squash) August 9, 2026 23:55
Codex flagged that ``_trash_paths`` was accepting a Finder ``missing``
outcome as trustworthy whenever *any* reachable network root contained
the path — an outer share could vouch for a detached inner share
mounted underneath it (e.g. ``/Volumes/NAS/archive`` beneath
``/Volumes/NAS``). Iterating ``batch_network_roots`` (a set) with a
first-match ``break`` also picked the reachable-root probe target in
arbitrary order, so the outer root was often the only one probed at
all. That would prune the catalog row for a photo that reappears on
reconnect.

Introduce ``_deepest_network_root_for_path``: longest-common-prefix
matching over ``network_roots`` (with the same one-symlink-prefix
expansion loop as ``_path_on_network_volume``) that returns the deepest
matching root. ``_trash_paths`` now builds a per-batch
``path_to_deepest_root`` mapping, probes reachability of each distinct
deepest root, and passes the mapping into
``_finder_missing_is_trustworthy``; trust now requires the path's
*deepest* matching root to be in ``reachable_network_roots`` rather
than merely being any-reachable-root-membership.

Add ``test_trash_paths_rejects_missing_when_nested_inner_mount_unreachable``:
two paths on nested mounts (outer reachable, inner detached); asserts the
outer path is accepted, the inner is preserved for retry, and reachability
was probed for *both* deepest roots — proving we no longer rely on set
iteration order picking a single winner.

jss367 commented Aug 10, 2026

Copy link
Copy Markdown
Owner Author

The P1 Codex finding on e4fcf678 ("Probe the deepest matching network mount") is addressed by 07241e44:

  • Introduced _deepest_network_root_for_path: longest-common-prefix matching over network_roots, with the same one-symlink-prefix expansion loop as _path_on_network_volume, returning the deepest matching root.
  • _trash_paths now builds a per-batch path_to_deepest_root mapping, probes reachability of each distinct deepest root, and passes the mapping into _finder_missing_is_trustworthy. Trust now requires the path's deepest matching root to be in reachable_network_roots — a reachable outer share can no longer vouch for a nested detached inner share (e.g. /Volumes/NAS/archive beneath /Volumes/NAS). The old code iterated batch_network_roots (a set) with a first-match break, so which root got probed was set-iteration-order-dependent.

Regression coverage:

  • test_trash_paths_rejects_missing_when_nested_inner_mount_unreachable sets up two paths on nested mounts (outer reachable, inner detached); asserts the outer path is accepted as missing, the inner is preserved for retry, and _network_root_reachable was called for both deepest roots — proving we no longer rely on set iteration order picking a single winner.

The other Codex findings on earlier commits (line 1798 symlink, line 2042 mount-identity, line 2189 duplicate-cleanup preflight, line 22487 already-missing losers, line 22519 preserve endpoint classification) were already addressed by later commits in this branch — verified against current HEAD.

Full trash/network suite passes (28/28 in vireo/tests/test_app.py). Broader mandated suite: 2097 pass, 16 skip; the pre-existing unrelated test_failed_import_does_not_chain still fails only because exiftool isn't available in this environment (same failure noted on b292a15b).


Generated by Claude Code

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 38f80265b1

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread vireo/app.py Outdated
Comment on lines +2214 to +2215
if network_roots is None:
network_roots = _network_volume_roots()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve an explicitly failed mount snapshot

When the duplicate-cleanup endpoint's first mount query fails, it passes network_roots=None after correctly classifying /Volumes/... paths via the fail-closed fallback, but this branch interprets that explicit failure as “not supplied” and queries again. If the second query succeeds after the share has detached and returns no matching root, the path is reclassified as local; the underlying mount-point directory can then make the absence preflight succeed, causing the catalog row to be deleted even though the photo reappears on remount. Fresh evidence beyond the earlier classification comment is the new parameter's conflation of its default with the mount-query failure value; use a distinct sentinel so an explicitly passed None remains fail-closed.

Useful? React with 👍 / 👎.

Codex flagged (P1) that _trash_paths(..., network_roots=None) treats
the fail-closed signal None — passed by callers whose own
_network_volume_roots() query failed — as if the caller had omitted
the argument, then re-queries the mount table. If that second query
succeeds after the share has detached, the caller's already-correct
classification of a custom-mount path (e.g. /Users/me/mnt/photos,
which the /Volumes fallback in _path_on_network_volume had
flagged network) is silently overwritten with an empty or mismatched
result and the path is reclassified as local — reintroducing the
unbounded in-process I/O this routing exists to prevent.

Add a module-level _NETWORK_ROOTS_UNSET sentinel as the default for
the network_roots parameter so None remains distinguishable from
'not supplied'. Only re-query when the caller actually omitted the
argument; an explicit None passes straight through and preserves
the fail-closed classification.

Regression test locks the behaviour: an explicit network_roots=None
routes a /Volumes/... path through Finder without touching
_network_volume_roots again.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4966747247

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread vireo/app.py Outdated
Comment on lines +2417 to +2419
for root in set(path_to_deepest_root.values()):
if _network_root_reachable(root):
reachable_network_roots.add(root)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Bound reachability probes across the whole Finder batch

When one Finder batch contains missing paths from several network mounts, this loop runs each five-second _network_root_reachable() probe serially. With the configured 20-item batch size, 20 unavailable roots add up to 100 seconds before the subsequent Finder recheck, so a bulk delete spanning multiple shares can still appear hung despite each individual subprocess having a timeout. Run these per-root probes concurrently or enforce one timeout budget for the entire batch.

Useful? React with 👍 / 👎.

jss367 and others added 2 commits August 10, 2026 02:38
Addresses Codex P2 review on #1446 (commit 4966747): the per-batch
reachability loop ran each `_network_root_reachable()` probe serially,
so a Finder batch spanning many unavailable roots could accumulate
`len(distinct_roots) × _MOUNT_QUERY_TIMEOUT_SECS` seconds of hang time
before the subsequent Finder recheck. With `_FINDER_TRASH_BATCH_SIZE = 20`
and a 5s probe timeout, a full batch of unreachable-root paths would
add up to ~100s and undermine the bounded-batch behaviour this code
exists to establish.

Run the distinct-root probes concurrently via a `ThreadPoolExecutor`
sized to the batch's root count. Each probe still enforces its
individual timeout, so wall-clock is bounded by one probe timeout
rather than the sum. Regression test uses a `threading.Barrier` to
prove the probes run concurrently — a serial implementation would
never satisfy the barrier's party count and the wait would raise.

jss367 commented Aug 10, 2026

Copy link
Copy Markdown
Owner Author

The P2 Codex finding on 4966747 ("Bound reachability probes across the whole Finder batch") is addressed by 508773e:

  • P2_trash_paths now probes each distinct deepest network root via concurrent.futures.ThreadPoolExecutor instead of a serial for loop. Each _network_root_reachable() still runs its own bounded stat subprocess with _MOUNT_QUERY_TIMEOUT_SECS, but wall-clock is now bounded by one probe timeout rather than len(distinct_roots) × timeout. A full 20-item Finder batch spanning unreachable shares completes its reachability sweep in ~5s instead of up to ~100s, so the batch's Finder recheck can no longer be starved by the pre-recheck reachability loop.

Regression coverage:

  • test_trash_paths_probes_distinct_roots_concurrently uses a threading.Barrier sized to the number of distinct roots (6). A serial implementation would never satisfy the barrier's party count and wait() would raise BrokenBarrierError on the 5-second timeout — the test asserts the trash run completes successfully, which only holds when the probes execute concurrently.
  • The pre-existing test_trash_paths_reuses_one_mount_recheck_per_finder_batch (single root) and test_trash_paths_rejects_missing_when_nested_inner_mount_unreachable (multi-root correctness) continue to pass, so the concurrency change did not regress single-root or nested-root behaviour.

The remaining unresolved Codex threads on earlier commits (line 1807 symlink, line 2096 mount-identity, line 2243 duplicate-cleanup preflight, line 22556 already-missing losers, line 22590 preserve endpoint classification) were already addressed by later commits — verified against current HEAD.

Full mandated suite: 2099 pass, 16 skip. The pre-existing unrelated test_failed_import_does_not_chain still fails only because exiftool is not available in this environment (same environmental failure noted on b292a15b / e4fcf678, reproduces on the pre-change commit 4966747).


Generated by Claude Code

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 508773ed74

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread vireo/app.py
Comment on lines +1760 to +1764
result = run(
["/usr/bin/stat", "-f", "%HT", root],
capture_output=True, text=True,
timeout=timeout,
**no_window_kwargs(),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Avoid synchronously reaping a wedged stat process

When /usr/bin/stat becomes stuck in an uninterruptible SMB filesystem call, subprocess.run(..., timeout=...) is not a hard deadline: after the timeout Python sends SIGKILL and then synchronously calls wait(), which can remain blocked until the kernel call returns. The executor context subsequently waits for that worker as well, so deleting from the exact kind of unavailable share addressed here can still hang indefinitely. Fresh evidence beyond the earlier batch-budget concern is that the new reachability implementation uses this kill-and-wait path; use a probe design that can abandon a timed-out child without synchronously reaping it.

Useful? React with 👍 / 👎.

Comment thread vireo/app.py
Comment on lines +1818 to +1822
else:
for root in network_roots:
try:
if os.path.commonpath((normalized, root)) == root:
return True

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve detached network mounts as network paths

When a NAS has already detached before mount discovery, mount succeeds but returns a set with no matching root, so /Volumes/NAS/photo.jpg falls through this loop and is classified as local. The subsequent parent snapshot and missing-file checks both inspect the underlying local mount-point directory, see the same device, and accept the photo as already gone, causing callers to delete its catalog row even though it reappears after remounting. Fresh evidence beyond the earlier remount findings is the successful-empty-set classification path; a missing current mount-table entry must not by itself prove that a catalog path is local.

Useful? React with 👍 / 👎.

Comment thread vireo/app.py
Comment on lines +1813 to +1817
if network_roots is None:
# Mount discovery failed. Prefer bounded Finder handling for any
# /Volumes path, including one reached through a local symlink.
if _volume_root_for_path(normalized) is not None:
return True

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Fail closed for custom mounts when discovery fails

When the bounded mount query fails and returns None, this fallback protects only paths under /Volumes. A network share mounted at a supported custom location such as /Users/me/mnt/photos is therefore classified as local and proceeds to _snapshot_parent_device() and os.path.isfile(), either of which can block the request indefinitely on an unhealthy share. Treat all macOS candidates conservatively when discovery itself failed, rather than interpreting non-/Volumes paths as local.

Useful? React with 👍 / 👎.

@github-actions
github-actions Bot merged commit c1aa878 into main Aug 10, 2026
22 checks passed

jss367 commented Aug 10, 2026

Copy link
Copy Markdown
Owner Author

The three P1 Codex findings on 508773ed are addressed by b3a8197:

  • P1 (avoid synchronously reaping a wedged stat process) — _network_root_reachable no longer calls subprocess.run(..., timeout=...); it drives Popen directly and, on timeout, hands the child to the new _abandon_stuck_subprocess helper, which SIGKILLs the process group and reaps on a daemon thread. subprocess.run's post-kill wait() would keep the caller blocked as long as /usr/bin/stat was stuck in an uninterruptible SMB kernel call — the exact case this probe exists to detect — and because the reachability sweep runs inside a ThreadPoolExecutor, the executor context then blocked on the same worker.
  • P1 (preserve detached network mounts as network paths) — _path_on_network_volume now fails closed on any macOS /Volumes/... path regardless of the discovered set. Previously, a NAS that had detached before mount discovery ran left its stub mount-point directory in place while mount returned no matching root, so /Volumes/NAS/photo.jpg fell through to the local branch; _snapshot_parent_device and os.path.isfile then succeeded against the stub directory, and callers pruned catalog rows for photos that reappear on remount. A /Volumes path missing from the current mount table is more plausibly a detached share than a genuinely local /Volumes subdirectory, so we route it through Finder.
  • P1 (fail closed for custom mounts when discovery fails) — the network_roots is None fallback previously protected only /Volumes/..., leaving supported custom mount locations like /Users/me/mnt/photos free to hang the caller on an in-process stat. Now every macOS candidate is treated as network-backed when discovery itself failed — we cannot distinguish a custom network mount from an ordinary local path, and the fail-closed cost (some Finder overhead on genuinely local files during a rare discovery failure) is smaller than the unbounded-hang cost on an unhealthy share.

Regression coverage:

  • test_network_root_reachable_abandons_child_that_hangs_on_kill simulates the uninterruptible-kernel-call case with a fake Popen whose wait() blocks forever, then asserts the probe returns within a 5s deadline from a worker thread — the previous synchronous wait() path would fail this test in production against any wedged SMB mount.
  • test_path_on_network_volume_fails_closed_on_detached_volumes_mount asserts /Volumes/NAS/photo.jpg is classified as network-backed even when the discovered roots either omit it or are entirely empty.
  • test_path_on_network_volume_fails_closed_for_custom_mounts_when_discovery_fails asserts /Users/me/mnt/photos/bird.NEF is classified as network-backed when network_roots is None on macOS, and remains local on non-macOS platforms.

The three existing test_network_root_reachable_* tests were updated to inject a fake Popen (via the new _popen kwarg) rather than a fake subprocess.run. test_network_mount_discovery_failure_avoids_direct_volume_rename gained an explicit sys.platform="darwin" monkeypatch since the /Volumes fail-closed fallback is now correctly darwin-gated — no bounded Finder alternative exists on Linux/Windows, so classifying a path as network-backed there would just make callers fail the operation for no benefit.

Full test_app.py suite passes (451/451), plus test_duplicates_api.py and test_delete_api.py (73 pass, 3 skip). Mandated suite excluding test_app: 1651 pass, 16 skip. The pre-existing unrelated test_failed_import_does_not_chain still fails only because exiftool is not available in this environment (same environmental failure noted on 4966747 / 508773e).


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

claude-agent PR managed by Claude PR Agent

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants