fix: repair decayBeliefs CTE so belief decay actually runs

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 <noreply@anthropic.com>
This commit is contained in:
kami
2026-07-30 23:36:24 +04:00
parent cd46ac397f
commit cc4199c79e
+10 -10
View File
@@ -1790,16 +1790,16 @@ export class DbService {
*/
async decayBeliefs(): Promise<number> {
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,