feat(discover): full reset — clear + rebuild suggestions

POST /api/discover/reset sets discover.resetRequested; the worker, at the
start of the next sweep, clears all PENDING suggestions + contributions +
seed throttles and nulls WatchedArtist.lastDiscoveredAt (keeping
followed/wanted/dismissed decisions), then regenerates everything with current
logic. A 'Reset' button on /discover (2-step confirm) triggers it.

worker 247, web 190 tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jonathan
2026-07-15 00:20:17 +02:00
parent 7e27654b5a
commit 9915bfbda2
6 changed files with 113 additions and 8 deletions
+12
View File
@@ -315,6 +315,18 @@ def _recompute_suggestions(conn: psycopg.Connection, affected: set[str],
return surfaced
def reset_pending(conn: psycopg.Connection) -> None:
"""Clear discovery state so the next sweep rebuilds from scratch with current logic. Drops
all PENDING suggestions (followed/wanted/dismissed user decisions are kept), every seed
contribution + library-seed throttle, and marks each watched artist due again."""
with conn.cursor() as cur:
cur.execute("DELETE FROM \"DiscoverySuggestion\" WHERE status = 'pending'")
cur.execute('DELETE FROM "DiscoverySeedContribution"')
cur.execute('DELETE FROM "DiscoverySeed"')
cur.execute('UPDATE "WatchedArtist" SET "lastDiscoveredAt" = NULL')
conn.commit()
def run_discovery(conn: psycopg.Connection, sources: list[SimilaritySource],
browser: MbBrowser, cfg: DiscoveryConfig, popularity=None) -> DiscoveryResult:
"""Record each seed's similar-artist contributions, then recompute affected
+5 -1
View File
@@ -10,7 +10,7 @@ from lyra_worker.claim import claim_next, reclaim_stuck_jobs
from lyra_worker.config import get_config
from lyra_worker.crypto import secret_key_problem
from lyra_worker.db import wait_for_db
from lyra_worker.discovery import DiscoveryConfig, DiscoveryResult, run_discovery
from lyra_worker.discovery import DiscoveryConfig, DiscoveryResult, reset_pending, run_discovery
from lyra_worker.library import clear_staging_root
from lyra_worker.monitor import MonitorConfig, fulfill_owned_releases, reconcile, sweep
from lyra_worker.pipeline import run_pipeline
@@ -147,6 +147,10 @@ def maybe_run_discovery(conn, sources, browser, config, dcfg, now, last_discover
starting = not in_progress
if starting: # begin a fresh sweep
if str(config.get("discover.resetRequested", "")).strip().lower() in _TRUE:
reset_pending(conn) # wipe stale pending suggestions before rebuilding
_set_config(conn, "discover.resetRequested", "false")
print("worker: discovery reset — cleared pending suggestions, rebuilding", flush=True)
_set_config(conn, "discover.inProgress", "true")
_set_config(conn, "discover.requested", "false")
+29
View File
@@ -277,3 +277,32 @@ def test_run_discovery_processes_at_most_chunk_size_seeds(conn):
with conn.cursor() as cur:
cur.execute('SELECT count(*) FROM "WatchedArtist" WHERE "lastDiscoveredAt" IS NOT NULL')
assert cur.fetchone()[0] == 2
def test_reset_pending_clears_state_and_marks_seeds_due(conn):
from lyra_worker.discovery import reset_pending
from tests.conftest import insert_discovery_suggestion
insert_watched_artist(conn, mbid="s1", name="Seed", last_discovered_sql="now()")
insert_discovery_suggestion(conn, kind="artist", artist_mbid="c1", status="pending")
insert_discovery_suggestion(conn, kind="artist", artist_mbid="c2", status="dismissed")
insert_discovery_suggestion(conn, kind="artist", artist_mbid="c3", status="wanted")
with conn.cursor() as cur:
cur.execute('INSERT INTO "DiscoverySeedContribution" (id, "candidateMbid", "candidateName", '
'"seedMbid", score, sources, "updatedAt") VALUES '
"(gen_random_uuid()::text, 'c1', 'C1', 's1', 1.0, '{}', now())")
cur.execute('INSERT INTO "DiscoverySeed" (mbid, "lastDiscoveredAt") VALUES (%s, now())', ("libseed",))
conn.commit()
reset_pending(conn)
with conn.cursor() as cur:
cur.execute("SELECT count(*) FROM \"DiscoverySuggestion\" WHERE status='pending'")
assert cur.fetchone()[0] == 0 # pending cleared
cur.execute("SELECT count(*) FROM \"DiscoverySuggestion\" WHERE status IN ('dismissed','wanted')")
assert cur.fetchone()[0] == 2 # user decisions kept
cur.execute('SELECT count(*) FROM "DiscoverySeedContribution"')
assert cur.fetchone()[0] == 0
cur.execute('SELECT count(*) FROM "DiscoverySeed"')
assert cur.fetchone()[0] == 0
cur.execute('SELECT count(*) FROM "WatchedArtist" WHERE "lastDiscoveredAt" IS NULL')
assert cur.fetchone()[0] == 1 # seed due again