[{"data":1,"prerenderedAt":1608},["ShallowReactive",2],{"search-de":3,"content-de-vision\u002Ffile-system":4,"surround-de-\u002Fvision\u002Ffile-system":1599},[],{"id":5,"title":6,"body":7,"description":1591,"extension":1592,"meta":1593,"navigation":1594,"path":1595,"seo":1596,"stem":1597,"__hash__":1598},"content_de\u002F5.vision\u002F5.file-system.md","Semantisches Dateisystem",{"type":8,"value":9,"toc":1579},"minimark",[10,15,19,84,89,96,127,172,182,192,226,252,256,261,274,382,397,401,452,659,691,695,730,733,737,740,791,801,813,817,831,837,840,870,880,884,893,1505,1524,1528,1542,1554,1575],[11,12,14],"h1",{"id":13},"architektur-des-semantischen-dateisystems","Architektur des semantischen Dateisystems",[16,17,18],"p",{},"Owlat speichert bereits heute Medien-Assets für E-Mail-Kampagnen. Das semantische Dateisystem erweitert das zu einer breiteren organisatorischen Dateischicht — in der Dateien Zusammenfassungen, automatische Tags, Embeddings und eine konversationsverknüpfte Provenienz erhalten. Die Vision: Dateien werden Teil des Agentenkontexts und tauchen automatisch auf, wenn sie relevant sind.",[16,20,21,22,26,27,30,31,34,35,39,40,43,44,47,48,51,52,55,56,59,60,63,64,67,68,71,72,75,76,79,80,83],{},"Das meiste davon ist heute gebaut: die Tabelle ",[23,24,25],"code",{},"semanticFiles",", CRUD-Funktionen in ",[23,28,29],{},"apps\u002Fapi\u002Fconvex\u002FsemanticFiles.ts",", Volltextsuche, Versionshistorie und eine Dateien-UI im Dashboard (",[23,32,33],{},"apps\u002Fweb\u002Fapp\u002Fpages\u002Fdashboard\u002Ffiles\u002F",", dazu ein Dateien-Tab an jedem Kontakt). Die KI-Verarbeitungsstrecke (Textextraktion, Zusammenfassung, automatische Tags, Embeddings) ist inzwischen ",[36,37,38],"strong",{},"angebunden und eingeplant"," — die Mutation ",[23,41,42],{},"create"," stößt ",[23,45,46],{},"processFile"," an, echte Vektorsuche läuft über den Index ",[23,49,50],{},"vector_files",", und der Schritt ",[23,53,54],{},"context_retrieval"," des Agenten zieht relevante Dateien in sein Briefing. Eingehende ",[36,57,58],{},"E-Mail-Anhänge werden automatisch aufgenommen",": ",[23,61,62],{},"mail.delivery.ingestFromWebhook"," holt jedes Anhang-Blatt aus der zugestellten ",[23,65,66],{},".eml"," und ruft den Einstiegspunkt ",[23,69,70],{},"semanticFiles.ingest"," mit ",[23,73,74],{},"sourceType: 'email_attachment'"," auf. Was noch fehlt: ein Erzeuger ",[23,77,78],{},"agent_generated"," (der Einstiegspunkt ",[23,81,82],{},"ingest"," akzeptiert diesen Quellentyp bereits — nur emittiert noch kein Agenten-Flow Dateien), echte Extraktion für Nicht-PDF-Binärdateien (Word\u002FExcel\u002FBilder) sowie Vorschläge zum Zusammenführen von Tags. Jeder Punkt wird unten an Ort und Stelle benannt.",[85,86,88],"h2",{"id":87},"so-funktioniert-es","So funktioniert es",[16,90,91,92,95],{},"Dateien gelangen über drei Quellentypen ins System (das Feld ",[23,93,94],{},"sourceType","):",[97,98,99,110,119],"ol",{},[100,101,102,105,106,109],"li",{},[36,103,104],{},"Direkter Upload"," (",[23,107,108],{},"upload",") — über die Dateien-UI im Dashboard.",[100,111,112,105,115,118],{},[36,113,114],{},"E-Mail-Anhang",[23,116,117],{},"email_attachment",") — der vorgesehene Weg für eingehende Anhänge.",[100,120,121,105,124,126],{},[36,122,123],{},"Agenten-generiert",[23,125,78],{},") — Artefakte, die ein Agent erzeugt.",[128,129,132],"callout",{"title":130,"type":131},"Heute vs. geplant","info",[16,133,134,135,105,138,141,142,144,145,105,147,149,150,105,152,154,155,157,158,160,161,163,164,166,167,59,169,171],{},"Der direkte Upload ist über das Dashboard durchgängig angebunden und durchläuft inzwischen die vollständige Verarbeitungsstrecke. Der gemeinsame Helper ",[23,136,137],{},"insertSemanticFile",[23,139,140],{},"semanticFiles.ts",") unterstützt alle drei Quellentypen, fügt aber nur die Zeile ein (und synchronisiert Kontakte). Zwei Einstiegspunkte planen ",[23,143,46],{}," ein: die nutzerseitige Mutation ",[23,146,42],{},[23,148,108],{},") und die interne Mutation ",[23,151,82],{},[23,153,117],{}," \u002F ",[23,156,78],{},"). Eingehende Anhänge fließen inzwischen automatisch — ",[23,159,62],{}," extrahiert jeden Anhang aus der rohen ",[23,162,66],{}," und ruft ",[23,165,82],{}," auf, sodass sie in der Dateibibliothek und in der Strecke Datei→Wissen landen. Die verbleibende Lücke ist die Quelle ",[23,168,78],{},[23,170,82],{}," akzeptiert sie, aber noch emittiert kein Agenten-Flow Artefakte.",[16,173,174,175,177,178,181],{},"Die Verarbeitungsstrecke (",[23,176,46],{}," in ",[23,179,180],{},"apps\u002Fapi\u002Fconvex\u002FsemanticFileProcessing.ts",") läuft für jede neue Datei:",[183,184,189],"pre",{"className":185,"code":187,"language":188},[186],"language-text","File uploaded \u002F received\n  → Store binary in Convex _storage\n  → Extract text content (by MIME type):\n    - text\u002F*, application\u002Fjson → raw text\n    - text\u002Fhtml → tags stripped\n    - text\u002Fcsv (or .csv) → raw text\n    - PDF → real text extraction via unpdf (placeholder on failure)\n    - Word, Excel, images → placeholder string ([Word document: name], etc.)\n  → Generate title, 2-3 sentence summary, and 5-10 auto-tags via the LLM\n  → Inherit context auto-tags from the thread subject + related contacts\n  → Generate an embedding (text-embedding-3-small, 1536 dims)\n  → Record embeddingModel + embeddingGeneratedAt for re-embedding\n  → Build searchableText for full-text search\n  → Patch metadata back onto the semanticFiles row\n  → Feed real extracted text into the knowledge graph (extractFromFile)\n","text",[23,190,187],{"__ignoreMap":191},"",[128,193,196],{"title":194,"type":195},"Die Verarbeitungsstrecke ist jetzt eingeplant","success",[16,197,198,200,201,204,205,207,208,211,212,105,215,218,219,154,222,225],{},[23,199,46],{}," ist eine ",[23,202,203],{},"internalAction",", die die nutzerseitige Mutation ",[23,206,42],{}," einplant (",[23,209,210],{},"ctx.scheduler.runAfter(0, …)","), sobald eine Datei eingefügt wird. Ein Cron ",[23,213,214],{},"backfillUnprocessed",[23,216,217],{},"crons.ts",", alle 15 Minuten) dient als Sicherheitsnetz und plant die Verarbeitung für kürzlich erstellte Dateien erneut ein, deren Embedding nie angekommen ist (z. B. verloren durch eine Deployment-Lücke). Hochgeladene Dateien tragen damit einen per LLM erzeugten Titel bzw. eine Zusammenfassung, automatische Tags, ein echtes Embedding sowie ",[23,220,221],{},"embeddingModel",[23,223,224],{},"embeddingGeneratedAt",".",[128,227,230],{"title":228,"type":229},"Textextraktion aus Binärdateien ist nur für PDFs aktiv","warning",[16,231,232,235,236,239,240,243,244,247,248,251],{},[23,233,234],{},"extractText()"," liest Klartext, JSON, HTML und CSV und extrahiert inzwischen echten Text aus PDFs mithilfe des Pakets ",[23,237,238],{},"unpdf"," (reines JS, serverless-tauglich); bei jedem Fehlschlag fällt es auf den Platzhalter ",[23,241,242],{},"[PDF file: contract.pdf]"," zurück. Word-, Excel- und Bilddateien werden weiterhin mit einem Platzhaltertext gespeichert (z. B. ",[23,245,246],{},"[Word document: report.docx]",", ",[23,249,250],{},"[Image: scan.png]",") und stützen sich für Zusammenfassung und Embedding auf den Dateinamen sowie einen eventuell vom Nutzer vergebenen Titel — es gibt bislang weder einen DOCX\u002FXLSX-Parser noch OCR für Bilder. Dateien, die als Platzhalter-Stub verbleiben, überspringt der Extraktionsschritt Datei→Wissen.",[85,253,255],{"id":254},"retrieval","Retrieval",[257,258,260],"h3",{"id":259},"volltextsuche","Volltextsuche",[16,262,263,264,267,268,177,271,273],{},"Die Stichwortsuche über ",[23,265,266],{},"searchableText"," ist live, über die Query ",[23,269,270],{},"search",[23,272,29],{},":",[183,275,279],{"className":276,"code":277,"language":278,"meta":191,"style":191},"language-typescript shiki shiki-themes github-light github-dark-dimmed","const results = await ctx.db\n  .query('semanticFiles')\n  .withSearchIndex('search_files', (q) => q.search('searchableText', searchQuery))\n  .take(limit ?? 20)\n","typescript",[23,280,281,304,324,363],{"__ignoreMap":191},[282,283,286,290,294,297,300],"span",{"class":284,"line":285},"line",1,[282,287,289],{"class":288},"s7YZ4","const",[282,291,293],{"class":292},"sviXB"," results",[282,295,296],{"class":288}," =",[282,298,299],{"class":288}," await",[282,301,303],{"class":302},"sYgZi"," ctx.db\n",[282,305,307,310,314,317,321],{"class":284,"line":306},2,[282,308,309],{"class":302},"  .",[282,311,313],{"class":312},"sPO5f","query",[282,315,316],{"class":302},"(",[282,318,320],{"class":319},"s-HuK","'semanticFiles'",[282,322,323],{"class":302},")\n",[282,325,327,329,332,334,337,340,344,347,350,353,355,357,360],{"class":284,"line":326},3,[282,328,309],{"class":302},[282,330,331],{"class":312},"withSearchIndex",[282,333,316],{"class":302},[282,335,336],{"class":319},"'search_files'",[282,338,339],{"class":302},", (",[282,341,343],{"class":342},"stnAF","q",[282,345,346],{"class":302},") ",[282,348,349],{"class":288},"=>",[282,351,352],{"class":302}," q.",[282,354,270],{"class":312},[282,356,316],{"class":302},[282,358,359],{"class":319},"'searchableText'",[282,361,362],{"class":302},", searchQuery))\n",[282,364,366,368,371,374,377,380],{"class":284,"line":365},4,[282,367,309],{"class":302},[282,369,370],{"class":312},"take",[282,372,373],{"class":302},"(limit ",[282,375,376],{"class":288},"??",[282,378,379],{"class":292}," 20",[282,381,323],{"class":302},[16,383,384,385,388,389,392,393,396],{},"Der Suchindex besitzt keine ",[23,386,387],{},"filterFields"," — Owlat betreibt genau eine Organisation pro Deployment (siehe ",[23,390,391],{},"apps\u002Fapi\u002Fconvex\u002Flib\u002FsessionOrganization.ts","), es gibt also keine ",[23,394,395],{},"organizationId",", nach der gefiltert werden könnte.",[257,398,400],{"id":399},"semantische-suche","Semantische Suche",[16,402,403,404,406,407,177,410,412,413,416,417,420,421,423,424,427,428,431,432,435,436,439,440,443,444,447,448,451],{},"Echte Vektorsuche über Dateien ist live. Da Convex' Vektor-API nur in Actions verfügbar ist, läuft sie in einer ",[23,405,203],{}," — ",[23,408,409],{},"semanticSearch",[23,411,180],{},". Es handelt sich um ein ",[36,414,415],{},"hybrides"," Retrieval: Die Action bettet den Anfragetext ein (oder nimmt ein vorberechnetes Embedding entgegen) und führt zwei überdimensioniert abgerufene Zweige aus — einen Vektorzweig über ",[23,418,419],{},"ctx.vectorSearch"," auf dem Index ",[23,422,50],{}," und einen Volltextzweig über ",[23,425,426],{},"ftsRankedFileIds"," auf ",[23,429,430],{},"search_files"," — und fusioniert die beiden Rankings anschließend per Reciprocal Rank Fusion (",[23,433,434],{},"lib\u002Frrf.ts","). Die fusionierten IDs werden über ",[23,437,438],{},"getByIds"," zu vollständigen Dateidokumenten hydratisiert (mit Storage-URLs und einem Ähnlichkeits-",[23,441,442],{},"_score","), anhand des erforderlichen Arguments ",[23,445,446],{},"scopeToContact"," zur Datenisolation auf Kontaktebene nachgefiltert und schließlich auf ",[23,449,450],{},"limit"," zugeschnitten:",[183,453,455],{"className":276,"code":454,"language":278,"meta":191,"style":191},"\u002F\u002F semanticFileProcessing.semanticSearch (internalAction) — simplified excerpt:\n\u002F\u002F Over-fetch both legs so the post-fusion contact filter still has survivors.\nconst fetchLimit = Math.min(256, Math.max(limit * 5, 50))\nconst hits = await ctx.vectorSearch('semanticFiles', 'vector_files', {\n  vector, \u002F\u002F query embedding produced in the same action\n  limit: fetchLimit,\n})\nconst ftsRanked = await ctx.runQuery(internal.semanticFiles.ftsRankedFileIds, {\n  queryText,\n  limit: fetchLimit,\n})\n\u002F\u002F Fuse the vector + full-text rankings (scale-agnostic).\nconst fusedIds = reciprocalRankFusion([hits.map((h) => h._id), ftsRanked])\nconst files = await ctx.runQuery(internal.semanticFiles.getByIds, { ids: fusedIds })\n\u002F\u002F Contact-scope AFTER fusion, then slice to `limit`.\n",[23,456,457,463,468,510,539,548,554,560,580,586,591,596,602,634,653],{"__ignoreMap":191},[282,458,459],{"class":284,"line":285},[282,460,462],{"class":461},"sDN9O","\u002F\u002F semanticFileProcessing.semanticSearch (internalAction) — simplified excerpt:\n",[282,464,465],{"class":284,"line":306},[282,466,467],{"class":461},"\u002F\u002F Over-fetch both legs so the post-fusion contact filter still has survivors.\n",[282,469,470,472,475,477,480,483,485,488,491,494,496,499,502,504,507],{"class":284,"line":326},[282,471,289],{"class":288},[282,473,474],{"class":292}," fetchLimit",[282,476,296],{"class":288},[282,478,479],{"class":302}," Math.",[282,481,482],{"class":312},"min",[282,484,316],{"class":302},[282,486,487],{"class":292},"256",[282,489,490],{"class":302},", Math.",[282,492,493],{"class":312},"max",[282,495,373],{"class":302},[282,497,498],{"class":288},"*",[282,500,501],{"class":292}," 5",[282,503,247],{"class":302},[282,505,506],{"class":292},"50",[282,508,509],{"class":302},"))\n",[282,511,512,514,517,519,521,524,527,529,531,533,536],{"class":284,"line":365},[282,513,289],{"class":288},[282,515,516],{"class":292}," hits",[282,518,296],{"class":288},[282,520,299],{"class":288},[282,522,523],{"class":302}," ctx.",[282,525,526],{"class":312},"vectorSearch",[282,528,316],{"class":302},[282,530,320],{"class":319},[282,532,247],{"class":302},[282,534,535],{"class":319},"'vector_files'",[282,537,538],{"class":302},", {\n",[282,540,542,545],{"class":284,"line":541},5,[282,543,544],{"class":302},"  vector, ",[282,546,547],{"class":461},"\u002F\u002F query embedding produced in the same action\n",[282,549,551],{"class":284,"line":550},6,[282,552,553],{"class":302},"  limit: fetchLimit,\n",[282,555,557],{"class":284,"line":556},7,[282,558,559],{"class":302},"})\n",[282,561,563,565,568,570,572,574,577],{"class":284,"line":562},8,[282,564,289],{"class":288},[282,566,567],{"class":292}," ftsRanked",[282,569,296],{"class":288},[282,571,299],{"class":288},[282,573,523],{"class":302},[282,575,576],{"class":312},"runQuery",[282,578,579],{"class":302},"(internal.semanticFiles.ftsRankedFileIds, {\n",[282,581,583],{"class":284,"line":582},9,[282,584,585],{"class":302},"  queryText,\n",[282,587,589],{"class":284,"line":588},10,[282,590,553],{"class":302},[282,592,594],{"class":284,"line":593},11,[282,595,559],{"class":302},[282,597,599],{"class":284,"line":598},12,[282,600,601],{"class":461},"\u002F\u002F Fuse the vector + full-text rankings (scale-agnostic).\n",[282,603,605,607,610,612,615,618,621,624,627,629,631],{"class":284,"line":604},13,[282,606,289],{"class":288},[282,608,609],{"class":292}," fusedIds",[282,611,296],{"class":288},[282,613,614],{"class":312}," reciprocalRankFusion",[282,616,617],{"class":302},"([hits.",[282,619,620],{"class":312},"map",[282,622,623],{"class":302},"((",[282,625,626],{"class":342},"h",[282,628,346],{"class":302},[282,630,349],{"class":288},[282,632,633],{"class":302}," h._id), ftsRanked])\n",[282,635,637,639,642,644,646,648,650],{"class":284,"line":636},14,[282,638,289],{"class":288},[282,640,641],{"class":292}," files",[282,643,296],{"class":288},[282,645,299],{"class":288},[282,647,523],{"class":302},[282,649,576],{"class":312},[282,651,652],{"class":302},"(internal.semanticFiles.getByIds, { ids: fusedIds })\n",[282,654,656],{"class":284,"line":655},15,[282,657,658],{"class":461},"\u002F\u002F Contact-scope AFTER fusion, then slice to `limit`.\n",[128,660,662],{"title":661,"type":131},"Es gibt bewusst keine Query `semanticSearch`",[16,663,664,665,667,668,670,671,675,676,678,679,681,682,177,684,687,688,690],{},"Der alte Stub mit Aktualitäts-Fallback wurde entfernt. ",[23,666,140],{}," trägt jetzt nur noch einen Kommentar, der erklärt, warum es keine ",[23,669,409],{},"-",[672,673,674],"em",{},"Query"," gibt: Ein Query-Kontext kann ",[23,677,419],{}," nicht aufrufen, eine solche Query würde also stillschweigend nach Aktualität sortierte Dateien liefern und dabei vorgeben, semantische Suche zu sein — eine Falle. Die einzige ",[23,680,409],{}," ist die ",[23,683,203],{},[23,685,686],{},"semanticFileProcessing.ts",", die echtes ",[23,689,419],{}," durchführt. Der Agent und jeder echte Aufrufer semantischer Suche gehen über diese Action.",[257,692,694],{"id":693},"kontextuelles-retrieval-für-agenten","Kontextuelles Retrieval für Agenten",[128,696,698],{"title":697,"type":195},"Jetzt angebunden",[16,699,700,701,703,704,105,706,709,710,713,714,717,718,721,722,725,726,729],{},"Die Agenten-Strecke durchsucht ",[23,702,25],{},", wenn sie eine Nachricht bearbeitet. Der Schritt ",[23,705,54],{},[23,707,708],{},"apps\u002Fapi\u002Fconvex\u002Fagent\u002Fsteps\u002Fcontext_retrieval\u002Findex.ts",") bildet eine Anfrage aus eingehendem Betreff + Text und ruft ",[23,711,712],{},"internal.semanticFileProcessing.semanticSearch"," auf (begrenzt auf ",[23,715,716],{},"fileLimit",", standardmäßig 3); die Treffer fließen als Briefing-Abschnitt ",[23,719,720],{},"[RELEVANT FILES]"," neben den Abschnitt ",[23,723,724],{},"[KNOWLEDGE]"," ein, den er aus ",[23,727,728],{},"internal.knowledge.retrieval.semanticSearch"," zieht. Beide Abfragen liegen hinter dem bestehenden Token-Budget des Schritts (Stufen normal \u002F kompaktiert \u002F Notfall).",[16,731,732],{},"Wenn der Agent also eine Nachricht bearbeitet, holt der Kontextschritt relevante Dokumente heran — Verträge, Rechnungen, Angebote —, sodass Antworten auf echten Artefakten (Dateiname, Titel, Zusammenfassung) fußen statt auf nichts. Datei- und Wissens-Retrieval laufen nur, wenn die eingehende Nachricht genug Anfragetext enthält (mehr als etwa 10 Zeichen aus Betreff + Text).",[85,734,736],{"id":735},"automatisches-tagging","Automatisches Tagging",[16,738,739],{},"Jede Datei kann zwei Arten von Tags tragen:",[741,742,743,778],"ul",{},[100,744,745,105,748,751,752,755,756,759,760,762,763,766,767,770,771,774,775,777],{},[36,746,747],{},"Automatische Tags",[23,749,750],{},"autoTags",") — vom LLM-Aufruf ",[23,753,754],{},"summarize"," während der Verarbeitung erzeugt (aus extrahiertem Text und Dateinamen) und anschließend ",[36,757,758],{},"um Kontext-Tags ergänzt",", die aus der Konversation geerbt werden, in der die Datei geteilt wurde. ",[23,761,46],{}," slugifiziert den Thread-Betreff und die Namen der zugehörigen Kontakte (eine Datei, die in einem Thread „Q3 Financials“ mit „Acme Corp“ abgelegt wird, erhält z. B. ",[23,764,765],{},"q3-financials"," und ",[23,768,769],{},"acme-corp",") über ",[23,772,773],{},"slugifyTag"," und führt sie in ",[23,776,750],{}," zusammen.",[100,779,780,105,783,786,787,790],{},[36,781,782],{},"Manuelle Tags",[23,784,785],{},"tags",") — von Nutzern über die Mutation ",[23,788,789],{},"update"," vergeben und unverändert gespeichert.",[16,792,793,794,796,797,800],{},"Auch die Versionsprovenienz landet hier: Löst eine Datei eine frühere Version ab, berechnet ",[23,795,46],{}," aus der Wortzahl-Differenz zum extrahierten Text der Vorgängerversion eine grobe ",[23,798,799],{},"changeSummary"," (z. B. „42 words added vs previous version“).",[128,802,804],{"title":803,"type":229},"Tag-Abgleich ist nicht implementiert",[16,805,806,807,766,810,812],{},"Aus dem Kontext geerbte automatische Tags gibt es heute, aber es gibt weiterhin keine Häufigkeitsverfolgung für Tags und keinen Vorschlag, ähnliche Tags zusammenzuführen (z. B. ",[23,808,809],{},"q3-finances",[23,811,765],{},"). Automatische Tags entstehen pro Datei zum Verarbeitungszeitpunkt; sie werden über den Korpus hinweg weder abgeglichen noch dedupliziert. Ein korpusweiter Tag-Abgleich bzw. Zusammenführungsvorschläge bleiben eine Idee für die Zukunft.",[85,814,816],{"id":815},"versionsverfolgung","Versionsverfolgung",[16,818,819,820,823,824,826,827,830],{},"Dateien werden versionsverknüpft, nicht ersetzt. Jeder Upload kann eine ",[23,821,822],{},"previousVersionId"," referenzieren, aus der ",[23,825,42],{}," die nächste Nummer für ",[23,828,829],{},"version"," ableitet:",[183,832,835],{"className":833,"code":834,"language":188},[186],"Contract v1 (uploaded Feb 10 by Alice, threadId = \"Acme negotiation\")\n  → Contract v2 (uploaded Feb 18 by Bob, same thread, after legal review)\n    → Contract v3 (uploaded Feb 25 by Alice, final signed version)\n",[23,836,834],{"__ignoreMap":191},[16,838,839],{},"Jede Zeile hält fest:",[741,841,842,852,861],{},[100,843,844,847,848,851],{},[36,845,846],{},"Wer"," sie hochgeladen hat (",[23,849,850],{},"uploadedBy",")",[100,853,854,857,858,851],{},[36,855,856],{},"Wo"," — der Konversations-Thread (",[23,859,860],{},"threadId",[100,862,863,866,867,851],{},[36,864,865],{},"Welche Kontakte"," sie betrifft (",[23,868,869],{},"contactIds",[16,871,872,873,875,876,879],{},"Das Feld ",[23,874,822],{}," bildet eine verkettete Liste von Versionen, die ",[23,877,878],{},"getVersionHistory"," durchläuft, um die vollständige Kette in der UI anzuzeigen.",[85,881,883],{"id":882},"schema","Schema",[16,885,886,887,105,890,95],{},"Die eigentliche Tabelle liegt in ",[23,888,889],{},"apps\u002Fapi\u002Fconvex\u002Fschema\u002Fknowledge.ts",[23,891,892],{},"knowledgeTables.semanticFiles",[183,894,896],{"className":276,"code":895,"language":278,"meta":191,"style":191},"semanticFiles: defineTable({\n  storageId: v.id('_storage'),\n  filename: v.string(),\n  mimeType: v.string(),\n  fileSize: v.number(),\n  \u002F\u002F Semantic metadata\n  title: v.optional(v.string()),\n  summary: v.optional(v.string()),\n  extractedText: v.optional(v.string()),\n  tags: v.optional(v.array(v.string())),\n  autoTags: v.optional(v.array(v.string())),\n  \u002F\u002F Provenance\n  sourceType: v.union(\n    v.literal('upload'),\n    v.literal('email_attachment'),\n    v.literal('agent_generated')\n  ),\n  sourceMessageId: v.optional(v.string()),\n  uploadedBy: v.optional(v.string()),\n  \u002F\u002F Why\u002Fwhere this version was shared (JSON-stringified context blob)\n  uploadContext: v.optional(v.string()),\n  \u002F\u002F Relationships\n  contactIds: v.optional(v.array(v.id('contacts'))),\n  threadId: v.optional(v.id('conversationThreads')),\n  \u002F\u002F Versioning\n  version: v.number(),\n  previousVersionId: v.optional(v.id('semanticFiles')),\n  \u002F\u002F Human-readable diff vs the previous version (text files only)\n  changeSummary: v.optional(v.string()),\n  \u002F\u002F Embedding for semantic search\n  embedding: v.array(v.float64()),\n  \u002F\u002F Model that produced `embedding`; re-embed when this changes\n  embeddingModel: v.optional(v.string()),\n  \u002F\u002F When `embedding` was generated; used to schedule re-embedding\n  embeddingGeneratedAt: v.optional(v.number()),\n  \u002F\u002F Full-text search\n  searchableText: v.optional(v.string()),\n  createdAt: v.number(),\n  updatedAt: v.number(),\n})\n  .index('by_created_at', ['createdAt'])\n  .index('by_thread', ['threadId'])\n  .index('by_previous_version', ['previousVersionId'])\n  .searchIndex('search_files', {\n    searchField: 'searchableText',\n    filterFields: [],\n  })\n  .vectorIndex('vector_files', {\n    vectorField: 'embedding',\n    dimensions: 1536,\n    filterFields: [],\n  })\n",[23,897,898,911,927,938,947,957,962,978,991,1004,1023,1040,1045,1056,1071,1084,1098,1104,1118,1132,1138,1152,1158,1182,1202,1208,1218,1236,1242,1256,1262,1277,1283,1297,1303,1317,1323,1337,1347,1357,1362,1384,1403,1422,1436,1447,1453,1459,1473,1484,1495,1500],{"__ignoreMap":191},[282,899,900,903,905,908],{"class":284,"line":285},[282,901,25],{"class":902},"sOLd2",[282,904,59],{"class":302},[282,906,907],{"class":312},"defineTable",[282,909,910],{"class":302},"({\n",[282,912,913,916,919,921,924],{"class":284,"line":306},[282,914,915],{"class":302},"  storageId: v.",[282,917,918],{"class":312},"id",[282,920,316],{"class":302},[282,922,923],{"class":319},"'_storage'",[282,925,926],{"class":302},"),\n",[282,928,929,932,935],{"class":284,"line":326},[282,930,931],{"class":302},"  filename: v.",[282,933,934],{"class":312},"string",[282,936,937],{"class":302},"(),\n",[282,939,940,943,945],{"class":284,"line":365},[282,941,942],{"class":302},"  mimeType: v.",[282,944,934],{"class":312},[282,946,937],{"class":302},[282,948,949,952,955],{"class":284,"line":541},[282,950,951],{"class":302},"  fileSize: v.",[282,953,954],{"class":312},"number",[282,956,937],{"class":302},[282,958,959],{"class":284,"line":550},[282,960,961],{"class":461},"  \u002F\u002F Semantic metadata\n",[282,963,964,967,970,973,975],{"class":284,"line":556},[282,965,966],{"class":302},"  title: v.",[282,968,969],{"class":312},"optional",[282,971,972],{"class":302},"(v.",[282,974,934],{"class":312},[282,976,977],{"class":302},"()),\n",[282,979,980,983,985,987,989],{"class":284,"line":562},[282,981,982],{"class":302},"  summary: v.",[282,984,969],{"class":312},[282,986,972],{"class":302},[282,988,934],{"class":312},[282,990,977],{"class":302},[282,992,993,996,998,1000,1002],{"class":284,"line":582},[282,994,995],{"class":302},"  extractedText: v.",[282,997,969],{"class":312},[282,999,972],{"class":302},[282,1001,934],{"class":312},[282,1003,977],{"class":302},[282,1005,1006,1009,1011,1013,1016,1018,1020],{"class":284,"line":588},[282,1007,1008],{"class":302},"  tags: v.",[282,1010,969],{"class":312},[282,1012,972],{"class":302},[282,1014,1015],{"class":312},"array",[282,1017,972],{"class":302},[282,1019,934],{"class":312},[282,1021,1022],{"class":302},"())),\n",[282,1024,1025,1028,1030,1032,1034,1036,1038],{"class":284,"line":593},[282,1026,1027],{"class":302},"  autoTags: v.",[282,1029,969],{"class":312},[282,1031,972],{"class":302},[282,1033,1015],{"class":312},[282,1035,972],{"class":302},[282,1037,934],{"class":312},[282,1039,1022],{"class":302},[282,1041,1042],{"class":284,"line":598},[282,1043,1044],{"class":461},"  \u002F\u002F Provenance\n",[282,1046,1047,1050,1053],{"class":284,"line":604},[282,1048,1049],{"class":302},"  sourceType: v.",[282,1051,1052],{"class":312},"union",[282,1054,1055],{"class":302},"(\n",[282,1057,1058,1061,1064,1066,1069],{"class":284,"line":636},[282,1059,1060],{"class":302},"    v.",[282,1062,1063],{"class":312},"literal",[282,1065,316],{"class":302},[282,1067,1068],{"class":319},"'upload'",[282,1070,926],{"class":302},[282,1072,1073,1075,1077,1079,1082],{"class":284,"line":655},[282,1074,1060],{"class":302},[282,1076,1063],{"class":312},[282,1078,316],{"class":302},[282,1080,1081],{"class":319},"'email_attachment'",[282,1083,926],{"class":302},[282,1085,1087,1089,1091,1093,1096],{"class":284,"line":1086},16,[282,1088,1060],{"class":302},[282,1090,1063],{"class":312},[282,1092,316],{"class":302},[282,1094,1095],{"class":319},"'agent_generated'",[282,1097,323],{"class":302},[282,1099,1101],{"class":284,"line":1100},17,[282,1102,1103],{"class":302},"  ),\n",[282,1105,1107,1110,1112,1114,1116],{"class":284,"line":1106},18,[282,1108,1109],{"class":302},"  sourceMessageId: v.",[282,1111,969],{"class":312},[282,1113,972],{"class":302},[282,1115,934],{"class":312},[282,1117,977],{"class":302},[282,1119,1121,1124,1126,1128,1130],{"class":284,"line":1120},19,[282,1122,1123],{"class":302},"  uploadedBy: v.",[282,1125,969],{"class":312},[282,1127,972],{"class":302},[282,1129,934],{"class":312},[282,1131,977],{"class":302},[282,1133,1135],{"class":284,"line":1134},20,[282,1136,1137],{"class":461},"  \u002F\u002F Why\u002Fwhere this version was shared (JSON-stringified context blob)\n",[282,1139,1141,1144,1146,1148,1150],{"class":284,"line":1140},21,[282,1142,1143],{"class":302},"  uploadContext: v.",[282,1145,969],{"class":312},[282,1147,972],{"class":302},[282,1149,934],{"class":312},[282,1151,977],{"class":302},[282,1153,1155],{"class":284,"line":1154},22,[282,1156,1157],{"class":461},"  \u002F\u002F Relationships\n",[282,1159,1161,1164,1166,1168,1170,1172,1174,1176,1179],{"class":284,"line":1160},23,[282,1162,1163],{"class":302},"  contactIds: v.",[282,1165,969],{"class":312},[282,1167,972],{"class":302},[282,1169,1015],{"class":312},[282,1171,972],{"class":302},[282,1173,918],{"class":312},[282,1175,316],{"class":302},[282,1177,1178],{"class":319},"'contacts'",[282,1180,1181],{"class":302},"))),\n",[282,1183,1185,1188,1190,1192,1194,1196,1199],{"class":284,"line":1184},24,[282,1186,1187],{"class":302},"  threadId: v.",[282,1189,969],{"class":312},[282,1191,972],{"class":302},[282,1193,918],{"class":312},[282,1195,316],{"class":302},[282,1197,1198],{"class":319},"'conversationThreads'",[282,1200,1201],{"class":302},")),\n",[282,1203,1205],{"class":284,"line":1204},25,[282,1206,1207],{"class":461},"  \u002F\u002F Versioning\n",[282,1209,1211,1214,1216],{"class":284,"line":1210},26,[282,1212,1213],{"class":302},"  version: v.",[282,1215,954],{"class":312},[282,1217,937],{"class":302},[282,1219,1221,1224,1226,1228,1230,1232,1234],{"class":284,"line":1220},27,[282,1222,1223],{"class":302},"  previousVersionId: v.",[282,1225,969],{"class":312},[282,1227,972],{"class":302},[282,1229,918],{"class":312},[282,1231,316],{"class":302},[282,1233,320],{"class":319},[282,1235,1201],{"class":302},[282,1237,1239],{"class":284,"line":1238},28,[282,1240,1241],{"class":461},"  \u002F\u002F Human-readable diff vs the previous version (text files only)\n",[282,1243,1245,1248,1250,1252,1254],{"class":284,"line":1244},29,[282,1246,1247],{"class":302},"  changeSummary: v.",[282,1249,969],{"class":312},[282,1251,972],{"class":302},[282,1253,934],{"class":312},[282,1255,977],{"class":302},[282,1257,1259],{"class":284,"line":1258},30,[282,1260,1261],{"class":461},"  \u002F\u002F Embedding for semantic search\n",[282,1263,1265,1268,1270,1272,1275],{"class":284,"line":1264},31,[282,1266,1267],{"class":302},"  embedding: v.",[282,1269,1015],{"class":312},[282,1271,972],{"class":302},[282,1273,1274],{"class":312},"float64",[282,1276,977],{"class":302},[282,1278,1280],{"class":284,"line":1279},32,[282,1281,1282],{"class":461},"  \u002F\u002F Model that produced `embedding`; re-embed when this changes\n",[282,1284,1286,1289,1291,1293,1295],{"class":284,"line":1285},33,[282,1287,1288],{"class":302},"  embeddingModel: v.",[282,1290,969],{"class":312},[282,1292,972],{"class":302},[282,1294,934],{"class":312},[282,1296,977],{"class":302},[282,1298,1300],{"class":284,"line":1299},34,[282,1301,1302],{"class":461},"  \u002F\u002F When `embedding` was generated; used to schedule re-embedding\n",[282,1304,1306,1309,1311,1313,1315],{"class":284,"line":1305},35,[282,1307,1308],{"class":302},"  embeddingGeneratedAt: v.",[282,1310,969],{"class":312},[282,1312,972],{"class":302},[282,1314,954],{"class":312},[282,1316,977],{"class":302},[282,1318,1320],{"class":284,"line":1319},36,[282,1321,1322],{"class":461},"  \u002F\u002F Full-text search\n",[282,1324,1326,1329,1331,1333,1335],{"class":284,"line":1325},37,[282,1327,1328],{"class":302},"  searchableText: v.",[282,1330,969],{"class":312},[282,1332,972],{"class":302},[282,1334,934],{"class":312},[282,1336,977],{"class":302},[282,1338,1340,1343,1345],{"class":284,"line":1339},38,[282,1341,1342],{"class":302},"  createdAt: v.",[282,1344,954],{"class":312},[282,1346,937],{"class":302},[282,1348,1350,1353,1355],{"class":284,"line":1349},39,[282,1351,1352],{"class":302},"  updatedAt: v.",[282,1354,954],{"class":312},[282,1356,937],{"class":302},[282,1358,1360],{"class":284,"line":1359},40,[282,1361,559],{"class":302},[282,1363,1365,1367,1370,1372,1375,1378,1381],{"class":284,"line":1364},41,[282,1366,309],{"class":302},[282,1368,1369],{"class":312},"index",[282,1371,316],{"class":302},[282,1373,1374],{"class":319},"'by_created_at'",[282,1376,1377],{"class":302},", [",[282,1379,1380],{"class":319},"'createdAt'",[282,1382,1383],{"class":302},"])\n",[282,1385,1387,1389,1391,1393,1396,1398,1401],{"class":284,"line":1386},42,[282,1388,309],{"class":302},[282,1390,1369],{"class":312},[282,1392,316],{"class":302},[282,1394,1395],{"class":319},"'by_thread'",[282,1397,1377],{"class":302},[282,1399,1400],{"class":319},"'threadId'",[282,1402,1383],{"class":302},[282,1404,1406,1408,1410,1412,1415,1417,1420],{"class":284,"line":1405},43,[282,1407,309],{"class":302},[282,1409,1369],{"class":312},[282,1411,316],{"class":302},[282,1413,1414],{"class":319},"'by_previous_version'",[282,1416,1377],{"class":302},[282,1418,1419],{"class":319},"'previousVersionId'",[282,1421,1383],{"class":302},[282,1423,1425,1427,1430,1432,1434],{"class":284,"line":1424},44,[282,1426,309],{"class":302},[282,1428,1429],{"class":312},"searchIndex",[282,1431,316],{"class":302},[282,1433,336],{"class":319},[282,1435,538],{"class":302},[282,1437,1439,1442,1444],{"class":284,"line":1438},45,[282,1440,1441],{"class":302},"    searchField: ",[282,1443,359],{"class":319},[282,1445,1446],{"class":302},",\n",[282,1448,1450],{"class":284,"line":1449},46,[282,1451,1452],{"class":302},"    filterFields: [],\n",[282,1454,1456],{"class":284,"line":1455},47,[282,1457,1458],{"class":302},"  })\n",[282,1460,1462,1464,1467,1469,1471],{"class":284,"line":1461},48,[282,1463,309],{"class":302},[282,1465,1466],{"class":312},"vectorIndex",[282,1468,316],{"class":302},[282,1470,535],{"class":319},[282,1472,538],{"class":302},[282,1474,1476,1479,1482],{"class":284,"line":1475},49,[282,1477,1478],{"class":302},"    vectorField: ",[282,1480,1481],{"class":319},"'embedding'",[282,1483,1446],{"class":302},[282,1485,1487,1490,1493],{"class":284,"line":1486},50,[282,1488,1489],{"class":302},"    dimensions: ",[282,1491,1492],{"class":292},"1536",[282,1494,1446],{"class":302},[282,1496,1498],{"class":284,"line":1497},51,[282,1499,1452],{"class":302},[282,1501,1503],{"class":284,"line":1502},52,[282,1504,1458],{"class":302},[16,1506,1507,1508,1510,1511,766,1513,1515,1516,1519,1520,1523],{},"Ein Feld ",[23,1509,395],{}," gibt es nicht — Owlat betreibt eine Organisation pro Deployment, also filtert keiner der beiden Indizes nach Organisation. ",[23,1512,221],{},[23,1514,224],{}," halten fest, welches Modell den Vektor erzeugt hat (",[23,1517,1518],{},"text-embedding-3-small",", die Konstante ",[23,1521,1522],{},"CURRENT_EMBEDDING_MODEL","), damit veraltete Embeddings bei einem Modellwechsel neu erzeugt werden können.",[85,1525,1527],{"id":1526},"integration-mit-bestehenden-systemen","Integration mit bestehenden Systemen",[16,1529,1530,1531,105,1534,1537,1538,1541],{},"Das semantische Dateisystem ersetzt nicht die bestehende Tabelle ",[23,1532,1533],{},"mediaAssets",[23,1535,1536],{},"apps\u002Fapi\u002Fconvex\u002Fschema\u002Ftemplates.ts","). Medien-Assets sind eigens für den E-Mail-Builder gebaut (Bilder, mit Breite\u002FHöhe, Suche, Tagging). Semantische Dateien sind eine breitere Schicht für organisatorische Dokumente. Beide nutzen Convex ",[23,1539,1540],{},"_storage"," für Binärdaten.",[16,1543,1544,1545,1547,1548,1550,1551,1553],{},"Dateien im Kontext des Agenten sichtbar zu machen, funktioniert inzwischen (der Schritt ",[23,1546,54],{}," fragt ",[23,1549,50],{}," ab). Eine vereinheitlichte Sicht — Medien-Assets und semantische Dateien über eine einzige Query zu durchsuchen — bleibt das längerfristige Ziel: Heute sind die beiden Schichten getrennt. Die verbleibenden übergreifenden Teile sind die automatische Aufnahme agentengenerierter Dateien (der Einstiegspunkt ",[23,1552,82],{}," akzeptiert den Quellentyp, aber noch emittiert kein Agenten-Flow Dateien — eingehende E-Mail-Anhänge werden bereits automatisch aufgenommen), echte Extraktion für Nicht-PDF-Binärdateien (Word\u002FExcel\u002FBilder) sowie ein korpusweiter Tag-Abgleich.",[128,1555,1557],{"title":1556,"type":131},"Dateien sind kein eigenes Produkt",[16,1558,1559,1560,1563,1564,1569,1570,1574],{},"Das Dateisystem ist eine Schicht innerhalb derselben Architektur: dasselbe Deployment, dasselbe Berechtigungsmodell (Schreibzugriffe laufen über ",[23,1561,1562],{},"requireAdminContext",") und derselbe Convex-Storage. Siehe den ",[1565,1566,1568],"a",{"href":1567},"\u002Fguide\u002Ffiles","Dateien-Leitfaden"," für die Nutzersicht und die Vision zum ",[1565,1571,1573],{"href":1572},"\u002Fvision\u002Fknowledge-graph","Wissensgraph"," für die verwandte Schicht typisierten Wissens.",[1576,1577,1578],"style",{},"html pre.shiki code .s7YZ4, html code.shiki .s7YZ4{--shiki-default:#D73A49;--shiki-dark:#F47067}html pre.shiki code .sviXB, html code.shiki .sviXB{--shiki-default:#005CC5;--shiki-dark:#6CB6FF}html pre.shiki code .sYgZi, html code.shiki .sYgZi{--shiki-default:#24292E;--shiki-dark:#ADBAC7}html pre.shiki code .sPO5f, html code.shiki .sPO5f{--shiki-default:#6F42C1;--shiki-dark:#DCBDFB}html pre.shiki code .s-HuK, html code.shiki .s-HuK{--shiki-default:#032F62;--shiki-dark:#96D0FF}html pre.shiki code .stnAF, html code.shiki .stnAF{--shiki-default:#E36209;--shiki-dark:#F69D50}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sDN9O, html code.shiki .sDN9O{--shiki-default:#6A737D;--shiki-dark:#768390}html pre.shiki code .sOLd2, html code.shiki .sOLd2{--shiki-default:#6F42C1;--shiki-dark:#F69D50}",{"title":191,"searchDepth":306,"depth":306,"links":1580},[1581,1582,1587,1588,1589,1590],{"id":87,"depth":306,"text":88},{"id":254,"depth":306,"text":255,"children":1583},[1584,1585,1586],{"id":259,"depth":326,"text":260},{"id":399,"depth":326,"text":400},{"id":693,"depth":326,"text":694},{"id":735,"depth":306,"text":736},{"id":815,"depth":306,"text":816},{"id":882,"depth":306,"text":883},{"id":1526,"depth":306,"text":1527},"Technische Architektur der semantischen Dateiablage von Owlat — heute Versionsverfolgung mit Provenienz, dazu die geplante Schicht für Embedding-basiertes Retrieval und automatisches Tagging.","md",{},true,"\u002Fvision\u002Ffile-system",{"title":6,"description":1591},"5.vision\u002F5.file-system","Q4WnsjB6oraGEErj1phcnDjm2BnnKc2cK8AgJSC8-yQ",[1600,1604],{"title":1601,"path":1602,"stem":1603,"children":-1},"Multi-Channel & CRM","\u002Fvision\u002Fmulti-channel","5.vision\u002F4.multi-channel",{"title":1605,"path":1606,"stem":1607,"children":-1},"Desktop-App & fortgeschrittene Agenten","\u002Fvision\u002Fdesktop-app","5.vision\u002F6.desktop-app",1786915111359]