feat(vibe): persist durable session plans and events
This commit is contained in:
@@ -20,3 +20,26 @@ describe('track release-date migration', () => {
|
||||
expect(migration!.sql).toContain('WHEN (OLD.release_date IS DISTINCT FROM NEW.release_date)');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Vibe durable session migration', () => {
|
||||
const migration = MIGRATIONS.find(
|
||||
({ id }) => id === '20260801_vibe_session_persistence',
|
||||
);
|
||||
|
||||
it('creates the event ledger, retry key, and revisioned plan tables', () => {
|
||||
expect(migration).toBeDefined();
|
||||
expect(migration!.sql).toContain('CREATE TABLE IF NOT EXISTS vibe_sessions');
|
||||
expect(migration!.sql).toContain('CREATE TABLE IF NOT EXISTS vibe_events');
|
||||
expect(migration!.sql).toContain('UNIQUE (session_id, client_event_id)');
|
||||
expect(migration!.sql).toContain('CREATE TABLE IF NOT EXISTS vibe_plan_versions');
|
||||
expect(migration!.sql).toContain('UNIQUE (session_id, version)');
|
||||
expect(migration!.sql).toContain('CREATE TABLE IF NOT EXISTS vibe_plan_items');
|
||||
expect(migration!.sql).toContain('UNIQUE (plan_version_id, track_id)');
|
||||
});
|
||||
|
||||
it('indexes session and event reads along their required time axes', () => {
|
||||
expect(migration!.sql).toContain('idx_vibe_sessions_user_last_event');
|
||||
expect(migration!.sql).toContain('idx_vibe_events_session_occurred');
|
||||
expect(migration!.sql).toContain('idx_vibe_events_user_occurred');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -644,4 +644,68 @@ export const MIGRATIONS: Migration[] = [
|
||||
EXECUTE FUNCTION propagate_album_release_date_to_tracks();
|
||||
`,
|
||||
},
|
||||
{
|
||||
// Vibe v2 needs an immutable event ledger and revisioned plans. The
|
||||
// legacy session_state table remains in place as a derived-state cache so
|
||||
// existing v2 endpoints can migrate independently.
|
||||
id: '20260801_vibe_session_persistence',
|
||||
sql: `
|
||||
CREATE TABLE IF NOT EXISTS 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 INDEX IF NOT EXISTS idx_vibe_sessions_user_last_event
|
||||
ON vibe_sessions (user_id, last_event_at DESC);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS 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 INDEX IF NOT EXISTS idx_vibe_events_session_occurred
|
||||
ON vibe_events (session_id, occurred_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_vibe_events_user_occurred
|
||||
ON vibe_events (user_id, occurred_at DESC);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS 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 CHECK (version > 0),
|
||||
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 IF NOT EXISTS vibe_plan_items (
|
||||
plan_version_id UUID NOT NULL REFERENCES vibe_plan_versions(id) ON DELETE CASCADE,
|
||||
ordinal INTEGER NOT NULL CHECK (ordinal >= 0),
|
||||
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)
|
||||
);
|
||||
`,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -525,6 +525,68 @@ CREATE TABLE IF NOT EXISTS session_state (
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_session_state_user ON session_state (user_id, last_interaction DESC);
|
||||
|
||||
-- Durable Vibe v2 session ledger. session_state remains a rebuildable cache for
|
||||
-- the existing director; these tables are the authoritative record for the
|
||||
-- next-generation, versioned planner.
|
||||
CREATE TABLE IF NOT EXISTS 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 INDEX IF NOT EXISTS idx_vibe_sessions_user_last_event
|
||||
ON vibe_sessions (user_id, last_event_at DESC);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS 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 INDEX IF NOT EXISTS idx_vibe_events_session_occurred
|
||||
ON vibe_events (session_id, occurred_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_vibe_events_user_occurred
|
||||
ON vibe_events (user_id, occurred_at DESC);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS 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 CHECK (version > 0),
|
||||
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 IF NOT EXISTS vibe_plan_items (
|
||||
plan_version_id UUID NOT NULL REFERENCES vibe_plan_versions(id) ON DELETE CASCADE,
|
||||
ordinal INTEGER NOT NULL CHECK (ordinal >= 0),
|
||||
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)
|
||||
);
|
||||
|
||||
-- Diversity budgets for the session director's planner.
|
||||
CREATE TABLE IF NOT EXISTS diversity_budgets (
|
||||
user_id UUID NOT NULL,
|
||||
|
||||
@@ -194,3 +194,67 @@ export interface RepetitionRule {
|
||||
dimension: string;
|
||||
min_distance: number;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Vibe v2 durable session ledger
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const VIBE_SESSION_STATUSES = ['active', 'paused', 'ended', 'expired', 'replaced'] as const;
|
||||
export type VibeSessionStatus = (typeof VIBE_SESSION_STATUSES)[number];
|
||||
|
||||
export interface VibeSession {
|
||||
id: string;
|
||||
user_id: string;
|
||||
status: VibeSessionStatus;
|
||||
seed_track_id: string | null;
|
||||
context: Record<string, unknown>;
|
||||
policy_version: string;
|
||||
started_at: Date;
|
||||
last_event_at: Date;
|
||||
ended_at: Date | null;
|
||||
}
|
||||
|
||||
export interface VibeEvent {
|
||||
id: string;
|
||||
client_event_id: string | null;
|
||||
session_id: string;
|
||||
user_id: string;
|
||||
track_id: string | null;
|
||||
type: string;
|
||||
occurred_at: Date;
|
||||
position_ms: number | null;
|
||||
duration_ms: number | null;
|
||||
payload: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface RecordedVibeEvent {
|
||||
event: VibeEvent;
|
||||
/** False when a retried client_event_id returned the original event. */
|
||||
inserted: boolean;
|
||||
}
|
||||
|
||||
export interface VibePlanVersion {
|
||||
id: string;
|
||||
session_id: string;
|
||||
version: number;
|
||||
reason: string;
|
||||
state_snapshot: Record<string, unknown>;
|
||||
objective_snapshot: Record<string, unknown>;
|
||||
created_at: Date;
|
||||
}
|
||||
|
||||
export interface VibePlanItem {
|
||||
plan_version_id: string;
|
||||
ordinal: number;
|
||||
track_id: string;
|
||||
slot_role: string | null;
|
||||
candidate_source: string;
|
||||
score: number;
|
||||
score_breakdown: Record<string, unknown>;
|
||||
explanation: unknown;
|
||||
committed: boolean;
|
||||
}
|
||||
|
||||
export interface VibePlan extends VibePlanVersion {
|
||||
items: VibePlanItem[];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user