From cc4199c79e349485d1a40bb8257dd26eacad3330 Mon Sep 17 00:00:00 2001 From: kami Date: Thu, 30 Jul 2026 23:36:24 +0400 Subject: [PATCH] fix: repair decayBeliefs CTE so belief decay actually runs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The CTE was `WITH halflives AS (SELECT profile, CASE profile ...)` with no FROM clause. Postgres rejects it with 42703 (column "profile" does not exist) on every hourly invocation, so the temporal dimension of the recommendation engine had never executed once — obsession (14d half-life) and contextual (7d) never faded. Rewritten as `WITH halflives(profile, halflife_sec) AS (VALUES ...)`, half-lives preserved exactly. One deliberate semantic change: the broken CASE had an `ELSE 30 * 86400` fallback, so an unrecognised profile would have decayed on a 30-day half-life. The VALUES join leaves unknown profiles undecayed instead. Today that is a no-op (only `forgotten`, already excluded by the WHERE), but a future profile added without a half-life will now conspicuously not decay rather than quietly decaying at an arbitrary rate. Verified against a scratch PG16 with one belief per profile aged exactly one half-life: UPDATE 2, obsession and contextual halved, forgotten and a fresh longterm untouched. Against the live DB (in a rolled-back transaction) the fix reports UPDATE 843. NOTE ON ROLLOUT: the first successful run applies ~23 days of accrued decay at once, cutting obsession beliefs to ~0.32x. That is correct behaviour, but recommendations will shift visibly. Expected, not a regression. REVIEW-2026-07-30.md finding 3. Co-Authored-By: Claude Opus 5 --- backend/src/services/db.service.ts | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/backend/src/services/db.service.ts b/backend/src/services/db.service.ts index f560f65..f7b2c3f 100644 --- a/backend/src/services/db.service.ts +++ b/backend/src/services/db.service.ts @@ -1790,16 +1790,16 @@ export class DbService { */ async decayBeliefs(): Promise { const res = await this.pgClient.query(` - WITH halflives AS ( - SELECT profile, - CASE profile - WHEN 'longterm' THEN 365 * 86400 - WHEN 'obsession' THEN 14 * 86400 - WHEN 'discovery' THEN 30 * 86400 - WHEN 'negative' THEN 180 * 86400 - WHEN 'contextual' THEN 7 * 86400 - ELSE 30 * 86400 - END AS halflife_sec + -- NOTE: this MUST be a VALUES list, not "SELECT profile, CASE profile ...", + -- which has no FROM clause and is rejected by Postgres with 42703 + -- (column "profile" does not exist) on every single run. + WITH halflives(profile, halflife_sec) AS ( + VALUES + ('longterm', 365 * 86400), + ('obsession', 14 * 86400), + ('discovery', 30 * 86400), + ('negative', 180 * 86400), + ('contextual', 7 * 86400) ) UPDATE listener_beliefs lb SET value = GREATEST(-1.0, LEAST(1.0, lb.value * POWER(0.5,