diff --git a/skills/open-kimi-ppt/scripts/export_pptx.py b/skills/open-kimi-ppt/scripts/export_pptx.py
index 92bd9ae..1bf8c1c 100755
--- a/skills/open-kimi-ppt/scripts/export_pptx.py
+++ b/skills/open-kimi-ppt/scripts/export_pptx.py
@@ -461,18 +461,23 @@ def find_download(
last_sizes: Dict[Path, int] = {}
stable: Dict[Path, int] = {}
while time.monotonic() < deadline:
- candidates: List[Path] = []
+ # Snapshot stats while collecting and tolerate races everywhere: the
+ # search roots include the live Downloads folder, where Chrome renames
+ # .crdownload files away between directory listing and stat().
+ entries: List[Tuple[Path, float, int]] = []
for root in search_roots:
if not root.exists():
continue
- candidates.extend(path for path in root.rglob("*") if path.is_file())
- for path in sorted(candidates, key=lambda item: item.stat().st_mtime, reverse=True):
- try:
- stat = path.stat()
- size = stat.st_size
- if since is not None and stat.st_mtime < since:
+ for path in root.rglob("*"):
+ if not path.is_file():
continue
- except OSError:
+ try:
+ info = path.stat()
+ except OSError:
+ continue
+ entries.append((path, info.st_mtime, info.st_size))
+ for path, mtime, size in sorted(entries, key=lambda entry: entry[1], reverse=True):
+ if since is not None and mtime < since:
continue
if size == last_sizes.get(path) and size > 0:
stable[path] = stable.get(path, 0) + 1
diff --git a/skills/open-kimi-ppt/tests/test_export_pptx.py b/skills/open-kimi-ppt/tests/test_export_pptx.py
index 24d5009..29640bb 100755
--- a/skills/open-kimi-ppt/tests/test_export_pptx.py
+++ b/skills/open-kimi-ppt/tests/test_export_pptx.py
@@ -163,6 +163,37 @@ class ExportPptxTests(unittest.TestCase):
found = MODULE.find_download([root], timeout=2.0, since=since)
self.assertEqual(found.resolve(), new.resolve())
+ def test_find_download_survives_files_vanishing_mid_scan(self):
+ # Chrome renames "*.crdownload" files away between directory listing
+ # and stat(); a vanished file must be skipped, not crash the export.
+ with tempfile.TemporaryDirectory() as name:
+ root = Path(name)
+ deck = root / "deck.pptx"
+ with zipfile.ZipFile(deck, "w") as archive:
+ archive.writestr(
+ "[Content_Types].xml",
+ ''
+ '',
+ )
+ archive.writestr("ppt/presentation.xml", "")
+ ghost = root / "ghost.crdownload"
+ ghost.write_bytes(b"partial download")
+
+ real_stat = Path.stat
+ seen = {"count": 0}
+
+ def racy_stat(self, **kwargs):
+ if self.name == "ghost.crdownload":
+ seen["count"] += 1
+ if seen["count"] > 1:
+ raise FileNotFoundError(2, "vanished mid-scan", str(self))
+ return real_stat(self, **kwargs)
+
+ with patch.object(Path, "stat", racy_stat):
+ found = MODULE.find_download([root], timeout=2.0)
+ self.assertEqual(found.resolve(), deck.resolve())
+
def test_browser_open_does_not_pass_download_path(self):
session = MODULE.BrowserSession(
"/bin/agent-browser",