Fix spurious stderr traceback when the output dir rename target already exists - #41
Conversation
…r collision exists _rename_output_dir attempts directory.rename(new_dir) unconditionally and falls back to the original path on any exception. On POSIX, renaming into an existing non-empty directory raises OSError (Directory not empty) -- which two summarize/resume runs landing on the same title slug will hit. The except branch already returns the correct path, but it logs via logger.warning(..., exc_info=True) with no configured handler, so Python's default lastResort handler prints a full traceback to stderr on every run that happens to collide -- non-blocking, but reads as a crash. Add an upfront check: if the rename target already exists and has content, return the original directory directly, without ever attempting the rename or logging anything. Narrow the fallback except to OSError (was a bare Exception, which also swallowed programming errors) and drop its level to debug, so genuinely unexpected OSErrors (permissions, cross-device links) stay covered but silent by default too. 4 new tests in TestRenameOutputDir. The two that exercise the actual bug assert on mechanism, not just return value: Path.rename is never even attempted on a content-collision, and no WARNING-level log record is emitted anywhere in _rename_output_dir. Verified against the unmodified function first (skips-cleanly and unexpected-os-error cases fail there for exactly this reason: rename is attempted and a WARNING is logged), then against the fix (green), then reverted the fix and confirmed both tests fail identically again before restoring it. Full suite: 260 passed (was 256), ruff check clean.
A6's root cause is no longer a hypothesis: a workflow_dispatch run on the now-public repo (31394710701) died in 5 seconds with the check-run annotation 'The job was not started because your account is locked due to a billing issue', so the lock is account-level and public-repo free minutes do not bypass it. Billable timing for the run is 0 ms. New post-flip status section: flip executed by Yanis on 2026-08-10, rendered-README GIFs verified decoded in a real browser, release v0.13.2 asset anonymously downloadable, leak scan of the five rendered surfaces clean, description + 8 topics set, profile pin is UI-only (no GraphQL mutation exists), and upstream PR paberr/ownscribe#41 opened on explicit go. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
| original if renaming isn't safely possible (e.g. the target already | ||
| exists with content, or the directory lives outside a renamable tree).""" | ||
| new_dir = directory.parent / f"{directory.name}_{title_slug}" | ||
| if new_dir.exists() and any(new_dir.iterdir()): |
There was a problem hiding this comment.
Should this check be inside the try too? new_dir.exists() and iterdir() sit outside the except OSError now, so if either raises (e.g. a permission error stat'ing the target), it'd propagate uncaught instead of falling back to the original directory like before.
Generated by Claude Code
| except Exception: | ||
| logging.getLogger(__name__).warning("Could not rename output directory", exc_info=True) | ||
| except OSError: | ||
| logging.getLogger(__name__).debug("Could not rename output directory", exc_info=True) |
There was a problem hiding this comment.
This also downgrades logging to debug for any other OSError here, not just the known collision case (the test at test_returns_original_on_unexpected_os_error locks that in for e.g. a cross-device rename failure too). Shouldn't a genuinely unexpected error still warning, and only the known collision case stay silent?
Generated by Claude Code
The collision check ran before the try block, so a file or an unreadable path at the rename target raised out of _rename_output_dir instead of falling back to the original directory. Unexpected OSErrors now warn without a traceback; only the expected collision stays silent.
|
Thanks a lot for the work, I implemented a couple of minor fixes on top and will merge it next. :) |
Summary
_rename_output_dirprints a full, scary-looking stack trace to stderr on everysummarizeorresumerun whose generated title slug collides with an existing, non-empty output directory,even though the run itself succeeds. This PR makes that a silent, expected skip instead.
Before / after
Before: reproduced directly: pre-create a non-empty directory at the rename target, then run
summarize/resumeagainst a source directory that would rename into it.Path.renameraisesOSError: Directory not empty(errno 66 on macOS/BSD, ENOTEMPTY on Linux), caught by the existingexcept Exception, which logs vialogger.warning(..., exc_info=True). With no logging handlerconfigured, Python's default
lastResorthandler prints the full traceback to stderr. The runstill completes and the summary still saves at the original (unrenamed) path, but the traceback
reads as a crash.
After: the same collision is detected upfront (
new_dir.exists() and any(new_dir.iterdir()))and the function returns the original directory immediately, without attempting the rename or
logging anything. The diff itself is the source of truth.
Tests executed
(
ruff format --checkflags 16 pre-existing files repo-wide, unrelated to this change; verifiedpresent on the unmodified
pipeline.pyat the same commit before this diff was applied.)What this PR does not do
_2/_Nsuffixing for repeated collisions on the same final name: that's a separate,larger renaming feature and out of scope here.
pipeline.py(title generation, transcription, summarizationflow are all untouched).
still succeeds either way; this PR only removes the spurious stderr traceback.