/** * Wake-up context builder for NoteLett palace. * * Builds L0/L1/L2 layered context within a configurable token budget. * Uses @bytelyst/palace shared wakeup builder for assembly + truncation. */ import { buildWakeUpLayers, WAKEUP_PRESETS, estimateTokens, truncateToTokenBudget } from '@bytelyst/palace'; import type { WakeUpContext } from '@bytelyst/palace'; import { config } from '../../lib/config.js'; import { embedText, stripHtmlForEmbedding } from '../../lib/embeddings.js'; import { getWing, listRooms, listMemories, searchSemantic, getWingSummary } from './repository.js'; import type { PalaceMemoryDoc, PalaceWingDoc } from './types.js'; export type { WakeUpContext }; export interface NoteLettWakeUpContext extends WakeUpContext { wingId: string; wingName: string; } const NOTELETT_PRESET = WAKEUP_PRESETS.notelett ?? { totalBudget: 600, l0Budget: 50, l1Budget: 150, l2Budget: 400, }; /** * Build wake-up context for a workspace/wing. * * L0 — Identity: workspace name + room topics (~50 tokens) * L1 — Critical Facts: recent decisions/preferences/insights (~150 tokens) * L2 — Task Context: semantically relevant memories (~400 tokens) */ export async function buildNoteLettWakeUp( userId: string, productId: string, wingId: string, taskDescription?: string, ): Promise { const wing = await getWing(userId, productId, wingId); if (!wing) { return { text: '', layers: [], totalChars: 0, truncated: false, wingId, wingName: 'Unknown', }; } const budget = { ...NOTELETT_PRESET, totalBudget: config.PALACE_WAKE_UP_BUDGET, }; const l0 = await buildL0Identity(userId, productId, wing); const l1 = await buildL1CriticalFacts(userId, productId, wingId, wing); const l2 = await buildL2TaskContext(userId, productId, wingId, taskDescription); const ctx = buildWakeUpLayers(l0, l1, l2, budget); return { ...ctx, wingId, wingName: wing.name, }; } /** * L0 — Identity: workspace name, description, room topics. */ async function buildL0Identity( userId: string, productId: string, wing: PalaceWingDoc, ): Promise { const rooms = await listRooms(userId, productId, wing.id); const roomNames = rooms.map(r => r.name).slice(0, 10).join(', '); const parts = [`Workspace: ${wing.name}`]; if (wing.description) parts.push(wing.description); if (roomNames) parts.push(`Topics: ${roomNames}`); return parts.join('\n'); } /** * L1 — Critical Facts: recent decisions, preferences, insights. * Uses cached L1 if available, otherwise regenerates. */ async function buildL1CriticalFacts( userId: string, productId: string, wingId: string, wing: PalaceWingDoc, ): Promise { // Try cached L1 if (wing.l1Cache && wing.l1CacheUpdatedAt) { const cacheAge = Date.now() - new Date(wing.l1CacheUpdatedAt).getTime(); if (cacheAge < 24 * 3600_000) return wing.l1Cache; } // Rebuild from recent decisions + preferences + insights const factHalls = ['decisions', 'preferences', 'insights'] as const; const recentFacts: PalaceMemoryDoc[] = []; for (const hall of factHalls) { const mems = await listMemories(userId, productId, { wingId, hall, limit: 5, }); recentFacts.push(...mems); } if (recentFacts.length === 0) return ''; // Sort by recency recentFacts.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt)); const lines = recentFacts.slice(0, 8).map(m => `- [${m.hall}] ${m.content}`); return lines.join('\n'); } /** * L2 — Task Context: semantically relevant memories (or recent if no task). */ async function buildL2TaskContext( userId: string, productId: string, wingId: string, taskDescription?: string, ): Promise { let memories: PalaceMemoryDoc[]; if (taskDescription && taskDescription.trim().length > 0) { const embedding = await embedText(taskDescription); if (embedding) { memories = await searchSemantic(userId, productId, taskDescription, embedding, wingId, 8); } else { memories = await listMemories(userId, productId, { wingId, limit: 8 }); } } else { memories = await listMemories(userId, productId, { wingId, limit: 8 }); } if (memories.length === 0) return ''; const lines = memories.map(m => `- [${m.hall}] ${m.content}`); return lines.join('\n'); } /** * Regenerate L1 cache for a wing (called after memory extraction). */ export async function regenerateCriticalFacts( userId: string, productId: string, wingId: string, ): Promise { const wing = await getWing(userId, productId, wingId); if (!wing) return ''; const l1 = await buildL1CriticalFacts(userId, productId, wingId, wing); // Update wing doc with cached L1 const { getCollection } = await import('../../lib/datastore.js'); const col = getCollection('palace_wings', '/userId'); await col.update(wingId, userId, { l1Cache: l1, l1CacheUpdatedAt: new Date().toISOString(), } as Partial); return l1; }