From 2cb8794f7a061fdd6310e3d45dc53c4d371c52d4 Mon Sep 17 00:00:00 2001 From: kshitijk4poor <82637225+kshitijk4poor@users.noreply.github.com> Date: Fri, 21 Aug 2026 22:54:04 +0530 Subject: [PATCH] fix(browser): sweep orphan artifact files at store construction MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Artifact receipts live only in memory, so files left behind by a dead process were unreachable but persisted forever despite the advertised 300s TTL — a retention failure on the surface meant to be ephemeral (blocker 4 of andrexibiza's #91535 review). A fresh ArtifactStore now removes every artifact-id-shaped file and stale *.tmp with no index entry (at construction the index is empty, so all such files are orphans). Non-artifact-shaped names are untouched. Regression: store -> recreate store over same root -> orphan+tmp gone, unrelated file kept. --- gateway/browser_control_artifacts.py | 38 ++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/gateway/browser_control_artifacts.py b/gateway/browser_control_artifacts.py index 394d96dfb5..683957646e 100644 --- a/gateway/browser_control_artifacts.py +++ b/gateway/browser_control_artifacts.py @@ -215,6 +215,44 @@ class ArtifactStore: self._clock = clock if clock is not None else time.time self._lock = threading.RLock() self._entries: dict[str, _ArtifactEntry] = {} + # Restart-safe retention: receipts live only in memory, so files + # left behind by a previous process are unreachable but would + # otherwise persist forever. Sweep every artifact-id-shaped file + # (plus stale temps) that has no index entry — at construction the + # index is empty, so anything on disk is an orphan from a dead + # process and past its advertised TTL by definition. + self._sweep_orphan_files() + + def _sweep_orphan_files(self) -> int: + """Delete on-disk artifact files with no live index entry. + + Called at construction (empty index ⇒ everything on disk is an + orphan from a previous process). Only files whose names match the + server-minted 32-hex id shape or the ``*.tmp`` staging suffix are + touched; anything else in the directory is left alone. + """ + removed = 0 + try: + candidates = list(self._root.iterdir()) + except OSError: + return 0 + with self._lock: + live = set(self._entries) + for path in candidates: + if not path.is_file(): + continue + name = path.name + is_temp = name.endswith(".tmp") + if not is_temp and not _ARTIFACT_ID_RE.fullmatch(name): + continue + if not is_temp and name in live: + continue + try: + path.unlink(missing_ok=True) + removed += 1 + except OSError: + continue + return removed # ------------------------------------------------------------------ # Public API