Merge pull request #2106 from TriliumNext/fix/llm-becca-sync

fix(llm): Fix Note Embeddings not being synced correctly and causing sync loops
This commit is contained in:
Elian Doran 2025-06-04 11:38:49 +03:00 committed by GitHub
commit 8445ece231
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 47 additions and 6 deletions

View File

@ -35,8 +35,8 @@ async function processEntityChanges(entityChanges: EntityChange[]) {
loadResults.addOption(attributeEntity.name); loadResults.addOption(attributeEntity.name);
} else if (ec.entityName === "attachments") { } else if (ec.entityName === "attachments") {
processAttachment(loadResults, ec); processAttachment(loadResults, ec);
} else if (ec.entityName === "blobs" || ec.entityName === "etapi_tokens") { } else if (ec.entityName === "blobs" || ec.entityName === "etapi_tokens" || ec.entityName === "note_embeddings") {
// NOOP // NOOP - these entities are handled at the backend level and don't require frontend processing
} else { } else {
throw new Error(`Unknown entityName '${ec.entityName}'`); throw new Error(`Unknown entityName '${ec.entityName}'`);
} }

View File

@ -44,9 +44,17 @@ interface OptionRow {}
interface NoteReorderingRow {} interface NoteReorderingRow {}
interface ContentNoteIdToComponentIdRow { interface NoteEmbeddingRow {
embedId: string;
noteId: string; noteId: string;
componentId: string; providerId: string;
modelId: string;
dimension: number;
version: number;
dateCreated: string;
utcDateCreated: string;
dateModified: string;
utcDateModified: string;
} }
type EntityRowMappings = { type EntityRowMappings = {
@ -56,6 +64,7 @@ type EntityRowMappings = {
options: OptionRow; options: OptionRow;
revisions: RevisionRow; revisions: RevisionRow;
note_reordering: NoteReorderingRow; note_reordering: NoteReorderingRow;
note_embeddings: NoteEmbeddingRow;
}; };
export type EntityRowNames = keyof EntityRowMappings; export type EntityRowNames = keyof EntityRowMappings;

View File

@ -12,6 +12,7 @@ import type { AttachmentRow, BlobRow, RevisionRow } from "@triliumnext/commons";
import BBlob from "./entities/bblob.js"; import BBlob from "./entities/bblob.js";
import BRecentNote from "./entities/brecent_note.js"; import BRecentNote from "./entities/brecent_note.js";
import type AbstractBeccaEntity from "./entities/abstract_becca_entity.js"; import type AbstractBeccaEntity from "./entities/abstract_becca_entity.js";
import type BNoteEmbedding from "./entities/bnote_embedding.js";
interface AttachmentOpts { interface AttachmentOpts {
includeContentLength?: boolean; includeContentLength?: boolean;
@ -32,6 +33,7 @@ export default class Becca {
attributeIndex!: Record<string, BAttribute[]>; attributeIndex!: Record<string, BAttribute[]>;
options!: Record<string, BOption>; options!: Record<string, BOption>;
etapiTokens!: Record<string, BEtapiToken>; etapiTokens!: Record<string, BEtapiToken>;
noteEmbeddings!: Record<string, BNoteEmbedding>;
allNoteSetCache: NoteSet | null; allNoteSetCache: NoteSet | null;
@ -48,6 +50,7 @@ export default class Becca {
this.attributeIndex = {}; this.attributeIndex = {};
this.options = {}; this.options = {};
this.etapiTokens = {}; this.etapiTokens = {};
this.noteEmbeddings = {};
this.dirtyNoteSetCache(); this.dirtyNoteSetCache();

View File

@ -9,9 +9,10 @@ import BBranch from "./entities/bbranch.js";
import BAttribute from "./entities/battribute.js"; import BAttribute from "./entities/battribute.js";
import BOption from "./entities/boption.js"; import BOption from "./entities/boption.js";
import BEtapiToken from "./entities/betapi_token.js"; import BEtapiToken from "./entities/betapi_token.js";
import BNoteEmbedding from "./entities/bnote_embedding.js";
import cls from "../services/cls.js"; import cls from "../services/cls.js";
import entityConstructor from "../becca/entity_constructor.js"; import entityConstructor from "../becca/entity_constructor.js";
import type { AttributeRow, BranchRow, EtapiTokenRow, NoteRow, OptionRow } from "@triliumnext/commons"; import type { AttributeRow, BranchRow, EtapiTokenRow, NoteRow, OptionRow, NoteEmbeddingRow } from "@triliumnext/commons";
import type AbstractBeccaEntity from "./entities/abstract_becca_entity.js"; import type AbstractBeccaEntity from "./entities/abstract_becca_entity.js";
import ws from "../services/ws.js"; import ws from "../services/ws.js";
@ -63,6 +64,10 @@ function load() {
for (const row of sql.getRows<EtapiTokenRow>(/*sql*/`SELECT etapiTokenId, name, tokenHash, utcDateCreated, utcDateModified FROM etapi_tokens WHERE isDeleted = 0`)) { for (const row of sql.getRows<EtapiTokenRow>(/*sql*/`SELECT etapiTokenId, name, tokenHash, utcDateCreated, utcDateModified FROM etapi_tokens WHERE isDeleted = 0`)) {
new BEtapiToken(row); new BEtapiToken(row);
} }
for (const row of sql.getRows<NoteEmbeddingRow>(/*sql*/`SELECT embedId, noteId, providerId, modelId, dimension, embedding, version, dateCreated, dateModified, utcDateCreated, utcDateModified FROM note_embeddings`)) {
new BNoteEmbedding(row).init();
}
}); });
for (const noteId in becca.notes) { for (const noteId in becca.notes) {
@ -85,7 +90,7 @@ eventService.subscribeBeccaLoader([eventService.ENTITY_CHANGE_SYNCED], ({ entity
return; return;
} }
if (["notes", "branches", "attributes", "etapi_tokens", "options"].includes(entityName)) { if (["notes", "branches", "attributes", "etapi_tokens", "options", "note_embeddings"].includes(entityName)) {
const EntityClass = entityConstructor.getEntityFromEntityName(entityName); const EntityClass = entityConstructor.getEntityFromEntityName(entityName);
const primaryKeyName = EntityClass.primaryKeyName; const primaryKeyName = EntityClass.primaryKeyName;
@ -143,6 +148,8 @@ eventService.subscribeBeccaLoader([eventService.ENTITY_DELETED, eventService.ENT
attributeDeleted(entityId); attributeDeleted(entityId);
} else if (entityName === "etapi_tokens") { } else if (entityName === "etapi_tokens") {
etapiTokenDeleted(entityId); etapiTokenDeleted(entityId);
} else if (entityName === "note_embeddings") {
noteEmbeddingDeleted(entityId);
} }
}); });
@ -278,6 +285,10 @@ function etapiTokenDeleted(etapiTokenId: string) {
delete becca.etapiTokens[etapiTokenId]; delete becca.etapiTokens[etapiTokenId];
} }
function noteEmbeddingDeleted(embedId: string) {
delete becca.noteEmbeddings[embedId];
}
eventService.subscribeBeccaLoader(eventService.ENTER_PROTECTED_SESSION, () => { eventService.subscribeBeccaLoader(eventService.ENTER_PROTECTED_SESSION, () => {
try { try {
becca.decryptProtectedNotes(); becca.decryptProtectedNotes();

View File

@ -32,6 +32,12 @@ class BNoteEmbedding extends AbstractBeccaEntity<BNoteEmbedding> {
} }
} }
init() {
if (this.embedId) {
this.becca.noteEmbeddings[this.embedId] = this;
}
}
updateFromRow(row: NoteEmbeddingRow): void { updateFromRow(row: NoteEmbeddingRow): void {
this.embedId = row.embedId; this.embedId = row.embedId;
this.noteId = row.noteId; this.noteId = row.noteId;
@ -44,6 +50,10 @@ class BNoteEmbedding extends AbstractBeccaEntity<BNoteEmbedding> {
this.dateModified = row.dateModified; this.dateModified = row.dateModified;
this.utcDateCreated = row.utcDateCreated; this.utcDateCreated = row.utcDateCreated;
this.utcDateModified = row.utcDateModified; this.utcDateModified = row.utcDateModified;
if (this.embedId) {
this.becca.noteEmbeddings[this.embedId] = this;
}
} }
override beforeSaving() { override beforeSaving() {

View File

@ -799,6 +799,7 @@ class ConsistencyChecks {
this.runEntityChangeChecks("attributes", "attributeId"); this.runEntityChangeChecks("attributes", "attributeId");
this.runEntityChangeChecks("etapi_tokens", "etapiTokenId"); this.runEntityChangeChecks("etapi_tokens", "etapiTokenId");
this.runEntityChangeChecks("options", "name"); this.runEntityChangeChecks("options", "name");
this.runEntityChangeChecks("note_embeddings", "embedId");
} }
findWronglyNamedAttributes() { findWronglyNamedAttributes() {

View File

@ -203,6 +203,13 @@ function fillInAdditionalProperties(entityChange: EntityChange) {
WHERE attachmentId = ?`, WHERE attachmentId = ?`,
[entityChange.entityId] [entityChange.entityId]
); );
} else if (entityChange.entityName === "note_embeddings") {
// Note embeddings are backend-only entities for AI/vector search
// Frontend doesn't need the full embedding data (which is large binary data)
// Just ensure entity is marked as handled - actual sync happens at database level
if (!entityChange.isErased) {
entityChange.entity = { embedId: entityChange.entityId };
}
} }
if (entityChange.entity instanceof AbstractBeccaEntity) { if (entityChange.entity instanceof AbstractBeccaEntity) {