Shipped on branch
wt/recs, 2026-09-20. The engine is pure and thoroughly
tested in src/recs/; the nightly jobs that run it are covered in
Nightly jobs.What each button means
Dwell is meant to measure active watching only — a paused or backgrounded
tab should not accrue duration.
watch_history.duration_seconds
(supabase/migrations/20260920090000_recommendations.sql) is the column
that carries it; the taste module (src/recs/taste.ts) already computes
the dwell factor from it, defaulting to a neutral 1.0 for any row where
it is still null.The taste vector
src/recs/taste.ts folds every one of a user’s signals into one Taste
vector — the same Probabilities shape the taxonomy vectors use — by
weighting each signal (heart/like/dislike undecayed, watch decayed by age
with a 30-day half-life) and summing into the channel’s category vector.
Channel similarity
src/recs/similar.ts computes, for every channel, its top 24 nearest
neighbors by cosine similarity over vectorFor() (the Jev-classified vector
when one exists, tags-only otherwise), with a small language boost and a
small popularity prior mixed in. scripts/build-channel-similar.ts runs this
nightly for the whole catalog and upserts the result into channel_similar.
Candidates → prescore → Jev blend
src/recs/candidates.ts builds a pool of channel candidates per user (seeded
by taste categories, saved/liked channels, and trending); src/recs/blend.ts
turns that pool into ranked RecRows. In full mode, each candidate’s
code-only prescore is blended with a Noul score returned by one Jev call per
user (src/recs/questions.ts shapes that request as a viewer state block —
ranked category names, channel names, languages, words only, never numbers —
plus one question per candidate). A candidate missing from the returned
noulScores map falls back to prescore-only for that one channel, even
inside an otherwise-full run.
Row lineup (Watch screen)
- Top picks for you —
user_recsrowtop-picks, signed-in users with 3 or more signals - Continue watching
- Your channels
- Because you liked/saved —
user_recsrowbecause:<channel_id>, up to 2 seeds (most-recent heart, else like) - You might like —
channel_similarneighbors, filtered against reactions and hidden channels - Trending on Bytestream — works for anonymous viewers too
- Catalog shelves
The degradation ladder
Jev is a bonus, never a dependency.1
full
A
TYPESAFE_API_KEY is set and Jev answers: prescore blended with the
per-user Noul score.2
vectors-only
No key, no credits, or a Jev call exhausted its retries: falls back to
the
channel_paths.probabilities cosine vectors already in the
database — no extra call needed.3
tags-only
No classification row exists for a channel at all: falls back to a
one-hot vector rolled up from the channel’s publisher tags to their
parent taxonomy keys.
0. user_recs.reason records which mode built each
row for later inspection, but the mode is never surfaced in the UI — a
viewer never sees which rung of the ladder their picks came from.