feat(vibe): adapt sessions to context and exploration
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-02 02:08:35 +04:00
parent fe13798c99
commit 61a1373ca9
14 changed files with 782 additions and 16 deletions
@@ -1,6 +1,12 @@
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
@@ -21,7 +27,7 @@ export type VibeEventType = (typeof VIBE_EVENT_TYPES)[number];
export interface StartVibeSessionInput {
seedTrackId?: string;
context?: Record<string, unknown>;
context?: VibeContext | Record<string, unknown>;
intent?: string;
policyVersion?: string;
resumeSessionId?: string;
@@ -73,13 +79,19 @@ export class VibeSessionCoordinator {
async start(userId: string, input: StartVibeSessionInput): Promise<VibeSessionResponse> {
if (input.resumeSessionId) return this.resume(userId, input.resumeSessionId);
const context = input.context ?? {};
const context = normalizeVibeContext({ ...(input.context ?? {}) });
const initialState = initialVibeState(context);
const policyVersion = input.policyVersion ?? DEFAULT_VIBE_POLICY_VERSION;
const session = await this.db.createVibeSession({
userId,
policyVersion,
seedTrackId: input.seedTrackId ?? null,
context,
context: { ...context },
profile: {
goals: initialState.sessionGoal,
explorationCoefficient: initialState.explorationCoefficient,
discoveryRadius: initialState.discoveryRadius,
},
});
// session_state is a derived cache used by the current director. Give it
@@ -87,8 +99,15 @@ export class VibeSessionCoordinator {
// session while the durable tables remain the source of truth.
await this.db.createSessionState(
userId,
typeof context.activity === 'string' ? context.activity : input.intent,
{ energy: 0.5, noveltyHunger: 0.3 },
initialState.contextLabel ?? input.intent,
{
energy: initialState.energy,
noveltyHunger: initialState.noveltyHunger,
explorationCoefficient: initialState.explorationCoefficient,
discoveryRadius: initialState.discoveryRadius,
sessionGoal: initialState.sessionGoal,
context,
},
session.id,
);
await this.db.recordVibeEvent({
@@ -180,6 +199,11 @@ export class VibeSessionCoordinator {
input: AppendVibeEventInput,
): Promise<VibeSessionResponse & { event: VibeEvent; idempotent: boolean }> {
try {
// Do this before the ledger write, rather than after it, because Vibe
// 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,
@@ -189,8 +213,13 @@ export class VibeSessionCoordinator {
occurredAt: input.occurredAt,
positionMs: input.positionMs,
durationMs: input.durationMs,
payload: input.payload,
payload,
});
// The ledger write is authoritative; this idempotent projection updates
// exploration only after the exact event exists. Keep the compatibility
// guard for old coordinator test doubles during the migration.
const projectSessionFeedback = (this.db as Partial<DbService>).projectVibeSessionFeedback;
if (projectSessionFeedback) await projectSessionFeedback.call(this.db, result.event);
if (!isMaterialFeedback(input.type)) {
const response = await this.getPlan(userId, sessionId);
return { ...response, event: result.event, idempotent: !result.inserted };