/** * Note lifecycle hooks — background AI enrichment triggered after save. * * Runs non-blocking (fire-and-forget) so note save is never delayed. * Gated behind feature flags. */ import { isFeatureEnabled } from './feature-flags.js'; import { embedText, stripHtmlForEmbedding } from './embeddings.js'; import { llm } from './llm.js'; import { trackEvent } from './telemetry.js'; import type { NoteDoc } from '../modules/notes/types.js'; import { getCollection } from './datastore.js'; import { onNoteSavedToPalace } from '../modules/palace/palace-hooks.js'; import type { FastifyBaseLogger } from 'fastify'; const MIN_WORDS_FOR_SUMMARY = 300; /** * Run after a note is created or updated. * Triggers background embedding + auto-summarize if enabled. */ export function runPostSaveHooks( note: NoteDoc, log: FastifyBaseLogger, workspaceName?: string, ): void { // Fire-and-forget — errors are logged, never thrown void backgroundEmbed(note, log); void backgroundAutoSummarize(note, log); void onNoteSavedToPalace(note, workspaceName ?? 'Untitled', log); } /** * Compute and store embedding vector for the note. */ async function backgroundEmbed( note: NoteDoc, log: FastifyBaseLogger, ): Promise { if (!isFeatureEnabled('notelett_auto_embed_enabled')) return; try { const plainText = stripHtmlForEmbedding(note.body ?? ''); if (plainText.length < 20) return; // Too short to embed meaningfully const embedding = await embedText(plainText); if (!embedding) return; // Update the note document with the embedding (don't overwrite other fields) const col = getCollection('notes', '/workspaceId'); const existing = await col.findById(note.id, note.workspaceId); if (!existing) return; await col.upsert({ ...existing, embedding }); log.debug({ noteId: note.id }, 'note embedding computed'); } catch (err) { log.warn({ noteId: note.id, err }, 'background embed failed'); } } /** * Auto-generate a summary artifact for long notes. */ async function backgroundAutoSummarize( note: NoteDoc, log: FastifyBaseLogger, ): Promise { if (!isFeatureEnabled('notelett_auto_summarize_enabled')) return; try { const plainText = stripHtmlForEmbedding(note.body ?? ''); const wordCount = plainText.split(/\s+/).filter(Boolean).length; if (wordCount < MIN_WORDS_FOR_SUMMARY) return; // Skip if already has a summary if (note.summaryArtifactId) return; const provider = llm(); const result = await provider.chatCompletion({ messages: [ { role: 'system', content: 'Create a concise summary (2-4 sentences) of the following note. Return only the summary.' }, { role: 'user', content: plainText.slice(0, 8000) }, ], temperature: 0.3, maxTokens: 512, }); const summary = result.content.trim(); if (!summary) return; // Store as artifact const { createNoteArtifact } = await import('../modules/note-artifacts/repository.js'); const now = new Date().toISOString(); const artifact = await createNoteArtifact({ id: `summary-${note.id}-${Date.now()}`, productId: note.productId, workspaceId: note.workspaceId, userId: note.userId, noteId: note.id, artifactType: 'summary', title: `Auto-summary of ${note.title}`, description: summary, createdAt: now, updatedAt: now, createdBy: 'system', updatedBy: 'system', }); // Link artifact back to note const col = getCollection('notes', '/workspaceId'); const existing = await col.findById(note.id, note.workspaceId); if (existing) { await col.upsert({ ...existing, summaryArtifactId: artifact.id }); } // Record agent action const { createNoteAgentAction } = await import('../modules/note-agent-actions/repository.js'); await createNoteAgentAction({ id: `auto-summary-${note.id}-${Date.now()}`, productId: note.productId, workspaceId: note.workspaceId, userId: note.userId, noteId: note.id, actorId: 'system', actorType: 'agent', toolName: 'auto_summarize', actionType: 'auto_enrich', state: 'applied', reason: `Auto-generated summary for note with ${wordCount} words`, afterSummary: summary.slice(0, 200), createdAt: now, updatedAt: now, createdBy: 'system', updatedBy: 'system', }); trackEvent('auto_summarize_triggered', note.userId, { wordCount: String(wordCount), noteId: note.id }); log.info({ noteId: note.id, artifactId: artifact.id }, 'auto-summary generated'); } catch (err) { log.warn({ noteId: note.id, err }, 'background auto-summarize failed'); } }