refactor(vibe): simplify durable session flow
Typecheck / typecheck (backend) (push) Has been cancelled
Typecheck / typecheck (workers) (push) Has been cancelled
Typecheck / typecheck (backend) (pull_request) Has been cancelled
Typecheck / typecheck (workers) (pull_request) Has been cancelled

This commit is contained in:
kami
2026-08-03 12:44:08 +04:00
parent 61a1373ca9
commit 9eba247a58
16 changed files with 191 additions and 520 deletions
@@ -1,12 +1,6 @@
import { DbService, VibeEvent, VibePlan, VibeSession } from './db.service.js';
import { SessionDirector } from './session-director.service.js';
import { Candidate } from './generators.service.js';
import {
initialVibeState,
normalizeVibeContext,
normalizeVibeEventPayload,
VibeContext,
} from './vibe-context.service.js';
/**
* This is deliberately a narrow bridge between the durable Vibe ledger and
@@ -16,7 +10,7 @@ import {
export const DEFAULT_VIBE_POLICY_VERSION = 'vibe-v2-initial';
export const VIBE_EVENT_TYPES = [
'session_started', 'session_resumed', 'session_ended', 'context_changed',
'session_started', 'session_resumed', 'session_ended',
'plan_published', 'track_served', 'playback_started', 'progress', 'completed',
'skipped', 'disliked', 'kept', 'favourite_added', 'queue_removed',
'manual_search', 'album_opened', 'artist_opened', 'playlist_added',
@@ -27,9 +21,6 @@ export type VibeEventType = (typeof VIBE_EVENT_TYPES)[number];
export interface StartVibeSessionInput {
seedTrackId?: string;
context?: VibeContext | Record<string, unknown>;
intent?: string;
policyVersion?: string;
resumeSessionId?: string;
}
@@ -79,14 +70,19 @@ export class VibeSessionCoordinator {
async start(userId: string, input: StartVibeSessionInput): Promise<VibeSessionResponse> {
if (input.resumeSessionId) return this.resume(userId, input.resumeSessionId);
const context = normalizeVibeContext({ ...(input.context ?? {}) });
const initialState = initialVibeState(context);
const policyVersion = input.policyVersion ?? DEFAULT_VIBE_POLICY_VERSION;
// Vibe has no reliable device/activity/location signal. Start from neutral
// recommendation state and let actual listening behaviour shape the plan.
const initialState = {
energy: 0.5,
noveltyHunger: 0.3,
explorationCoefficient: 0.3,
discoveryRadius: 0.38,
sessionGoal: { type: 'discovery' as const, target: 1, progress: 0 },
};
const session = await this.db.createVibeSession({
userId,
policyVersion,
policyVersion: DEFAULT_VIBE_POLICY_VERSION,
seedTrackId: input.seedTrackId ?? null,
context: { ...context },
profile: {
goals: initialState.sessionGoal,
explorationCoefficient: initialState.explorationCoefficient,
@@ -99,14 +95,13 @@ export class VibeSessionCoordinator {
// session while the durable tables remain the source of truth.
await this.db.createSessionState(
userId,
initialState.contextLabel ?? input.intent,
undefined,
{
energy: initialState.energy,
noveltyHunger: initialState.noveltyHunger,
explorationCoefficient: initialState.explorationCoefficient,
discoveryRadius: initialState.discoveryRadius,
sessionGoal: initialState.sessionGoal,
context,
},
session.id,
);
@@ -114,7 +109,7 @@ export class VibeSessionCoordinator {
sessionId: session.id,
userId,
type: 'session_started',
payload: { policyVersion, context, intent: input.intent ?? null },
payload: { policyVersion: DEFAULT_VIBE_POLICY_VERSION },
});
const candidates = await this.director.buildPlan(userId, session.id, input.seedTrackId);
@@ -128,8 +123,7 @@ export class VibeSessionCoordinator {
reason: 'session_started',
stateSnapshot: state,
objectiveSnapshot: {
policyVersion,
intent: input.intent ?? null,
policyVersion: DEFAULT_VIBE_POLICY_VERSION,
horizonTracks: candidates.length,
...(candidates[0]?.plan?.objective ?? {}),
},
@@ -203,7 +197,6 @@ export class VibeSessionCoordinator {
// events are immutable. The DB repeats this boundary for non-HTTP
// callers; keeping it here also makes coordinator callers see exactly
// what will be persisted.
const payload = normalizeVibeEventPayload(input.type, input.payload);
const result = await this.db.recordVibeEvent({
sessionId,
userId,
@@ -213,7 +206,7 @@ export class VibeSessionCoordinator {
occurredAt: input.occurredAt,
positionMs: input.positionMs,
durationMs: input.durationMs,
payload,
payload: input.payload,
});
// The ledger write is authoritative; this idempotent projection updates
// exploration only after the exact event exists. Keep the compatibility