feat(vibe): persist durable session plans and events

This commit is contained in:
kami
2026-08-01 22:32:42 +04:00
parent 3641ec9e8e
commit 515cab2f89
6 changed files with 639 additions and 0 deletions
+23
View File
@@ -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');
});
});
+64
View File
@@ -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)
);
`,
},
];
+62
View File
@@ -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,
+64
View File
@@ -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[];
}