refactor(vibe): simplify durable session flow
This commit is contained in:
@@ -6,7 +6,6 @@ import { fileURLToPath } from 'url';
|
||||
import { dirname, join } from 'path';
|
||||
import { Pool, PoolClient } from 'pg';
|
||||
import { SearchService } from './search.service.js';
|
||||
import { normalizeVibeContext, normalizeVibeEventPayload } from './vibe-context.service.js';
|
||||
|
||||
/** Anything with a `.query()` — either the shared Pool or a checked-out client. */
|
||||
type Queryable = Pool | PoolClient;
|
||||
@@ -1515,18 +1514,12 @@ export class DbService {
|
||||
userId: string;
|
||||
policyVersion: string;
|
||||
seedTrackId?: string | null;
|
||||
context?: Record<string, unknown>;
|
||||
profile?: {
|
||||
goals: Record<string, unknown>;
|
||||
explorationCoefficient: number;
|
||||
discoveryRadius: number;
|
||||
};
|
||||
}): Promise<VibeSession> {
|
||||
// DbService is also used directly by workers and migrations. Keep the
|
||||
// durable storage boundary canonical even when callers bypass the HTTP
|
||||
// coordinator, so opaque or precise client telemetry can never become
|
||||
// session context.
|
||||
const canonicalContext = normalizeVibeContext(params.context ?? {});
|
||||
return this.withTransaction(async (client) => {
|
||||
// Serialize starts for one listener even when there is no active row to
|
||||
// lock yet. The row lock below then safely replaces any prior session.
|
||||
@@ -1568,7 +1561,7 @@ export class DbService {
|
||||
[
|
||||
params.userId,
|
||||
params.seedTrackId ?? null,
|
||||
JSON.stringify(canonicalContext),
|
||||
'{}',
|
||||
params.policyVersion,
|
||||
JSON.stringify(params.profile?.goals ?? { type: 'discovery', target: 1, progress: 0 }),
|
||||
params.profile?.explorationCoefficient ?? 0.3,
|
||||
@@ -1615,29 +1608,6 @@ export class DbService {
|
||||
return (res.rows[0] as VibeSessionProfile | undefined) ?? null;
|
||||
}
|
||||
|
||||
/** Replace only the coarse, sanitised context attached to an active session.
|
||||
* The immutable context_changed event remains the audit trail. */
|
||||
async updateVibeSessionContext(sessionId: string, userId: string, context: Record<string, unknown>): Promise<void> {
|
||||
const canonicalContext = normalizeVibeContext(context);
|
||||
await this.withTransaction(async client => {
|
||||
const updated = await client.query(
|
||||
`UPDATE vibe_sessions SET context = $3::jsonb, last_event_at = NOW()
|
||||
WHERE id = $1 AND user_id = $2 AND status = 'active'
|
||||
RETURNING id`,
|
||||
[sessionId, userId, JSON.stringify(canonicalContext)],
|
||||
);
|
||||
if (!updated.rows[0]) throw new Error('Vibe session was not found or is not owned by this user');
|
||||
await client.query(
|
||||
`UPDATE session_state
|
||||
SET context = COALESCE($3::jsonb->>'activity', $3::jsonb->>'device'),
|
||||
state_vector = state_vector || jsonb_build_object('context', $3::jsonb),
|
||||
last_interaction = NOW()
|
||||
WHERE session_id = $1 AND user_id = $2`,
|
||||
[sessionId, userId, JSON.stringify(canonicalContext)],
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Project unknown-track feedback into the session exploration controls.
|
||||
* This deliberately runs behind its own projection marker: the immutable
|
||||
@@ -1825,10 +1795,6 @@ export class DbService {
|
||||
durationMs?: number | null;
|
||||
payload?: Record<string, unknown>;
|
||||
}): Promise<RecordedVibeEvent> {
|
||||
// This service is also called by jobs and tests which bypass the HTTP
|
||||
// route. Preserve the context privacy boundary at the final point before
|
||||
// an immutable ledger write.
|
||||
const payload = normalizeVibeEventPayload(params.type, params.payload);
|
||||
return this.withTransaction(async (client) => {
|
||||
// A session-row lock serializes both event writes and terminal state
|
||||
// transitions. In particular, it avoids the READ COMMITTED CTE snapshot
|
||||
@@ -1880,7 +1846,7 @@ export class DbService {
|
||||
occurredAt,
|
||||
params.positionMs ?? null,
|
||||
params.durationMs ?? null,
|
||||
JSON.stringify(payload ?? {}),
|
||||
JSON.stringify(params.payload ?? {}),
|
||||
]
|
||||
);
|
||||
const event = insertRes.rows[0] as VibeEvent | undefined;
|
||||
@@ -1889,8 +1855,6 @@ export class DbService {
|
||||
}
|
||||
|
||||
await this.projectVibeFeedback(event, client);
|
||||
await this.projectVibeContextChanged(event, client);
|
||||
|
||||
await client.query(
|
||||
`UPDATE vibe_sessions
|
||||
SET last_event_at = GREATEST(last_event_at, $2::timestamptz)
|
||||
@@ -1901,30 +1865,6 @@ export class DbService {
|
||||
});
|
||||
}
|
||||
|
||||
/** Apply the context projection in the same transaction as its *inserted*
|
||||
* ledger event. A client-event retry returns before this method, so its body
|
||||
* can never overwrite session state with a different context. */
|
||||
private async projectVibeContextChanged(event: VibeEvent, client: PoolClient): Promise<void> {
|
||||
if (event.type !== 'context_changed') return;
|
||||
const context = event.payload?.context;
|
||||
if (!context || typeof context !== 'object' || Array.isArray(context)) return;
|
||||
const canonicalContext = context as Record<string, unknown>;
|
||||
await client.query(
|
||||
`UPDATE vibe_sessions
|
||||
SET context = $3::jsonb
|
||||
WHERE id = $1 AND user_id = $2 AND status = 'active'`,
|
||||
[event.session_id, event.user_id, JSON.stringify(canonicalContext)],
|
||||
);
|
||||
await client.query(
|
||||
`UPDATE session_state
|
||||
SET context = COALESCE($3::jsonb->>'activity', $3::jsonb->>'device'),
|
||||
state_vector = state_vector || jsonb_build_object('context', $3::jsonb),
|
||||
last_interaction = NOW()
|
||||
WHERE session_id = $1 AND user_id = $2`,
|
||||
[event.session_id, event.user_id, JSON.stringify(canonicalContext)],
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Materialize Vibe feedback into the listener inputs used by the incumbent
|
||||
* director. The projection marker and every write share the event's
|
||||
|
||||
Reference in New Issue
Block a user