fix(vibe): let a dislike outlive the session that heard it

Disliking a track in a Vibe wrote one row to the session ledger and
nothing else. The ledger only excludes a track from the session it was
recorded in, so the same track came back the next evening, and the one
after that. A dislike in a Vibe is the same verdict as a dislike
anywhere else, so it now takes the same path.

Two more things undid a dislike that did land. The library scan rewrote
every track's state from the file on disk, which restored every HIDDEN
track to LIBRARY on every scan; finding a file again says nothing about
whether the listener wants to hear it. And hiding only matched tracks in
LIBRARY, so a disliked probation recommendation stayed eligible.

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:16 +04:00
parent 0749f6ad10
commit 5a73a6a6f3
4 changed files with 55 additions and 3 deletions
@@ -229,6 +229,14 @@ export class VibeSessionCoordinator {
// guard for old coordinator test doubles during the migration.
const projectSessionFeedback = (this.db as Partial<DbService>).projectVibeSessionFeedback;
if (projectSessionFeedback) await projectSessionFeedback.call(this.db, result.event);
// A dislike in a Vibe is the same verdict as a dislike anywhere else. The
// ledger alone only excludes the track from this one session, which is
// why a disliked track kept coming back the next evening. Run it once per
// distinct event so a retried delivery cannot log a second feedback row.
if (input.type === 'disliked' && input.trackId && result.inserted) {
const dislikeTrack = (this.db as Partial<DbService>).dislikeTrack;
if (dislikeTrack) await dislikeTrack.call(this.db, userId, input.trackId);
}
if (!isMaterialFeedback(input.type)) {
const response = await this.getPlan(userId, sessionId);
return { ...response, event: result.event, idempotent: !result.inserted };