# Vibe v2 — Session Director Specification

## Status and purpose

This is the target specification for Vibe v2. It turns Vibe from a
recommendation queue into a session director: an autonomous system that
continuously composes the listener's next hour of music.

The unit of optimisation is not the next track. It is a listening experience
that remains coherent, fresh, and rewarding over hours and continues to feel
new tomorrow.

This document supplements docs/architecture/09-recommendation-and-identity-v2.md.
Where the two documents conflict, this document controls the Session Director
and Vibe playback contract.

## Product contract

Vibe must feel like a thoughtful radio DJ:

- It plays familiar music, adjacent music, and worthwhile discoveries in a
  deliberate balance.
- It responds to a skip, full listen, or explicit action quickly enough that
  the listener can perceive the change.
- It does not collapse into a few favourite artists, genres, languages,
  decades, labels, producers, or audio-feature bands.
- It has a direction: energy, novelty, and intensity can evolve, but should
  not jump without a reason.
- It returns to ideas introduced earlier (callbacks) and makes room for
  surprises.
- It remembers prior sessions sufficiently to avoid replaying yesterday's
  shape, not merely yesterday's tracks.

    durable history + live feedback + context
                     |
                     v
               listener state
                     |
                     v
       candidate retrieval and expansion
                     |
                     v
       sequence planner / constraint engine
                     |
                     v
         revisable next 20–50 tracks
                     |
                     v
                  playback
                     |
                     +------------ feedback and replan ------------+

Vibe is not a saved playlist. A client may display a small, revisable preview
of the future, but the server owns the authoritative plan and may rewrite every
unplayed item at any time.

## Goals and non-goals

### Goals

1. Maximise expected session reward, not click-through rate or immediate
   predicted enjoyment alone.
2. Make recommendations conditional on current state, listening context, and
   the already-played portion of the session.
3. Balance comfort, discovery, diversity, coherence, freshness, and long-term
   learning while penalising fatigue, repetition, and predictability.
4. Support a self-hosted local library first. Probation tracks are candidates
   only when their audio is available locally and acquisition policy permits it.
5. Remain useful with sparse metadata. Missing attributes reduce confidence;
   they must never silently become a hard negative.
6. Be explainable: every selected item has provenance, a reason for its slot,
   and the constraints that affected it.

### Non-goals

- Do not acquire copyrighted audio or bypass existing acquisition gates.
- Do not infer sensitive personal attributes. Context is opt-in and coarse
  (for example home, work, walking), never precise location.
- Do not treat a permanently fixed exploration ratio, genre quota, or score
  weight as the final design. Defaults are bootstraps, not truth.
- Do not expose internal goals in a way that makes the experience feel
  manipulative. Explanations remain human-scale.

---

## 1. System model

### 1.1 Inputs

| Input | Examples | Role |
|---|---|---|
| Permanent taste | favourite artists, genre affinity, negative feedback | Establishes the comfort zone |
| Multi-horizon memory | 30 minutes, 7 days, 3 months, lifetime | Separates current obsession from durable taste |
| Current session | played/queued tracks, skips, callbacks, budget spend | Determines what fits now |
| Context | hour, weekday, device, activity, coarse location, optional weather | Changes interpretation of taste |
| Music knowledge | graph claims, metadata, audio features, embeddings, quality | Retrieves and describes candidates |

### 1.2 Optimisation objective

For sequence q = [t1, …, tn], optimise discounted sequence reward rather than
independently sorting tracks:

    J(q | state) = sum over i of gamma^i × (
        enjoyment(ti)
      + discovery_value(ti)
      + transition_quality(ti-1, ti)
      + freshness(ti)
      + diversity_gain(q through i)
      + goal_progress(ti)
      - fatigue(ti)
      - repetition(ti, q through i)
      - predictability(q through i)
      - disruption(ti-1, ti)
    )

Hard safety and availability constraints apply before optimisation. Gamma
discounts distant slots so the director is decisive about the next few tracks
without pretending it knows the exact state 40 tracks later.

Initial weights may be hand-tuned, but must be versioned policy configuration.
They become learnable only after sufficient reliable events exist.

### 1.3 Planning horizon

- Keep an internal horizon of 20–50 tracks, chosen from track duration and
  session conditions.
- Publish only 3–8 tracks to the client as a mutable preview.
- Treat only the immediate next track as committed.
- Replan after every material event and before the preview falls below three
  playable tracks.
- Do not send duplicate tracks in one Vibe session unless repeat-one was
  explicitly requested.

---

## 2. Durable session model

### 2.1 Session identity

Every Vibe request, playback event, plan version, and feedback event MUST be
keyed by session_id and user_id. play_history.batch_id is not a session ID and
must not be used as one.

Sessions end explicitly, after configurable inactivity, or when a new Vibe
session replaces the current one. A session can resume in a short grace period
without losing state or goals.

### 2.2 Session context

Context is optional, versioned, and privacy-preserving.

    interface VibeContext {
      timeZone?: string;
      localHour?: number;  // normally server-derived
      weekday?: number;    // normally server-derived
      dayKind?: 'weekday' | 'weekend' | 'holiday';
      device?: 'desktop' | 'phone' | 'speaker' | 'car' | 'headphones';
      activity?: 'focus' | 'relax' | 'walking' | 'workout' | 'social' | 'unknown';
      locationCategory?: 'home' | 'work' | 'gym' | 'travel' | 'unknown';
      weather?: 'clear' | 'rain' | 'snow' | 'hot' | 'cold' | 'unknown';
      source?: 'current_track' | 'artist' | 'genre' | 'surprise' | 'resume';
    }

The frontend must provide an unobtrusive activity/context selector, beginning
with activity and device. Browser context is a hint and is never required.

### 2.3 Event ledger

Store immutable events before updating derived state. This makes feedback
auditable, supports offline evaluation, and prevents listener state being the
only record of why a plan changed.

    CREATE TABLE vibe_sessions (
      id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
      user_id UUID NOT NULL,
      status TEXT NOT NULL CHECK (status IN
        ('active','paused','ended','expired','replaced')),
      seed_track_id UUID REFERENCES tracks(id) ON DELETE SET NULL,
      context JSONB NOT NULL DEFAULT '{}'::jsonb,
      policy_version TEXT NOT NULL,
      started_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
      last_event_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
      ended_at TIMESTAMPTZ
    );

    CREATE TABLE vibe_events (
      id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
      client_event_id UUID,
      session_id UUID NOT NULL REFERENCES vibe_sessions(id) ON DELETE CASCADE,
      user_id UUID NOT NULL,
      track_id UUID REFERENCES tracks(id) ON DELETE SET NULL,
      type TEXT NOT NULL,
      occurred_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
      position_ms INTEGER,
      duration_ms INTEGER,
      payload JSONB NOT NULL DEFAULT '{}'::jsonb,
      UNIQUE (session_id, client_event_id)
    );

    CREATE TABLE vibe_plan_versions (
      id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
      session_id UUID NOT NULL REFERENCES vibe_sessions(id) ON DELETE CASCADE,
      version INTEGER NOT NULL,
      reason TEXT NOT NULL,
      state_snapshot JSONB NOT NULL,
      objective_snapshot JSONB NOT NULL,
      created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
      UNIQUE (session_id, version)
    );

    CREATE TABLE vibe_plan_items (
      plan_version_id UUID NOT NULL REFERENCES vibe_plan_versions(id) ON DELETE CASCADE,
      ordinal INTEGER NOT NULL,
      track_id UUID NOT NULL REFERENCES tracks(id) ON DELETE CASCADE,
      slot_role TEXT,
      candidate_source TEXT NOT NULL,
      score REAL NOT NULL,
      score_breakdown JSONB NOT NULL,
      explanation JSONB NOT NULL,
      committed BOOLEAN NOT NULL DEFAULT false,
      PRIMARY KEY (plan_version_id, ordinal),
      UNIQUE (plan_version_id, track_id)
    );

Create indexes on vibe_sessions(user_id, last_event_at DESC),
vibe_events(session_id, occurred_at), and vibe_events(user_id, occurred_at DESC).

Initial event types:

    session_started, session_resumed, session_ended, context_changed,
    plan_published, track_served, playback_started, progress,
    completed, skipped, disliked, kept, favourite_added, queue_removed,
    manual_search, album_opened, artist_opened, playlist_added,
    track_replayed, volume_changed, playback_error

Event writes must be idempotent. The client supplies client_event_id for any
action that might be retried after a network failure.

### 2.4 Derived listener state

session_state is a cache derived from events and recent history, not the sole
source of truth. It must be rebuildable.

    interface ListenerSessionState {
      sessionId: string;
      updatedAt: string;
      ageMin: number;

      energy: number;
      valence: number | null;
      focus: number | null;
      attention: number | null;
      cognitiveLoad: number | null;
      emotionalIntensity: number | null;
      noveltyHunger: number;
      noveltyTolerance: number;
      danceabilityTarget: number | null;
      acousticnessTarget: number | null;
      instrumentalnessTarget: number | null;
      vocalPreference: 'vocal' | 'instrumental' | 'mixed';
      tempoTarget: number | null;
      moodTags: Array<{ tag: string; weight: number }>;

      explorationCoefficient: number;
      discoveryRadius: number;
      entropyTarget: number;
      currentArc: ArcInstance;
      activeGoals: SessionGoal[];
      callbackLedger: CallbackToken[];
      fatigue: FatigueSnapshot;
      budgetSpend: BudgetSpendSnapshot;
      recent: RecentSessionSummary;
    }

Every inferred value carries confidence and source. Low confidence causes
broader, safer choices; it never means the value is zero.

### 2.5 State update rules

- Completion shifts continuous state modestly toward reliable track features,
  weighted by listen ratio and feature confidence.
- A quick skip is negative evidence for the track and recommendation route; it
  is not automatically a dislike of every artist/genre/feature on the track.
- A saved discovery or intentional replay is strong evidence for exploration at
  that distance and source.
- Repeated unfamiliar skips shrink exploration temporarily; repeated unfamiliar
  completions expand it gradually.
- Context changes may reset desired trajectory while retaining fatigue and
  long-term memory.
- Updates MUST be ordered per session. Concurrent feedback must be serialized
  or use a session-version compare-and-swap.

---

## 3. Memory horizons and listener model

The listener has independent memories, each with its own decay and purpose.

| Horizon | Window | Captures | Primary effect |
|---|---:|---|---|
| Immediate | 30 minutes | current direction, skips, active fatigue | transitions and next slots |
| Session | session lifetime | arc, budgets, callbacks, served tracks | sequence planning |
| Daily | 24 hours | today’s exposure and shape | fatigue and fresh starts |
| Weekly | 7 days | recent obsessions and routines | variety across days |
| Medium | 3 months | stable recent taste | affinity and discovery neighbourhood |
| Lifetime | slow decay | durable favourites and familiar anchors | comfort candidates |

Existing long-term, obsession, discovery, negative, forgotten, and contextual
belief profiles remain useful. Vibe adds a session layer; it does not flatten
all profiles into one taste score.

### 3.1 Session similarity memory

After each session, generate a compact fingerprint:

    interface SessionFingerprint {
      artistDistribution: Record<string, number>;
      genreDistribution: Record<string, number>;
      languageDistribution: Record<string, number>;
      decadeDistribution: Record<string, number>;
      audioTrajectory: Array<{ energy: number; tempo?: number; valence?: number }>;
      discoveryRate: number;
      generatorDistribution: Record<string, number>;
      acceptedDiscoveries: string[];
      durationMin: number;
    }

At the next session start, softly penalise similarity to recent fingerprints,
especially one or two days ago. An explicit artist/album/genre seed may
intentionally override this penalty.

---

## 4. Music representation and candidate retrieval

### 4.1 Feature completeness

Each playable track should expose, when available:

- main/featured artists, album, label, producers, composers, scenes;
- genres and tags with confidence;
- release year/decade, language, vocal/instrumental classification;
- BPM, key, energy, valence, danceability, acousticness, instrumentalness,
  liveness, loudness, duration;
- live version, cover, remix, soundtrack, and side-project relationships;
- quality, availability, and feature-completeness indicators.

Missing audio analysis is unknown, never low energy or instrumental.

### 4.2 Embeddings and discovery radius

The long-term target is a shared semantic space for tracks, artists, albums,
genres, sessions, and listener/context representation. It may begin with
feature-derived vectors and later use learned embeddings. Recommended
dimensions are 768 or 1024. Musical, lyrical, collaborative, and contextual
vectors can be blended at retrieval time.

| Distance | Interpretation |
|---:|---|
| 0.00 | already familiar/favourite |
| 0.15 | same artist or tightly associated artist |
| 0.30 | strongly similar artist/sound |
| 0.45 | different artist in a known style |
| 0.65 | adjacent genre or scene |
| 0.90 | surprising but explainable |

discoveryRadius is a range, not a switch. Sequences normally move through
adjacent distances and return to an anchor. Large jumps need a surprise slot or
listener reinforcement.

### 4.3 Candidate pools

Generate independently, retain all provenance, deduplicate by track and
canonical identity, then rank.

| Pool | Default share | Purpose |
|---|---:|---|
| Familiar/favourites | 30% | comfort and callbacks |
| Similar tracks/artists | 20% | continuity |
| User-niche recent/trending | 15% | timely relevant discovery |
| Long-tail discovery | 10% | avoid popularity collapse |
| Contextual candidates | 10% | fit activity/time |
| Artist/graph traversal | 10% | explainable adjacency |
| Controlled serendipity | 5% | bounded surprise |

These are retrieval targets, not mandatory final-plan shares. Allocation adapts
to exploration, candidate availability, fatigue, and the active arc.

Required generator classes:

1. Comfort: favourites and trusted artists with fatigue suppression.
2. Adjacent: graph/ANN neighbours of recent successes/current trajectory.
3. Discovery: unfamiliar artists at a safe distance with credible graph path.
4. Revival: forgotten favourites, old obsessions, accepted tracks after rest.
5. Deep dive: album/obsession exploration limited by album/artist budgets.
6. Contextual: candidates with comparable activity, hour, or device evidence.
7. Freshness: new releases/niche trends fitting the listener graph.
8. Serendipity: deliberately bounded unexplored route, never random catalogue.

Candidate provenance retains every nominating path, not just the winning source.
It is required for explanations and source-level learning.

---

## 5. Fatigue, repetition, and diversity

### 5.1 Fatigue principle

Everything can fatigue; everything recovers. Fatigue is decayed exposure, not
a permanent ban.

    fatigue(dimension, entity, now) =
      min(1, sum of exposure_weight(event) × exp(-event_age / tau_dimension))

Exposure weight is higher for completion, lower for a short sample, and zero
for playback failure. Tau is configurable per dimension and may be adapted from
observed tolerance.

Track fatigue must blend multiple windows: today, yesterday, week, and month.
It must not be a single recent-history query.

### 5.2 Required fatigue dimensions

| Dimension | Example | Planner action |
|---|---|---|
| Track | heard today/replayed yesterday | strong suppression unless requested |
| Artist/canonical identity | 12 tracks by one artist | widen artist pool |
| Album | half an album played | spread across hours/days |
| Genre/scene | 18 metal tracks | move to adjacent style |
| Language | 35 Japanese tracks | mix another language/instrumental |
| Vocal/instrumentation | all vocal, same vocal type | alternate texture |
| Tempo/energy/valence | narrow BPM/mood band | controlled transition |
| Producer/label | repeated creative lineage | force new route |
| Decade | all recent releases | reintroduce another era |
| Recommendation route | same graph edge repeatedly | diversify path |

Unknown metadata may not claim to satisfy a specific quota.

### 5.3 Repetition rules

Hard rules, unless explicitly overridden:

- A served track never reappears in one Vibe session.
- A track in its configured recent window is ineligible.
- No more than two tracks by a canonical artist in 20 tracks.
- No more than three tracks from an album in 40 tracks.
- Skipped/disliked tracks are ineligible for the rest of the session.

Soft, adaptive minimum distances apply to artist, album, genre, language,
producer, and energy band. If the pool is too small, relax the least important
soft constraint, record it, and never silently relax hard track exclusion.

### 5.4 Diversity budgets

Budgets are planner resources, not passive analytics. Initial defaults:

| Dimension | Target | Horizon |
|---|---:|---:|
| Any artist | at most 20% | 30 min |
| Any genre | at most 40% | 30 min |
| Any language | at most 60% | 30 min |
| Instrumental | at least 10% if inventory permits | 30 min |
| New artists | about 15% | 60 min |
| Familiar favourites | about 25% | 60 min |

Budgets may be upper bounds, lower bounds, or target ranges. The planner
projects spend across the proposed sequence, not only completed history.

Explicit user intent may temporarily override soft targets. Starting an album
or repeatedly selecting an artist narrows the session intentionally; diversity
prevents accidental loops, not explicit choice.

### 5.5 Anti-loop detector

Run after every state update and every plan proposal. Detect concentration across
artist/canonical identity, album, genre, scene, label, producer, language,
decade, BPM, energy, valence, vocal type, candidate source, and graph path.

Use HHI and Shannon entropy accurately; HHI must not be called entropy.
Correct the detected dimension directly:

| Loop | Required correction |
|---|---|
| Artist/album | retrieve other artists; reserve only later callback |
| Genre/scene | retrieve adjacent genres at compatible energy |
| Language/vocal | reserve next eligible alternate slot |
| Tempo/energy | alter next arc target gradually |
| Producer/label/route | exclude repeated relationship from retrieval |
| Low route diversity | require another generator/path |

Do not merely boost an already available experimental candidate if it does not
fix the detected loop.

---

## 6. Arcs, callbacks, surprise, and long-term rhythm

### 6.1 Arc templates

An arc is desired trajectory plus slot roles and transition tolerance, not just
a generator list.

| Arc | Example trajectory |
|---|---|
| Comfort | known → known → adjacent → favourite |
| Discovery | favourite → similar → new → familiar callback |
| Energetic | medium → high → peak → cooldown |
| Late-night | soft → ambient → acoustic → slow electronic |
| Focus | instrumental/low-vocal → steady complexity → gentle reset |
| Album exploration | familiar anchor → album chapter → relief → callback |

Slots define target ranges/deltas for energy, tempo, valence, acousticness,
instrumentality, novelty distance, and familiarity. Missing features reduce
confidence rather than reject a candidate.

Arc selection depends on state, context, age, explicit intent, and weekly
schedule. It changes only when feedback/context makes the current arc
implausible.

### 6.2 Transition model

For every pair, compute transition quality from:

- tempo delta and beat compatibility where reliable;
- harmonic/key compatibility where available;
- energy, valence, acousticness, danceability, instrumentality deltas;
- genre/scene continuity or an explainable bridge;
- artist/album separation;
- language and vocal contrast when fatigue calls for it;
- novelty-distance progression; and
- current arc fit.

A good transition is not always similarity. A cooldown after a peak or a
surprise after an anchor is good if it fits the arc.

### 6.3 Callbacks

A callback token is created when an artist, genre, energy peak, theme, or
favourite is worth revisiting. It has minimum/maximum separation and cannot
violate fatigue/repetition rules.

    favourite → new artist → adjacent artist → callback to favourite
    heavy → soft bridge → different heavy track
    forgotten favourite → side project → return to old era

Callbacks are optional and must never become repetition.

### 6.4 Surprise budget

Every sustained session needs bounded surprise. Attempt at least one eligible,
explainable surprise per hour when inventory permits. Types include forgotten
favourite, live/acoustic/cover/remix, producer/side project, old obsession,
soundtrack connection, and novel graph route.

A surprise is paired with a recovery anchor. A quick skip lowers propensity for
that surprise type, not the listener’s whole taste profile.

### 6.5 Invisible goals

Goals are durable, bounded, and never override hard constraints or explicit
direction. Examples:

- complete an album over several days without a block;
- introduce a promising artist gradually;
- revisit a favourite monthly after recovery;
- balance languages/decades over a week;
- rotate producer/scene routes;
- evaluate a probation discovery;
- preserve a surprise opportunity per hour.

    CREATE TABLE vibe_goals (
      id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
      user_id UUID NOT NULL,
      type TEXT NOT NULL,
      entity_type TEXT,
      entity_id UUID,
      state JSONB NOT NULL DEFAULT '{}'::jsonb,
      priority REAL NOT NULL DEFAULT 0.5,
      status TEXT NOT NULL CHECK (status IN ('active','paused','complete','expired')),
      created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
      due_at TIMESTAMPTZ,
      completed_at TIMESTAMPTZ
    );

### 6.6 Weekly rhythm

Vibe may use an opt-in weak weekly prior: Monday discoveries, Tuesday album
exploration, Wednesday comfort, Thursday forgotten favourites, Friday high
energy, weekend broad/context-led mix. It must never override explicit intent
or current context.

---

## 7. Candidate ranking and sequence planning

### 7.1 Candidate proposal score

    candidate_score =
        w_affinity × personal_affinity
      + w_session × session_state_fit
      + w_transition × transition_fit_from_previous
      + w_discovery × discovery_value
      + w_freshness × freshness
      + w_quality × track_quality
      + w_goal × goal_progress
      - w_fatigue × fatigue_penalty
      - w_repeat × repetition_penalty
      - w_saturation × dimension_saturation
      - w_risk × unsupported_metadata_or_route_risk

This is a proposal score, not final selection. Persist normalised,
policy-versioned score breakdown with every selected plan item.

### 7.2 Constraint engine

Apply constraints during sequence construction:

1. Exclude unavailable, hidden, retired, served, and blocked tracks.
2. Enforce hard repetition/safety constraints.
3. Satisfy arc slot requirements.
4. Project budgets across the horizon.
5. Penalise session similarity and path concentration.
6. Reserve callback, discovery, and surprise opportunities.
7. Select the highest-value feasible sequence.

If no feasible sequence exists, return a degraded plan with structured
constraint_relaxations. Relax soft diversity targets, then arc precision, then
freshness. Never reintroduce an explicitly disliked or served track simply to
fill the plan.

### 7.3 Planner algorithm

Initial implementation is constrained beam search:

- retrieve about 500–2,000 deduplicated candidates;
- keep beam width 20–50 with incremental objective and projected state;
- expand only compatible candidates per slot;
- plan 20–50 slots; commit first; publish preview only;
- replan from current state after feedback while preserving committed track and
  still-valid callback/goal tokens.

Later options include MCTS, transformer decoding, or model-predictive control.
The planner interface must stay algorithm-agnostic.

### 7.4 Controlled unpredictability

Measure predictability over recent and proposed sequence using artist, genre,
source/path, novelty distance, and feature distributions.

- Too predictable: allocate adjacent/surprise candidate and protect a later
  comfort callback.
- Too chaotic: shrink discovery radius and inject a familiar anchor.

The target is intentional surprise, neither randomness nor maximum familiarity.

---

## 8. Feedback and learning

### 8.1 Feedback interpretation

| Signal | Interpretation | Immediate action |
|---|---|---|
| Skip under 5 sec | strong mismatch | remove route/track, reduce local risk, replan |
| Skip under 20 sec | mismatch | penalise candidate/path, replan |
| Late skip | weak negative/transition issue | modest penalty |
| Completion | weak positive | update state/affinity modestly |
| Replay | strong positive | reinforce affinity and comfort |
| Keep/favourite | strong positive | reinforce entities and route |
| Playlist add/share | very strong positive | reinforce strongly |
| Mute/hide artist/genre | strong negative | exclude or suppress |
| Manual search/open album | intent evidence | bias session appropriately |
| Volume change | weak/noisy evidence | aggregate with other signals |
| Session abandonment | delayed negative reward | evaluate preceding sequence |

Completion uses actual position and duration. Long intros must not be treated
exactly like short tracks.

### 8.2 Dynamic exploration

Maintain exploration coefficient E in range 0–1 and discovery radius.

    quick skip of unfamiliar track       E -= 0.05
    completion of unfamiliar track       E += 0.03
    save/favourite unknown artist        E += 0.12
    repeated known completion            E -= 0.01 only if comfort over budget
    explicit more-discovery control      bounded immediate increase

Clamp and smooth updates so one event does not whiplash a session. Persist the
evidence. play_of_never_seen or equivalent MUST be emitted by production
playback; a read-only schema capability is not a feature.

### 8.3 Learning rollout

1. Ship deterministic rules with complete event and plan logging.
2. Replay historical sessions for offline evaluation.
3. Use contextual bandits for calibrated immediate/short-horizon weights and
   generator routing under safety limits.
4. Consider offline RL/model-based sequence policy only after reliable
   off-policy evaluation exists.

Retain deterministic fallback and a kill switch for learned policy.

---

## 9. API and frontend contract

### 9.1 Start/resume

    POST /api/v2/vibe/sessions

    {
      "seedTrackId": "uuid or optional",
      "context": { "activity": "focus", "device": "headphones" },
      "intent": "optional mode",
      "resumeSessionId": "uuid or optional"
    }

Response includes sessionId, planVersion, now, revisable preview, and concise
state summary. User identity comes from authenticated/trusted request context,
not a silent shared default UUID.

### 9.2 Playback event

    POST /api/v2/vibe/sessions/:sessionId/events

    {
      "eventId": "client UUID",
      "type": "progress | completed | skipped | kept | disliked | ...",
      "trackId": "uuid",
      "positionMs": 12500,
      "durationMs": 203000,
      "payload": {}
    }

Response includes canonical planVersion, replacement preview, state summary,
and replan reason.

The client MUST reconcile its unplayed Vibe buffer whenever replacement preview
is returned. It must not keep playing stale prefetched items only because they
were fetched before feedback. The currently loaded track is not interrupted
unless the listener explicitly skips it.

### 9.3 Serve next

    POST /api/v2/vibe/sessions/:sessionId/next
    { "expectedPlanVersion": 4 }

The response is idempotent for request/plan version and marks the item served.
If a newer plan exists, return that preview instead of a stale track.

### 9.4 UI requirements

- Present Vibe as an evolving session, not static playlist.
- Show concise direction such as gentle discovery or late-night cooldown.
- Preview only revisable next tracks and label them adaptive.
- Offer Keep, Dislike, Skip, End, plus implicit progress tracking.
- Offer lightweight context and more-familiar/more-discovery controls.
- Explain a track on demand with provenance and human-readable reason.
- Do not let ordinary library actions accidentally write Vibe feedback.
- Preserve session identity through navigation and recover after transient
  network failure.

---

## 10. Operational requirements

### 10.1 Consistency and concurrency

- Serialize plan mutation per session.
- Use monotonic planVersion and client optimistic concurrency.
- Redis may cache active plans; Postgres event/plan records are authoritative.
  Redis expiry must never erase only session history.
- Stale-session reaper ends inactive sessions and creates final fingerprint; it
  never deletes listener history.
- Revalidate track state and availability immediately before serving.

### 10.2 Performance

- Candidate retrieval p95 under 250 ms for warm local catalogue.
- Replan p95 under 750 ms for a 20-track baseline horizon.
- Serve-next p95 under 150 ms when valid plan exists.
- If ANN, external metadata, or context is unavailable, fall back to local
  graph/metadata/favourites and record degraded source state.

### 10.3 Observability

Log/measure per policy version and session:

- candidate counts/rejection reasons per generator;
- metadata coverage;
- constraint violations and relaxations;
- replan latency/reason;
- client stale-preview replacement success;
- fatigue/budget/entropy trajectories;
- discovery source/distance acceptance;
- session outcome metrics.

Never log raw precise location or unnecessary personal context.

---

## 11. Success metrics

Primary metrics:

- average uninterrupted listening duration;
- completed-session duration and return probability;
- discovery acceptance by distance/source;
- new artists saved/favourited;
- playlist additions and intentional replays;
- perceived freshness;
- artist/genre/language/path diversity and repetition rate;
- sessions with a successful surprise and healthy comfort anchor.

Secondary diagnostics:

- quick/medium skips, hides, abandonments;
- plan replacement latency;
- no-eligible-candidate rate;
- metadata coverage/fallback frequency;
- hard-constraint satisfaction;
- similarity to recent sessions.

CTR is diagnostic only. It must not become the objective that causes
favourite-artist loops.

---

## 12. Delivery phases and acceptance criteria

### Phase 0 — Correct session contract

Deliver durable sessions/events/versioned plans, session IDs on all playback
events, and client replacement of unplayed preview after replan.

Acceptance:

- A quick skip changes unplayed preview within one successful event round trip.
- Completion/skip cannot be attributed to another session.
- Backend restart does not lose event history or plan audit.

### Phase 1 — Enforced fatigue and diversity

Wire every budget/fatigue dimension used by policy into construction. Add album,
language, vocal/instrumental, and route controls.

Acceptance:

- Fixtures prove artist, album, genre, language, and track constraints.
- Changing a budget changes the plan, not only debug output.
- Anti-loop response corrects the detected dimension.

### Phase 2 — Musical arcs and sequence planner

Add feature access, transition scoring, callbacks, surprise tokens, constrained
beam search.

Acceptance:

- Energetic arc rises then cools within tolerance when features exist.
- Discovery arc anchors a discovery between familiar items.
- Chosen plans beat greedy order on offline fixture objective.

### Phase 3 — Context, long-term goals, session memory

Add context capture, fingerprints, weekly priors, and scheduling.

Acceptance:

- Context affects ranking only with comparable evidence; otherwise fallback is safe.
- Consecutive unseeded sessions are less similar than baseline without reducing
  completion rate.
- Album/artist goals spread exposure across sessions.

### Phase 4 — Dynamic exploration and learning

Emit complete feedback, calibrate radius, add offline evaluation, then guarded
online learning.

Acceptance:

- Unknown-track completion/skip changes exploration in expected bounded direction.
- Learned decisions have policy version, feature log, and fallback.
- No rollout proceeds without offline and guardrail metrics.

### Phase 5 — Embeddings and world model

Introduce ANN retrieval and, only when data quality warrants it, learn:

    listener_state(t) + selected_track -> listener_state(t + 1)

The controller can then select tracks partly for the state they create, not
only immediate affinity. This phase is optional and never blocks the
deterministic director.

---

## 13. Test matrix

| Scenario | Required assertion |
|---|---|
| Fast skip of unknown | preview replaced; radius shrinks; track excluded |
| Accepted discovery | radius grows; source receives positive attribution |
| Artist loop | cap respected; compatible bridge used |
| Language fatigue | alternate language/instrumental appears if available |
| Album deep dive | tracks spread; no accidental completion block |
| Low entropy | explainable diversity plus familiar anchor |
| High entropy | comfort inserted without favourite collapse |
| Context change | new preview without losing fatigue history |
| Redis loss | durable event/plan restores coherent preview |
| Sparse metadata | safe plan and reduced-confidence record |
| Explicit artist intent | soft diversity may yield; served tracks never repeat |
| Long session | no served duplicate; callbacks/surprise/budgets bounded |

Use deterministic catalogue fixtures with known artists, albums, languages,
audio features, and graph routes. Test resulting sequences, not merely whether
a candidate list was sorted.

## Final product definition

Vibe succeeds when a listener can spend six hours with it and feel it
understood both their taste and their moment: it mixed comfort with meaningful
discovery, maintained a coherent evolving arc, avoided fatigue and obvious
loops, made memorable returns, and still left tomorrow feeling fresh.

