From 90a3e73daf18448ee5239b1e19d92ded0dbc77ae Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Mon, 27 Apr 2026 00:36:33 -0700 Subject: [PATCH] fix(debug): sweep expired paste.rs uploads on a real timer (#16431) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously 'hermes debug share' uploads only got DELETEd when the user ran 'hermes debug share' again — opportunistic-sweep-on-invoke was the only cleanup path. A user who uploaded once and never ran debug again left pastes up until paste.rs's retention kicked in (which, empirically, never actually expires them). Hook _sweep_expired_pastes into the gateway cron ticker at the same hourly cadence as the image/document cache cleanups. The opportunistic sweep in 'hermes debug share' stays as a fallback for CLI-only users who never start the gateway. --- gateway/run.py | 16 +++++++++++++++- hermes_cli/debug.py | 16 +++++++++++----- 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/gateway/run.py b/gateway/run.py index 222432657c..01eb529693 100644 --- a/gateway/run.py +++ b/gateway/run.py @@ -11153,13 +11153,16 @@ def _start_cron_ticker(stop_event: threading.Event, adapters=None, loop=None, in cron delivery path so live adapters can be used for E2EE rooms. Also refreshes the channel directory every 5 minutes and prunes the - image/audio/document cache once per hour. + image/audio/document cache + expired ``hermes debug share`` pastes + once per hour. """ from cron.scheduler import tick as cron_tick from gateway.platforms.base import cleanup_image_cache, cleanup_document_cache + from hermes_cli.debug import _sweep_expired_pastes IMAGE_CACHE_EVERY = 60 # ticks — once per hour at default 60s interval CHANNEL_DIR_EVERY = 5 # ticks — every 5 minutes + PASTE_SWEEP_EVERY = 60 # ticks — once per hour logger.info("Cron ticker started (interval=%ds)", interval) tick_count = 0 @@ -11200,6 +11203,17 @@ def _start_cron_ticker(stop_event: threading.Event, adapters=None, loop=None, in except Exception as e: logger.debug("Document cache cleanup error: %s", e) + if tick_count % PASTE_SWEEP_EVERY == 0: + try: + deleted, remaining = _sweep_expired_pastes() + if deleted: + logger.info( + "Paste sweep: deleted %d expired paste(s), %d pending", + deleted, remaining, + ) + except Exception as e: + logger.debug("Paste sweep error: %s", e) + stop_event.wait(timeout=interval) logger.info("Cron ticker stopped") diff --git a/hermes_cli/debug.py b/hermes_cli/debug.py index 8915d8a6a7..e2eac544f2 100644 --- a/hermes_cli/debug.py +++ b/hermes_cli/debug.py @@ -45,8 +45,13 @@ def _pending_file() -> Path: Each entry: ``{"url": "...", "expire_at": }``. Scheduled DELETEs used to be handled by spawning a detached Python process per paste that slept for 6 hours; those accumulated forever if the user - ran ``hermes debug share`` repeatedly. We now persist the schedule - to disk and sweep expired entries on the next debug invocation. + ran ``hermes debug share`` repeatedly. + + Deletion is now driven by the gateway's cron ticker + (``gateway/run.py::_start_cron_ticker``) which calls + ``_sweep_expired_pastes`` once per hour. ``hermes debug share`` also + runs an opportunistic sweep on entry as a fallback for CLI-only users + who never start the gateway. """ return get_hermes_home() / "pastes" / "pending.json" @@ -223,9 +228,10 @@ def _schedule_auto_delete(urls: list[str], delay_seconds: int = _AUTO_DELETE_SEC interpreters that never exited until the sleep completed. The replacement is stateless: we append to ``~/.hermes/pastes/pending.json`` - and rely on opportunistic sweeps (``_sweep_expired_pastes``) called from - every ``hermes debug`` invocation. If the user never runs ``hermes debug`` - again, paste.rs's own retention policy handles cleanup. + and the gateway's cron ticker sweeps expired entries once per hour. + ``hermes debug share`` also runs an opportunistic sweep as a fallback + for CLI-only users. If neither runs again, paste.rs's own retention + policy handles cleanup. """ _record_pending(urls, delay_seconds=delay_seconds)