feat(vibe): offer the four fifths of the library it never reached

Of 5,365 tracks, 692 had ever been played and 1,027 had ever entered a
plan. Four things kept the Vibe circling the same music.

Comfort took each top artist's two highest-scoring tracks in a fixed
order, so it proposed the same eighteen candidates in every session for
as long as the listener's top artists held still. It now samples two
from each artist's best ten.

A track the listener has never played now scores as if it carried real
relevance. Without that, library-fallback — the one generator that
samples the whole library evenly — entered at 0.05 and lost every slot.

A quarter of every plan is now reserved for unheard tracks outright,
taken from the tail so the track about to play keeps its shaping.

Hard exclusion reached back forty completed plays, so a track heard last
night was a candidate again tonight. It now covers everything heard in
the last fourteen days, counted from a durable serve or a play and never
from a plan item, since most planned tracks are replaced unheard. A cap
keeps the window from ever holding back more than 40% of the library.

Library-fallback also offers probation recommendations now. It is the
only generator that does not walk the graph, so it is how an acquired
recommendation gets heard before anything has enriched it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KENqSChfyqWnor6ud2WWH6
This commit is contained in:
kami
2026-08-10 13:59:44 +04:00
parent 60085c1d72
commit d28a92803b
4 changed files with 293 additions and 16 deletions
+30 -15
View File
@@ -82,6 +82,8 @@ export type Generator = (db: DbService, ctx: GeneratorContext) => Promise<Candid
const OBJECTIVE_USER = '00000000-0000-0000-0000-000000000000'; const OBJECTIVE_USER = '00000000-0000-0000-0000-000000000000';
const FALLBACK_CANDIDATE_LIMIT = 60; const FALLBACK_CANDIDATE_LIMIT = 60;
/** How deep into a comfort artist's catalogue the two proposed tracks are drawn from. */
const COMFORT_ARTIST_POOL = 10;
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// 1. COMFORT — Top artists by longterm affinity > 0.5 // 1. COMFORT — Top artists by longterm affinity > 0.5
@@ -97,22 +99,31 @@ async function comfortGenerator(db: DbService, ctx: GeneratorContext): Promise<C
const artistValueMap = new Map(topArtists.map(b => [b.entity_id, b.value])); const artistValueMap = new Map(topArtists.map(b => [b.entity_id, b.value]));
const artistIds = topArtists.map(b => b.entity_id); const artistIds = topArtists.map(b => b.entity_id);
// Two tracks per artist, drawn at random from that artist's best known
// COMFORT_ARTIST_POOL. Taking the top two by fused value instead proposed the
// same handful of tracks in every session for as long as the listener's top
// artists held still, which is most of why a Vibe felt like it never moved.
const res = await db.pgClient.query( const res = await db.pgClient.query(
`SELECT sub.id, sub.artist_id `SELECT sampled.id, sampled.artist_id
FROM ( FROM (
SELECT t.id, cf.object_id AS artist_id, SELECT ranked.id, ranked.artist_id,
ROW_NUMBER() OVER (PARTITION BY cf.object_id ORDER BY cf.fused_value DESC) AS rn ROW_NUMBER() OVER (PARTITION BY ranked.artist_id ORDER BY RANDOM()) AS pick
FROM tracks t FROM (
JOIN claim_fusion cf ON cf.subject_type = 'track' AND cf.subject_id = t.id SELECT t.id, cf.object_id AS artist_id,
AND cf.predicate IN ('credited_main_on', 'featured_on') ROW_NUMBER() OVER (PARTITION BY cf.object_id ORDER BY cf.fused_value DESC) AS rn
AND cf.object_type = 'artist' FROM tracks t
AND cf.object_id = ANY($1::uuid[]) JOIN claim_fusion cf ON cf.subject_type = 'track' AND cf.subject_id = t.id
AND (cf.user_id = $2 OR cf.user_id = $3) AND cf.predicate IN ('credited_main_on', 'featured_on')
WHERE (t.state = 'LIBRARY' OR (t.state = 'RECOMMENDED' AND t.probation_status = 'probation')) AND cf.object_type = 'artist'
AND NOT (t.id = ANY($4::uuid[])) AND cf.object_id = ANY($1::uuid[])
) sub AND (cf.user_id = $2 OR cf.user_id = $3)
WHERE sub.rn <= 2 WHERE (t.state = 'LIBRARY' OR (t.state = 'RECOMMENDED' AND t.probation_status = 'probation'))
ORDER BY sub.artist_id, sub.rn`, AND NOT (t.id = ANY($4::uuid[]))
) ranked
WHERE ranked.rn <= ${COMFORT_ARTIST_POOL}
) sampled
WHERE sampled.pick <= 2
ORDER BY sampled.artist_id, sampled.pick`,
[artistIds, OBJECTIVE_USER, ctx.userId, ctx.recentExclusions] [artistIds, OBJECTIVE_USER, ctx.userId, ctx.recentExclusions]
); );
@@ -567,9 +578,13 @@ async function contextualGenerator(db: DbService, ctx: GeneratorContext): Promis
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
async function libraryFallbackGenerator(db: DbService, ctx: GeneratorContext): Promise<Candidate[]> { async function libraryFallbackGenerator(db: DbService, ctx: GeneratorContext): Promise<Candidate[]> {
const res = await db.pgClient.query( const res = await db.pgClient.query(
// Probation belongs here too. This is the one generator that does not walk
// the graph, so it is the only way an acquired recommendation nothing has
// enriched yet can ever be offered — which is why Found tracks were sitting
// on disk unplayed.
`SELECT t.id `SELECT t.id
FROM tracks t FROM tracks t
WHERE t.state = 'LIBRARY' WHERE (t.state = 'LIBRARY' OR (t.state = 'RECOMMENDED' AND t.probation_status = 'probation'))
AND NOT (t.id = ANY($1::uuid[])) AND NOT (t.id = ANY($1::uuid[]))
ORDER BY RANDOM() ORDER BY RANDOM()
LIMIT $2`, LIMIT $2`,
+28
View File
@@ -66,6 +66,21 @@ describe('generators', () => {
expect(results[0].generatorId).toBe('comfort'); expect(results[0].generatorId).toBe('comfort');
}); });
it('samples two tracks at random from each artist rather than fixing on their top two', async () => {
const db = makeMockDb();
(db.pgClient.query as any).mockResolvedValue({ rows: [] });
const ctx = makeCtx({ beliefs: [
{ entity_type: 'artist', entity_id: 'artist-1', value: 0.8, profile: 'longterm', dimension: 'affinity' } as any,
] });
await generatorByName.comfort(db, ctx);
const [sql] = (db.pgClient.query as any).mock.calls[0];
expect(sql).toContain('ORDER BY RANDOM()');
expect(sql).toContain('ranked.rn <= 10');
expect(sql).toContain('sampled.pick <= 2');
});
it('returns empty when no high-affinity artists', async () => { it('returns empty when no high-affinity artists', async () => {
const db = makeMockDb(); const db = makeMockDb();
const ctx = makeCtx({ beliefs: [ { entity_type: 'artist', entity_id: 'a1', value: 0.3, profile: 'longterm', dimension: 'affinity' } as any ] }); const ctx = makeCtx({ beliefs: [ { entity_type: 'artist', entity_id: 'a1', value: 0.3, profile: 'longterm', dimension: 'affinity' } as any ] });
@@ -168,6 +183,19 @@ describe('generators', () => {
}); });
}); });
describe('library fallback', () => {
it('offers probation recommendations, which no graph generator can reach yet', async () => {
const db = makeMockDb();
(db.pgClient.query as any).mockResolvedValue({ rows: [{ id: 'probation-1' }] });
const results = await ALL_GENERATORS[8](db, makeCtx());
const [sql] = (db.pgClient.query as any).mock.calls[0];
expect(sql).toContain("t.state = 'RECOMMENDED' AND t.probation_status = 'probation'");
expect(results[0]).toMatchObject({ trackId: 'probation-1', generatorId: 'library-fallback' });
});
});
describe('experimental', () => { describe('experimental', () => {
it('returns tracks from unfamiliar genres', async () => { it('returns tracks from unfamiliar genres', async () => {
const db = makeMockDb(); const db = makeMockDb();
@@ -58,6 +58,8 @@ export interface CandidateConstraintMetadata {
instrumental?: boolean; instrumental?: boolean;
favorite?: boolean; favorite?: boolean;
newArtist?: boolean; newArtist?: boolean;
/** No play of this track has ever been recorded for this listener. */
unheard?: boolean;
energy?: number; energy?: number;
bpm?: number; bpm?: number;
valence?: number; valence?: number;
@@ -182,11 +184,35 @@ const W_FATIGUE = 0.4;
const W_DIVERSITY = 0.3; const W_DIVERSITY = 0.3;
const W_ENTROPY = 0.2; const W_ENTROPY = 0.2;
const W_REPETITION = 0.5; const W_REPETITION = 0.5;
/** What a never-played track is worth against a well-attested favourite. */
const W_UNHEARD = 0.35;
const PLAN_SIZE = 20; const PLAN_SIZE = 20;
const MAX_ARTIST_PER_PLAN = 2; const MAX_ARTIST_PER_PLAN = 2;
const MAX_ALBUM_PER_40_TRACKS = 3; const MAX_ALBUM_PER_40_TRACKS = 3;
const ALBUM_HORIZON_TRACKS = 40; const ALBUM_HORIZON_TRACKS = 40;
/**
* The share of a plan reserved for tracks the listener has never played. The
* score bonus alone leaves this to chance; a floor is what makes every Vibe
* move somewhere rather than most of them happening to.
*/
const UNHEARD_PLAN_SHARE = 0.25;
/**
* How long a track stays out of the Vibe after the listener actually hears it.
* Hard exclusion used to reach back only 40 completed plays, so a track heard
* last night was a candidate again tonight. Counted from a durable `track_served`
* event or a play, never from a plan item — most planned tracks are replaced
* before anyone hears them, and excluding those would burn the library.
*/
const HEARD_COOLDOWN_DAYS = 14;
/**
* A cooldown must never starve the pool. Whatever the window says, at most this
* share of the eligible library is held back, keeping the newest exclusions.
*/
const COOLDOWN_LIBRARY_SHARE = 0.4;
/** /**
* Producer/label relationships live on artist nodes in the fused graph, not * Producer/label relationships live on artist nodes in the fused graph, not
* on track nodes. The graph represents both directions of a relationship, * on track nodes. The graph represents both directions of a relationship,
@@ -1516,6 +1542,10 @@ export class SessionDirector {
artist.artist_id, genre.genre_id, tl.language, artist.artist_id, genre.genre_id, tl.language,
taf.instrumentalness, taf.instrumentalness,
EXISTS(SELECT 1 FROM favorites f WHERE f.user_id = $1 AND f.track_id = t.id) AS favorite, EXISTS(SELECT 1 FROM favorites f WHERE f.user_id = $1 AND f.track_id = t.id) AS favorite,
NOT EXISTS(
SELECT 1 FROM play_history heard
WHERE heard.user_id = $1 AND heard.track_id = t.id
) AS unheard,
NOT EXISTS( NOT EXISTS(
SELECT 1 FROM play_history ph SELECT 1 FROM play_history ph
JOIN track_artists_v2 old_artist ON old_artist.track_id = ph.track_id JOIN track_artists_v2 old_artist ON old_artist.track_id = ph.track_id
@@ -1552,6 +1582,7 @@ export class SessionDirector {
? (row.instrumentalness as number) >= 0.5 : undefined, ? (row.instrumentalness as number) >= 0.5 : undefined,
favorite: row.favorite === true, favorite: row.favorite === true,
newArtist: row.new_artist === true && row.artist_id != null, newArtist: row.new_artist === true && row.artist_id != null,
unheard: row.unheard === true,
energy: (row.energy as number | null) ?? undefined, energy: (row.energy as number | null) ?? undefined,
bpm: (row.bpm as number | null) ?? undefined, bpm: (row.bpm as number | null) ?? undefined,
valence: (row.valence as number | null) ?? undefined, valence: (row.valence as number | null) ?? undefined,
@@ -1627,10 +1658,18 @@ export class SessionDirector {
const explorationFit = 1 - Math.abs(novelty - (state.discoveryRadius ?? 0.38)); const explorationFit = 1 - Math.abs(novelty - (state.discoveryRadius ?? 0.38));
const sessionSimilarity = sessionSimilarityPenalty(item, c.generatorId, recentSessionFingerprints); const sessionSimilarity = sessionSimilarityPenalty(item, c.generatorId, recentSessionFingerprints);
// A track the listener has never heard scores as if it carried real
// relevance. Without this the only generator that samples the whole
// library, library-fallback, entered at relevance 0.05 and lost every
// slot to the same few well-attested favourites — which is how four
// fifths of the library stayed unplayed.
const unheardBonus = item?.unheard ? 1 : 0;
let score = W_ENJOY * c.relevance let score = W_ENJOY * c.relevance
- W_FATIGUE * avgFatigue - W_FATIGUE * avgFatigue
+ W_DIVERSITY * diversityBonus + W_DIVERSITY * diversityBonus
+ W_ENTROPY * entropyBonus + W_ENTROPY * entropyBonus
+ W_UNHEARD * unheardBonus
+ 0.08 * explorationFit + 0.08 * explorationFit
- sessionSimilarity; - sessionSimilarity;
@@ -1727,6 +1766,79 @@ export class SessionDirector {
// --------------------------------------------------------------- // ---------------------------------------------------------------
// D.9 — Plan + replan loop // D.9 — Plan + replan loop
// --------------------------------------------------------------- // ---------------------------------------------------------------
/**
* Tracks the listener actually heard inside the cooldown window, newest
* first, capped so the exclusion can never hold back more than
* COOLDOWN_LIBRARY_SHARE of what is playable. Both halves matter: a Vibe
* serves tracks the listener never finishes, and the listener plays tracks
* outside any Vibe, and both count as "I have heard this recently".
*/
private async getHeardCooldownTrackIds(userId: string): Promise<string[]> {
const res = await this.db.pgClient.query(
`WITH eligible AS (
SELECT COUNT(*)::int AS total FROM tracks
WHERE state = 'LIBRARY' OR (state = 'RECOMMENDED' AND probation_status = 'probation')
),
heard AS (
SELECT track_id, MAX(heard_at) AS heard_at FROM (
SELECT track_id, played_at AS heard_at
FROM play_history
WHERE user_id = $1 AND played_at > NOW() - ($2::int * INTERVAL '1 day')
UNION ALL
SELECT track_id, occurred_at AS heard_at
FROM vibe_events
WHERE user_id = $1 AND type = 'track_served' AND track_id IS NOT NULL
AND occurred_at > NOW() - ($2::int * INTERVAL '1 day')
) heard_rows
GROUP BY track_id
)
SELECT ranked.track_id FROM (
SELECT heard.track_id,
ROW_NUMBER() OVER (ORDER BY heard.heard_at DESC) AS rn,
eligible.total
FROM heard CROSS JOIN eligible
) ranked
WHERE ranked.rn <= FLOOR(ranked.total * $3::numeric)`,
[userId, HEARD_COOLDOWN_DAYS, COOLDOWN_LIBRARY_SHARE],
);
return (res.rows as { track_id: string }[]).map(row => row.track_id);
}
/**
* Guarantee the plan's share of never-played tracks. The ranker's bonus makes
* an unheard track competitive; this makes it certain, by substituting the
* best unheard candidates the ranker did not select for the lowest-priority
* heard entries. The first entry is never swapped — that is the track about
* to play, and the listener asked for it by seeding this Vibe.
*/
private enforceUnheardFloor(
plan: Candidate[],
pool: Candidate[],
metadata: Map<string, CandidateConstraintMetadata>,
retainedPlan: Candidate[],
): Candidate[] {
const isUnheard = (candidate: Candidate) => metadata.get(candidate.trackId)?.unheard === true;
const total = plan.length + retainedPlan.length;
if (total === 0) return plan;
const present = [...plan, ...retainedPlan].filter(isUnheard).length;
let missing = Math.ceil(total * UNHEARD_PLAN_SHARE) - present;
if (missing <= 0) return plan;
const planned = new Set([...plan, ...retainedPlan].map(candidate => candidate.trackId));
// `pool` arrives in ranked order, so this takes the best unused ones.
const spare = pool.filter(candidate => isUnheard(candidate) && !planned.has(candidate.trackId));
if (spare.length === 0) return plan;
const filled = [...plan];
for (let index = filled.length - 1; index > 0 && missing > 0 && spare.length > 0; index--) {
if (isUnheard(filled[index])) continue;
filled[index] = spare.shift()!;
missing--;
}
return filled;
}
async buildPlan( async buildPlan(
userId: string, userId: string,
sessionId: string, sessionId: string,
@@ -1817,6 +1929,7 @@ export class SessionDirector {
const recentExclusionSet = new Set<string>([ const recentExclusionSet = new Set<string>([
...recentPlays.map(p => p.trackId), ...recentPlays.map(p => p.trackId),
...durableSessionTrackIds, ...durableSessionTrackIds,
...(await this.getHeardCooldownTrackIds(userId)),
...(options.excludedTrackIds ?? []), ...(options.excludedTrackIds ?? []),
]); ]);
if (seedTrackId) recentExclusionSet.add(seedTrackId); if (seedTrackId) recentExclusionSet.add(seedTrackId);
@@ -1915,7 +2028,8 @@ export class SessionDirector {
// objective snapshots. Do not silently hide a degraded sequence. // objective snapshots. Do not silently hide a degraded sequence.
console.warn('Vibe constraint relaxations', { sessionId, relaxations: constrained.relaxations }); console.warn('Vibe constraint relaxations', { sessionId, relaxations: constrained.relaxations });
} }
return constrained.plan.slice(0, Math.max(0, planSize - retainedPlan.length)); const plan = constrained.plan.slice(0, Math.max(0, planSize - retainedPlan.length));
return this.enforceUnheardFloor(plan, deduped, metadata, retainedPlan);
} }
/** Apply the durable delivery budget to fresh arc slots. Retained entries /** Apply the durable delivery budget to fresh arc slots. Retained entries
@@ -423,6 +423,91 @@ describe('SessionDirector', () => {
expect(ranked[0].relevance).toBeGreaterThanOrEqual(ranked[ranked.length - 1].relevance); expect(ranked[0].relevance).toBeGreaterThanOrEqual(ranked[ranked.length - 1].relevance);
}); });
it('lifts a never-played track above a familiar one of similar relevance', async () => {
const db = makeMockDb();
(db.pgClient.query as any).mockResolvedValue({
rows: [
{ track_id: 'heard', artist_id: 'a1', unheard: false, favorite: false },
{ track_id: 'unheard', artist_id: 'a2', unheard: true, favorite: false },
],
});
const director = new SessionDirector(db);
const candidates = [
{ trackId: 'heard', generatorId: 'comfort', relevance: 0.6, explanation: [] },
{ trackId: 'unheard', generatorId: 'library-fallback', relevance: 0.4, explanation: [] },
] as any;
const fatigue = { artist: new Map(), album: new Map(), genre: new Map(), track: new Map(), language: new Map(), vocal: 0 };
const state = { energy: 0.5, noveltyHunger: 0.3, sessionAgeMin: 10, lastArtistIds: [], lastGenreIds: [], context: null };
const repetitionState = { recentTrackIds: new Set<string>(), recentArtistIds: new Set<string>() };
const ranked = await director.rankCandidates(candidates, fatigue, [], state, repetitionState);
expect(ranked[0].trackId).toBe('unheard');
});
});
describe('unheard floor', () => {
const director = new SessionDirector(makeMockDb());
const meta = (unheard: boolean) => ({ unheard });
it('substitutes unheard candidates for the lowest-priority heard entries', () => {
const plan = ['p0', 'p1', 'p2', 'p3'].map(candidate);
const pool = [...plan, ...['u1', 'u2'].map(candidate)];
const metadata = new Map([
...plan.map(item => [item.trackId, meta(false)] as const),
['u1', meta(true)] as const,
['u2', meta(true)] as const,
]);
const filled = (director as any).enforceUnheardFloor(plan, pool, metadata, []);
// One in four of a four-item plan, taken from the tail.
expect(filled.map((item: any) => item.trackId)).toEqual(['p0', 'p1', 'p2', 'u1']);
});
it('never displaces the track about to play', () => {
const plan = [candidate('p0')];
const pool = [candidate('p0'), candidate('u1')];
const metadata = new Map([['p0', meta(false)] as const, ['u1', meta(true)] as const]);
const filled = (director as any).enforceUnheardFloor(plan, pool, metadata, []);
expect(filled.map((item: any) => item.trackId)).toEqual(['p0']);
});
it('leaves a plan that already meets the floor alone', () => {
const plan = [candidate('u1'), candidate('p1'), candidate('p2'), candidate('p3')];
const pool = [...plan, candidate('u2')];
const metadata = new Map([
['u1', meta(true)] as const,
['p1', meta(false)] as const,
['p2', meta(false)] as const,
['p3', meta(false)] as const,
['u2', meta(true)] as const,
]);
const filled = (director as any).enforceUnheardFloor(plan, pool, metadata, []);
expect(filled.map((item: any) => item.trackId)).toEqual(['u1', 'p1', 'p2', 'p3']);
});
it('counts the retained tail towards the floor', () => {
const plan = [candidate('p0'), candidate('p1')];
const retained = [candidate('u0'), candidate('r1')];
const pool = [...plan, candidate('u1')];
const metadata = new Map([
['p0', meta(false)] as const,
['p1', meta(false)] as const,
['u0', meta(true)] as const,
['r1', meta(false)] as const,
['u1', meta(true)] as const,
]);
const filled = (director as any).enforceUnheardFloor(plan, pool, metadata, retained);
expect(filled.map((item: any) => item.trackId)).toEqual(['p0', 'p1']);
});
}); });
describe('recent session fingerprint penalty', () => { describe('recent session fingerprint penalty', () => {
@@ -776,6 +861,41 @@ describe('SessionDirector', () => {
}); });
}); });
describe('heard cooldown', () => {
it('hands generators every track heard inside the window as a hard exclusion', async () => {
const db = makeMockDb();
(db.pgClient.query as any).mockImplementation((sql: string) => {
if (sql.includes('heard_rows')) return Promise.resolve({ rows: [{ track_id: 'heard-last-week' }] });
return Promise.resolve({ rows: [] });
});
const director = new SessionDirector(db);
vi.spyOn(director, 'buildState').mockResolvedValue({
energy: 0.5, noveltyHunger: 0.3, sessionAgeMin: 0, lastArtistIds: [], lastGenreIds: [], context: null,
});
vi.spyOn(director, 'computeFatigue').mockResolvedValue({
artist: new Map(), album: new Map(), genre: new Map(), language: new Map(), track: new Map(), vocal: 0.5,
});
vi.spyOn(director, 'getBudgets').mockResolvedValue([]);
vi.spyOn(director, 'buildRepetitionState').mockResolvedValue({ recentTrackIds: new Set(), recentArtistIds: new Set() });
vi.spyOn(director as any, 'loadArtistMap').mockResolvedValue(new Map());
(db as any).getVibeSessionTrackIds = vi.fn().mockResolvedValue([]);
const originalGenerators = [...ALL_GENERATORS];
let seenExclusions: string[] = [];
ALL_GENERATORS.splice(0, ALL_GENERATORS.length, async (_db, ctx) => {
seenExclusions = [...ctx.recentExclusions];
return [{ ...candidate('heard-last-week'), generatorId: 'comfort' }];
});
try {
const plan = await director.buildPlan('user-1', 'session-1');
expect(seenExclusions).toContain('heard-last-week');
expect(plan.map(item => item.trackId)).not.toContain('heard-last-week');
} finally {
ALL_GENERATORS.splice(0, ALL_GENERATORS.length, ...originalGenerators);
}
});
});
describe('buildState', () => { describe('buildState', () => {
it('returns state with default values when no prior session', async () => { it('returns state with default values when no prior session', async () => {
const db = makeMockDb(); const db = makeMockDb();