[{"data":1,"prerenderedAt":2435},["ShallowReactive",2],{"search-de":3,"content-de-vision\u002Fmulti-channel":4,"surround-de-\u002Fvision\u002Fmulti-channel":2426},[],{"id":5,"title":6,"body":7,"description":2419,"extension":2420,"meta":2421,"navigation":125,"path":2422,"seo":2423,"stem":2424,"__hash__":2425},"content_de\u002F5.vision\u002F4.multi-channel.md","Multi-Channel & CRM",{"type":8,"value":9,"toc":2404},"minimark",[10,15,25,60,65,75,531,550,566,571,676,710,726,763,767,786,1214,1226,1230,1233,1498,1505,1557,1561,1617,1639,1642,1658,1661,1682,1686,1689,1697,1752,1930,1934,1941,1945,1948,2072,2078,2123,2127,2141,2269,2275,2289,2296,2322,2326,2329,2343,2346,2400],[11,12,14],"h1",{"id":13},"architektur-von-multi-channel-crm","Architektur von Multi-Channel & CRM",[16,17,18,19,24],"p",{},"Owlat beginnt mit E-Mail. Der ",[20,21,23],"a",{"href":22},"\u002Fvision#the-communication-hub","Kommunikations-Hub"," ist jedoch kanalunabhängig — jede Nachricht wird zu einem strukturierten Ereignis in derselben Strecke, ganz gleich, woher sie stammt. Diese Seite behandelt die technische Architektur für die Multi-Channel-Unterstützung und den CRM-Hub, der Kontakte über alle Kanäle hinweg vereinheitlicht.",[16,26,27,28,32,33,36,37,40,41,36,44,47,48,51,52,55,56,59],{},"Große Teile dieser Architektur sind bereits implementiert: die ausgehenden Kanaladapter (",[29,30,31],"code",{},"apps\u002Fapi\u002Fconvex\u002Fchannels\u002Fadapters\u002F","), die Tabellen ",[29,34,35],{},"unifiedMessages"," und ",[29,38,39],{},"channelConfigs",", die CRM-Tabellen ",[29,42,43],{},"contactIdentities",[29,45,46],{},"contactRelationships"," (",[29,49,50],{},"apps\u002Fapi\u002Fconvex\u002Fschema\u002Fmessaging.ts",", ",[29,53,54],{},"apps\u002Fapi\u002Fconvex\u002Fschema\u002Fcontacts.ts","), die aktiven eingehenden Webhook-Routen, die inzwischen die Agenten-Strecke speisen, AES-256-GCM-Verschlüsselung im Ruhezustand für Kanal-Zugangsdaten, ein Cron zum automatischen Zusammenführen von Kontakten bei exaktem Bezeichner, ein 5-Minuten-Cron für Kanal-Health-Checks (",[29,57,58],{},"apps\u002Fapi\u002Fconvex\u002Fcrons.ts",") und ein ausgehender Versandpfad, der Zugangsdaten entschlüsselt und die Adapter für SMS\u002FWhatsApp\u002Fgenerisch aufruft. Was folgt, ist daher das ausgelieferte Rückgrat plus die Stellen, an denen die Vision dem Code noch vorauseilt: Der ausgehende Versand ist an den Antwortpfad des KI-Agenten und den manuellen Admin-Composer angeschlossen, bleibt aber untätig, solange keine Anbieter-Zugangsdaten existieren; die automatische Beziehungsextraktion hat noch keinen Aufrufer; und das gesundheitsgesteuerte Einreihen ausgehender Nachrichten steht weiterhin auf der Roadmap.",[61,62,64],"h2",{"id":63},"schnittstelle-der-kanaladapter","Schnittstelle der Kanaladapter",[16,66,67,68,70,71,74],{},"Kanaladapter sind einsteckbare TypeScript-Klassen, die verschiedene Kommunikationskanäle in ein einheitliches Nachrichtenformat normalisieren. Sie liegen in ",[29,69,31],{}," (die Schnittstelle ist ",[29,72,73],{},"types.ts",") und laufen innerhalb von Convex — keine separaten Dienste, keine zusätzliche Infrastruktur.",[76,77,82],"pre",{"className":78,"code":79,"language":80,"meta":81,"style":81},"language-typescript shiki shiki-themes github-light github-dark-dimmed","\u002F\u002F The channel discriminator is declared once, by the validators the schema and\n\u002F\u002F every Convex function argument already use (`lib\u002FconvexValidators.ts`), and\n\u002F\u002F derived everywhere else — including the web config form's channel type:\n\u002F\u002F   type UnifiedMessageChannel = 'email' | 'sms' | 'whatsapp' | 'generic' | 'chat'\n\u002F\u002F   type OutboundChannel       = 'sms' | 'whatsapp' | 'generic'   \u002F\u002F has an adapter\n\ninterface ChannelAdapter {\n  \u002F**\n   * Which channel this adapter dispatches — `OutboundChannel`, the\n   * dispatchable subset ('sms' | 'whatsapp' | 'generic'), so an adapter\n   * claiming `email` or `chat` is a compile error rather than a convention.\n   *\u002F\n  id: OutboundChannel\n\n  \u002F** Send a message through this channel *\u002F\n  send(message: OutboundMessage): Promise\u003CSendResult>\n\n  \u002F** Check delivery status of a sent message *\u002F\n  getDeliveryStatus(externalId: string): Promise\u003CDeliveryStatus>\n\n  \u002F** Report current connection health *\u002F\n  healthCheck(): Promise\u003CChannelHealth>\n}\n\n\u002F\u002F Exactly the two things `updateChannelHealth` persists. The shape once also\n\u002F\u002F declared `lastSuccessfulSend`, `rateLimitRemaining` and `latencyMs`: no\n\u002F\u002F adapter ever set the first two, and the third was measured on every probe\n\u002F\u002F and then discarded, so D10 dropped all three. Persisting probe latency is a\n\u002F\u002F schema change, not a member.\ninterface ChannelHealth {\n  status: 'healthy' | 'degraded' | 'down'\n  lastError?: string\n}\n\ninterface OutboundMessage {\n  contactId: string\n  channel: UnifiedMessageChannel   \u002F\u002F all five — dispatch fails safe on the two with no adapter\n  content: {\n    text?: string\n    html?: string\n    subject?: string        \u002F\u002F email only\n    mediaUrl?: string       \u002F\u002F SMS\u002FWhatsApp\n  }\n  threadId?: string\n  metadata?: Record\u003Cstring, string>\n}\n","typescript","",[29,83,84,93,99,105,111,120,127,142,148,154,160,166,172,185,190,196,231,236,242,272,277,283,303,309,314,320,326,332,338,344,354,377,389,394,399,408,418,432,442,452,462,475,488,494,504,526],{"__ignoreMap":81},[85,86,89],"span",{"class":87,"line":88},"line",1,[85,90,92],{"class":91},"sDN9O","\u002F\u002F The channel discriminator is declared once, by the validators the schema and\n",[85,94,96],{"class":87,"line":95},2,[85,97,98],{"class":91},"\u002F\u002F every Convex function argument already use (`lib\u002FconvexValidators.ts`), and\n",[85,100,102],{"class":87,"line":101},3,[85,103,104],{"class":91},"\u002F\u002F derived everywhere else — including the web config form's channel type:\n",[85,106,108],{"class":87,"line":107},4,[85,109,110],{"class":91},"\u002F\u002F   type UnifiedMessageChannel = 'email' | 'sms' | 'whatsapp' | 'generic' | 'chat'\n",[85,112,114,117],{"class":87,"line":113},5,[85,115,116],{"class":91},"\u002F\u002F   type OutboundChannel       = 'sms' | 'whatsapp' | 'generic'",[85,118,119],{"class":91},"   \u002F\u002F has an adapter\n",[85,121,123],{"class":87,"line":122},6,[85,124,126],{"emptyLinePlaceholder":125},true,"\n",[85,128,130,134,138],{"class":87,"line":129},7,[85,131,133],{"class":132},"s7YZ4","interface",[85,135,137],{"class":136},"sOLd2"," ChannelAdapter",[85,139,141],{"class":140},"sYgZi"," {\n",[85,143,145],{"class":87,"line":144},8,[85,146,147],{"class":91},"  \u002F**\n",[85,149,151],{"class":87,"line":150},9,[85,152,153],{"class":91},"   * Which channel this adapter dispatches — `OutboundChannel`, the\n",[85,155,157],{"class":87,"line":156},10,[85,158,159],{"class":91},"   * dispatchable subset ('sms' | 'whatsapp' | 'generic'), so an adapter\n",[85,161,163],{"class":87,"line":162},11,[85,164,165],{"class":91},"   * claiming `email` or `chat` is a compile error rather than a convention.\n",[85,167,169],{"class":87,"line":168},12,[85,170,171],{"class":91},"   *\u002F\n",[85,173,175,179,182],{"class":87,"line":174},13,[85,176,178],{"class":177},"stnAF","  id",[85,180,181],{"class":132},":",[85,183,184],{"class":136}," OutboundChannel\n",[85,186,188],{"class":87,"line":187},14,[85,189,126],{"emptyLinePlaceholder":125},[85,191,193],{"class":87,"line":192},15,[85,194,195],{"class":91},"  \u002F** Send a message through this channel *\u002F\n",[85,197,199,203,206,209,211,214,217,219,222,225,228],{"class":87,"line":198},16,[85,200,202],{"class":201},"sPO5f","  send",[85,204,205],{"class":140},"(",[85,207,208],{"class":177},"message",[85,210,181],{"class":132},[85,212,213],{"class":136}," OutboundMessage",[85,215,216],{"class":140},")",[85,218,181],{"class":132},[85,220,221],{"class":136}," Promise",[85,223,224],{"class":140},"\u003C",[85,226,227],{"class":136},"SendResult",[85,229,230],{"class":140},">\n",[85,232,234],{"class":87,"line":233},17,[85,235,126],{"emptyLinePlaceholder":125},[85,237,239],{"class":87,"line":238},18,[85,240,241],{"class":91},"  \u002F** Check delivery status of a sent message *\u002F\n",[85,243,245,248,250,253,255,259,261,263,265,267,270],{"class":87,"line":244},19,[85,246,247],{"class":201},"  getDeliveryStatus",[85,249,205],{"class":140},[85,251,252],{"class":177},"externalId",[85,254,181],{"class":132},[85,256,258],{"class":257},"sviXB"," string",[85,260,216],{"class":140},[85,262,181],{"class":132},[85,264,221],{"class":136},[85,266,224],{"class":140},[85,268,269],{"class":136},"DeliveryStatus",[85,271,230],{"class":140},[85,273,275],{"class":87,"line":274},20,[85,276,126],{"emptyLinePlaceholder":125},[85,278,280],{"class":87,"line":279},21,[85,281,282],{"class":91},"  \u002F** Report current connection health *\u002F\n",[85,284,286,289,292,294,296,298,301],{"class":87,"line":285},22,[85,287,288],{"class":201},"  healthCheck",[85,290,291],{"class":140},"()",[85,293,181],{"class":132},[85,295,221],{"class":136},[85,297,224],{"class":140},[85,299,300],{"class":136},"ChannelHealth",[85,302,230],{"class":140},[85,304,306],{"class":87,"line":305},23,[85,307,308],{"class":140},"}\n",[85,310,312],{"class":87,"line":311},24,[85,313,126],{"emptyLinePlaceholder":125},[85,315,317],{"class":87,"line":316},25,[85,318,319],{"class":91},"\u002F\u002F Exactly the two things `updateChannelHealth` persists. The shape once also\n",[85,321,323],{"class":87,"line":322},26,[85,324,325],{"class":91},"\u002F\u002F declared `lastSuccessfulSend`, `rateLimitRemaining` and `latencyMs`: no\n",[85,327,329],{"class":87,"line":328},27,[85,330,331],{"class":91},"\u002F\u002F adapter ever set the first two, and the third was measured on every probe\n",[85,333,335],{"class":87,"line":334},28,[85,336,337],{"class":91},"\u002F\u002F and then discarded, so D10 dropped all three. Persisting probe latency is a\n",[85,339,341],{"class":87,"line":340},29,[85,342,343],{"class":91},"\u002F\u002F schema change, not a member.\n",[85,345,347,349,352],{"class":87,"line":346},30,[85,348,133],{"class":132},[85,350,351],{"class":136}," ChannelHealth",[85,353,141],{"class":140},[85,355,357,360,362,366,369,372,374],{"class":87,"line":356},31,[85,358,359],{"class":177},"  status",[85,361,181],{"class":132},[85,363,365],{"class":364},"s-HuK"," 'healthy'",[85,367,368],{"class":132}," |",[85,370,371],{"class":364}," 'degraded'",[85,373,368],{"class":132},[85,375,376],{"class":364}," 'down'\n",[85,378,380,383,386],{"class":87,"line":379},32,[85,381,382],{"class":177},"  lastError",[85,384,385],{"class":132},"?:",[85,387,388],{"class":257}," string\n",[85,390,392],{"class":87,"line":391},33,[85,393,308],{"class":140},[85,395,397],{"class":87,"line":396},34,[85,398,126],{"emptyLinePlaceholder":125},[85,400,402,404,406],{"class":87,"line":401},35,[85,403,133],{"class":132},[85,405,213],{"class":136},[85,407,141],{"class":140},[85,409,411,414,416],{"class":87,"line":410},36,[85,412,413],{"class":177},"  contactId",[85,415,181],{"class":132},[85,417,388],{"class":257},[85,419,421,424,426,429],{"class":87,"line":420},37,[85,422,423],{"class":177},"  channel",[85,425,181],{"class":132},[85,427,428],{"class":136}," UnifiedMessageChannel",[85,430,431],{"class":91},"   \u002F\u002F all five — dispatch fails safe on the two with no adapter\n",[85,433,435,438,440],{"class":87,"line":434},38,[85,436,437],{"class":177},"  content",[85,439,181],{"class":132},[85,441,141],{"class":140},[85,443,445,448,450],{"class":87,"line":444},39,[85,446,447],{"class":177},"    text",[85,449,385],{"class":132},[85,451,388],{"class":257},[85,453,455,458,460],{"class":87,"line":454},40,[85,456,457],{"class":177},"    html",[85,459,385],{"class":132},[85,461,388],{"class":257},[85,463,465,468,470,472],{"class":87,"line":464},41,[85,466,467],{"class":177},"    subject",[85,469,385],{"class":132},[85,471,258],{"class":257},[85,473,474],{"class":91},"        \u002F\u002F email only\n",[85,476,478,481,483,485],{"class":87,"line":477},42,[85,479,480],{"class":177},"    mediaUrl",[85,482,385],{"class":132},[85,484,258],{"class":257},[85,486,487],{"class":91},"       \u002F\u002F SMS\u002FWhatsApp\n",[85,489,491],{"class":87,"line":490},43,[85,492,493],{"class":140},"  }\n",[85,495,497,500,502],{"class":87,"line":496},44,[85,498,499],{"class":177},"  threadId",[85,501,385],{"class":132},[85,503,388],{"class":257},[85,505,507,510,512,515,517,520,522,524],{"class":87,"line":506},45,[85,508,509],{"class":177},"  metadata",[85,511,385],{"class":132},[85,513,514],{"class":136}," Record",[85,516,224],{"class":140},[85,518,519],{"class":257},"string",[85,521,51],{"class":140},[85,523,519],{"class":257},[85,525,230],{"class":140},[85,527,529],{"class":87,"line":528},46,[85,530,308],{"class":140},[16,532,533,537,538,541,542,545,546,549],{},[534,535,536],"strong",{},"Der Vertrag ist bewusst rein ausgehend."," Ein Adapter verantwortet Versand, Zustellstatus und Verbindungsgesundheit — genau das, was die Versand-Action, der Cron für den Zustellstatus und der Health-Cron aufrufen. Er verantwortet nichts Eingehendes: Signaturprüfung und Payload-Normalisierung leben in ",[29,539,540],{},"apps\u002Fapi\u002Fconvex\u002Fwebhooks\u002Fadapters\u002F{twilio,meta,generic}.ts",", auf der echten HTTP-Route, gegen echte Secrets. Die Schnittstelle deklarierte einst zusätzlich ein ",[29,543,544],{},"parseInbound"," und ein ",[29,547,548],{},"validateSignature"," — eine zweite Kopie von Regeln, welche die Webhook-Adapter bereits verantworteten, ohne Aufrufer und mit einer Drift, die der Compiler nicht sehen konnte —, weshalb der Ehrlichkeits-Durchgang D10 das Paar gelöscht hat, statt zwei Antworten auf eine Frage zu behalten.",[16,551,552,553,556,557,36,561,565],{},"Einen neuen Kanal hinzuzufügen bedeutet daher zweierlei, nicht einerlei: diese Schnittstelle für die ausgehende Hälfte implementieren und für die eingehende Hälfte ein Modul unter ",[29,554,555],{},"webhooks\u002Fadapters\u002F"," samt zugehöriger Route ergänzen — dasselbe Muster wie bei den bestehenden Webhooks von ",[20,558,560],{"href":559},"\u002Fdeveloper\u002Fconvex","Resend",[20,562,564],{"href":563},"\u002Fdeveloper\u002Fmta-system","MTA",".",[567,568,570],"h3",{"id":569},"eingebaute-adapter","Eingebaute Adapter",[572,573,574,593],"table",{},[575,576,577],"thead",{},[578,579,580,584,587,590],"tr",{},[581,582,583],"th",{},"Adapter",[581,585,586],{},"Ausgehend",[581,588,589],{},"Eingehend",[581,591,592],{},"Anbieter",[594,595,596,613,629,644,660],"tbody",{},[578,597,598,604,607,610],{},[599,600,601],"td",{},[534,602,603],{},"E-Mail",[599,605,606],{},"MTA \u002F SES \u002F Resend (bestehend)",[599,608,609],{},"Eingehendes SMTP über den MTA",[599,611,612],{},"Selbst gehostet oder Cloud",[578,614,615,620,623,626],{},[599,616,617],{},[534,618,619],{},"SMS",[599,621,622],{},"Twilio-API",[599,624,625],{},"Webhook vom Anbieter",[599,627,628],{},"Cloud-API",[578,630,631,636,639,642],{},[599,632,633],{},[534,634,635],{},"WhatsApp",[599,637,638],{},"WhatsApp Business API",[599,640,641],{},"Webhook von Meta",[599,643,628],{},[578,645,646,651,654,657],{},[599,647,648],{},[534,649,650],{},"Generisch",[599,652,653],{},"HTTP POST an eine externe URL",[599,655,656],{},"HTTP POST mit gemeinsamem Secret von einem externen System",[599,658,659],{},"Beliebig",[578,661,662,667,670,673],{},[599,663,664],{},[534,665,666],{},"Chat",[599,668,669],{},"Convex-Echtzeit (nativ)",[599,671,672],{},"Convex-Mutation (nativ)",[599,674,675],{},"Eingebaut",[16,677,678,681,682,685,686,689,690,693,694,697,698,701,702,705,706,709],{},[534,679,680],{},"Nur drei der fünf Zeilen sind Adapterklassen."," E-Mail und Chat sind Kanäle des einheitlichen Nachrichtenmodells, keine Kanalanbieter: Ausgehende E-Mail läuft über den Send-Worker (",[29,683,684],{},"apps\u002Fapi\u002Fconvex\u002Fdelivery\u002Fworker.ts",") zu einem konfigurierten Versandanbieter (MTA \u002F SES \u002F Resend \u002F Mandrill) via ",[29,687,688],{},"apps\u002Fapi\u002Fconvex\u002Flib\u002FsendProviders\u002F",", und Chat ist nativ — ",[29,691,692],{},"sendChatMessage"," schreibt direkt in Convex-Tabellen, mit Echtzeit-Aktualisierung über Subscriptions. Beide hatten einst eine Platzhalterklasse, welche die obige Schnittstelle implementierte; jede täuschte ihre Antworten vor (ein ",[29,695,696],{},"send()",", das fest einen Fehlschlag oder eine erfundene ID zurückgab, ein ",[29,699,700],{},"healthCheck()",", das fest „healthy“ zurückgab, ein ",[29,703,704],{},"validateSignature()",", das fest ",[29,707,708],{},"true"," zurückgab), und keine hatte einen Aufrufer — deshalb wurden beide gelöscht, statt sie als Attrappe zu behalten.",[16,711,712,713,715,716,47,719,722,723,725],{},"Die Adapter für SMS, WhatsApp und generisch sind echte Sende-Clients für Twilio\u002FMeta\u002FHTTP (",[29,714,31],{},"), und eine Convex-Versand-Action steuert sie an: ",[29,717,718],{},"dispatchOutbound",[29,720,721],{},"apps\u002Fapi\u002Fconvex\u002Fchannels\u002Foutbound.ts",") entschlüsselt die Zugangsdaten des Kanals, instanziiert den passenden Adapter, sendet und hält eine ausgehende ",[29,724,35],{},"-Zeile fest (siehe Kasten unten).",[727,728,731],"callout",{"title":729,"type":730},"Der ausgehende Versand ist verdrahtet, bleibt aber ohne Zugangsdaten untätig","warning",[16,732,733,734,737,738,741,742,745,746,741,749,751,752,754,755,758,759,762],{},"Eingehend ist für SMS, WhatsApp und den generischen Kanal aktiv (die Webhook-Routen weiter unten). Der ",[534,735,736],{},"Versandpfad"," für ausgehende Nachrichten ist gebaut und hat heute zwei aktive Aufrufer — den Antwortpfad des KI-Agenten (",[29,739,740],{},"sendApprovedReply"," in ",[29,743,744],{},"apps\u002Fapi\u002Fconvex\u002Fagent\u002FagentPipeline.ts",") und den manuellen Admin-Composer (",[29,747,748],{},"sendChannelMessage",[29,750,721],{},"). Beide planen ",[29,753,718],{}," ein, das die Zugangsdaten entschlüsselt, den Adapter aufruft und das Ergebnis mit einem ausfallsicheren ",[29,756,757],{},"failed","-Zeilen-Vertrag festhält, der niemals wirft. Er bleibt nur so lange ein No-op (mit dem Vermerk ",[29,760,761],{},"Channel not configured","), bis eine Betreiberin oder ein Betreiber Anbieter-Zugangsdaten konfiguriert. E-Mail und Chat versenden heute schon echt; SMS\u002FWhatsApp\u002Fgenerisch sind nur einen Satz Anbieter-Zugangsdaten entfernt.",[61,764,766],{"id":765},"einheitliches-nachrichtenmodell","Einheitliches Nachrichtenmodell",[16,768,769,770,772,773,777,778,781,782,785],{},"Konversationsnachrichten aller Kanäle fließen in eine einzige Tabelle ",[29,771,35],{}," — SMS, WhatsApp, generisch und Chat vollständig, dazu ",[774,775,776],"em",{},"konversationelle"," E-Mail (eingehende Nachrichten und bestätigte Agentenantworten, idempotent über den Index ",[29,779,780],{},"by_external_message_id"," mitgespiegelt). Kampagnen-, Transaktions- und Automations-E-Mail ist bewusst ausgeschlossen — das sind keine Gesprächsbeiträge und erscheint stattdessen über ",[29,783,784],{},"contactActivities"," im Tab „Aktivität“ des Kontakts. Die Tabelle:",[76,787,789],{"className":78,"code":788,"language":80,"meta":81,"style":81},"unifiedMessages: defineTable({\n  threadId: v.id('conversationThreads'),\n  channel: v.union(\n    v.literal('email'),\n    v.literal('sms'),\n    v.literal('whatsapp'),\n    v.literal('generic'),\n    v.literal('chat')\n  ),\n  direction: v.union(v.literal('inbound'), v.literal('outbound')),\n  contactId: v.optional(v.id('contacts')),\n  memberId: v.optional(v.string()),     \u002F\u002F internal sender (BetterAuth user ID)\n  content: v.string(),                   \u002F\u002F JSON: { text, html, subject, mediaUrl }\n  contentVersion: v.optional(v.number()), \u002F\u002F schema version of the content blob\n  externalMessageId: v.optional(v.string()),\n  status: v.union(\n    v.literal('received'),\n    v.literal('queued'),\n    v.literal('sent'),\n    v.literal('delivered'),\n    v.literal('read'),\n    v.literal('failed')\n  ),\n  metadata: v.optional(v.string()),      \u002F\u002F Channel-specific metadata (JSON)\n  createdAt: v.number(),\n})\n  .index('by_thread', ['threadId'])\n  .index('by_channel', ['channel'])\n  .index('by_contact', ['contactId'])\n  .index('by_created_at', ['createdAt'])\n",[29,790,791,804,820,831,846,859,872,885,899,904,934,953,970,983,1001,1015,1024,1037,1050,1063,1076,1089,1102,1106,1123,1133,1138,1160,1178,1196],{"__ignoreMap":81},[85,792,793,795,798,801],{"class":87,"line":88},[85,794,35],{"class":136},[85,796,797],{"class":140},": ",[85,799,800],{"class":201},"defineTable",[85,802,803],{"class":140},"({\n",[85,805,806,809,812,814,817],{"class":87,"line":95},[85,807,808],{"class":140},"  threadId: v.",[85,810,811],{"class":201},"id",[85,813,205],{"class":140},[85,815,816],{"class":364},"'conversationThreads'",[85,818,819],{"class":140},"),\n",[85,821,822,825,828],{"class":87,"line":101},[85,823,824],{"class":140},"  channel: v.",[85,826,827],{"class":201},"union",[85,829,830],{"class":140},"(\n",[85,832,833,836,839,841,844],{"class":87,"line":107},[85,834,835],{"class":140},"    v.",[85,837,838],{"class":201},"literal",[85,840,205],{"class":140},[85,842,843],{"class":364},"'email'",[85,845,819],{"class":140},[85,847,848,850,852,854,857],{"class":87,"line":113},[85,849,835],{"class":140},[85,851,838],{"class":201},[85,853,205],{"class":140},[85,855,856],{"class":364},"'sms'",[85,858,819],{"class":140},[85,860,861,863,865,867,870],{"class":87,"line":122},[85,862,835],{"class":140},[85,864,838],{"class":201},[85,866,205],{"class":140},[85,868,869],{"class":364},"'whatsapp'",[85,871,819],{"class":140},[85,873,874,876,878,880,883],{"class":87,"line":129},[85,875,835],{"class":140},[85,877,838],{"class":201},[85,879,205],{"class":140},[85,881,882],{"class":364},"'generic'",[85,884,819],{"class":140},[85,886,887,889,891,893,896],{"class":87,"line":144},[85,888,835],{"class":140},[85,890,838],{"class":201},[85,892,205],{"class":140},[85,894,895],{"class":364},"'chat'",[85,897,898],{"class":140},")\n",[85,900,901],{"class":87,"line":150},[85,902,903],{"class":140},"  ),\n",[85,905,906,909,911,914,916,918,921,924,926,928,931],{"class":87,"line":156},[85,907,908],{"class":140},"  direction: v.",[85,910,827],{"class":201},[85,912,913],{"class":140},"(v.",[85,915,838],{"class":201},[85,917,205],{"class":140},[85,919,920],{"class":364},"'inbound'",[85,922,923],{"class":140},"), v.",[85,925,838],{"class":201},[85,927,205],{"class":140},[85,929,930],{"class":364},"'outbound'",[85,932,933],{"class":140},")),\n",[85,935,936,939,942,944,946,948,951],{"class":87,"line":162},[85,937,938],{"class":140},"  contactId: v.",[85,940,941],{"class":201},"optional",[85,943,913],{"class":140},[85,945,811],{"class":201},[85,947,205],{"class":140},[85,949,950],{"class":364},"'contacts'",[85,952,933],{"class":140},[85,954,955,958,960,962,964,967],{"class":87,"line":168},[85,956,957],{"class":140},"  memberId: v.",[85,959,941],{"class":201},[85,961,913],{"class":140},[85,963,519],{"class":201},[85,965,966],{"class":140},"()),     ",[85,968,969],{"class":91},"\u002F\u002F internal sender (BetterAuth user ID)\n",[85,971,972,975,977,980],{"class":87,"line":174},[85,973,974],{"class":140},"  content: v.",[85,976,519],{"class":201},[85,978,979],{"class":140},"(),                   ",[85,981,982],{"class":91},"\u002F\u002F JSON: { text, html, subject, mediaUrl }\n",[85,984,985,988,990,992,995,998],{"class":87,"line":187},[85,986,987],{"class":140},"  contentVersion: v.",[85,989,941],{"class":201},[85,991,913],{"class":140},[85,993,994],{"class":201},"number",[85,996,997],{"class":140},"()), ",[85,999,1000],{"class":91},"\u002F\u002F schema version of the content blob\n",[85,1002,1003,1006,1008,1010,1012],{"class":87,"line":192},[85,1004,1005],{"class":140},"  externalMessageId: v.",[85,1007,941],{"class":201},[85,1009,913],{"class":140},[85,1011,519],{"class":201},[85,1013,1014],{"class":140},"()),\n",[85,1016,1017,1020,1022],{"class":87,"line":198},[85,1018,1019],{"class":140},"  status: v.",[85,1021,827],{"class":201},[85,1023,830],{"class":140},[85,1025,1026,1028,1030,1032,1035],{"class":87,"line":233},[85,1027,835],{"class":140},[85,1029,838],{"class":201},[85,1031,205],{"class":140},[85,1033,1034],{"class":364},"'received'",[85,1036,819],{"class":140},[85,1038,1039,1041,1043,1045,1048],{"class":87,"line":238},[85,1040,835],{"class":140},[85,1042,838],{"class":201},[85,1044,205],{"class":140},[85,1046,1047],{"class":364},"'queued'",[85,1049,819],{"class":140},[85,1051,1052,1054,1056,1058,1061],{"class":87,"line":244},[85,1053,835],{"class":140},[85,1055,838],{"class":201},[85,1057,205],{"class":140},[85,1059,1060],{"class":364},"'sent'",[85,1062,819],{"class":140},[85,1064,1065,1067,1069,1071,1074],{"class":87,"line":274},[85,1066,835],{"class":140},[85,1068,838],{"class":201},[85,1070,205],{"class":140},[85,1072,1073],{"class":364},"'delivered'",[85,1075,819],{"class":140},[85,1077,1078,1080,1082,1084,1087],{"class":87,"line":279},[85,1079,835],{"class":140},[85,1081,838],{"class":201},[85,1083,205],{"class":140},[85,1085,1086],{"class":364},"'read'",[85,1088,819],{"class":140},[85,1090,1091,1093,1095,1097,1100],{"class":87,"line":285},[85,1092,835],{"class":140},[85,1094,838],{"class":201},[85,1096,205],{"class":140},[85,1098,1099],{"class":364},"'failed'",[85,1101,898],{"class":140},[85,1103,1104],{"class":87,"line":305},[85,1105,903],{"class":140},[85,1107,1108,1111,1113,1115,1117,1120],{"class":87,"line":311},[85,1109,1110],{"class":140},"  metadata: v.",[85,1112,941],{"class":201},[85,1114,913],{"class":140},[85,1116,519],{"class":201},[85,1118,1119],{"class":140},"()),      ",[85,1121,1122],{"class":91},"\u002F\u002F Channel-specific metadata (JSON)\n",[85,1124,1125,1128,1130],{"class":87,"line":316},[85,1126,1127],{"class":140},"  createdAt: v.",[85,1129,994],{"class":201},[85,1131,1132],{"class":140},"(),\n",[85,1134,1135],{"class":87,"line":322},[85,1136,1137],{"class":140},"})\n",[85,1139,1140,1143,1146,1148,1151,1154,1157],{"class":87,"line":328},[85,1141,1142],{"class":140},"  .",[85,1144,1145],{"class":201},"index",[85,1147,205],{"class":140},[85,1149,1150],{"class":364},"'by_thread'",[85,1152,1153],{"class":140},", [",[85,1155,1156],{"class":364},"'threadId'",[85,1158,1159],{"class":140},"])\n",[85,1161,1162,1164,1166,1168,1171,1173,1176],{"class":87,"line":334},[85,1163,1142],{"class":140},[85,1165,1145],{"class":201},[85,1167,205],{"class":140},[85,1169,1170],{"class":364},"'by_channel'",[85,1172,1153],{"class":140},[85,1174,1175],{"class":364},"'channel'",[85,1177,1159],{"class":140},[85,1179,1180,1182,1184,1186,1189,1191,1194],{"class":87,"line":340},[85,1181,1142],{"class":140},[85,1183,1145],{"class":201},[85,1185,205],{"class":140},[85,1187,1188],{"class":364},"'by_contact'",[85,1190,1153],{"class":140},[85,1192,1193],{"class":364},"'contactId'",[85,1195,1159],{"class":140},[85,1197,1198,1200,1202,1204,1207,1209,1212],{"class":87,"line":346},[85,1199,1142],{"class":140},[85,1201,1145],{"class":201},[85,1203,205],{"class":140},[85,1205,1206],{"class":364},"'by_created_at'",[85,1208,1153],{"class":140},[85,1210,1211],{"class":364},"'createdAt'",[85,1213,1159],{"class":140},[16,1215,1216,1217,1220,1221,1225],{},"Die Tabelle ",[29,1218,1219],{},"conversationThreads"," (eingeführt in der ",[20,1222,1224],{"href":1223},"\u002Fvision\u002Fagent-pipeline","Agenten-Strecke",") wird zum universellen Knotenpunkt. Ein Thread kann E-Mail-Nachrichten, SMS-Nachrichten, Chat-Nachrichten und Webhook-Ereignisse enthalten — allesamt in chronologischer Reihenfolge.",[567,1227,1229],{"id":1228},"kanalkonfiguration","Kanalkonfiguration",[16,1231,1232],{},"Jede Organisation konfiguriert, welche Kanäle aktiv sind und wie sie sich verbinden:",[76,1234,1236],{"className":78,"code":1235,"language":80,"meta":81,"style":81},"channelConfigs: defineTable({\n  channel: v.union(\n    v.literal('email'),\n    v.literal('sms'),\n    v.literal('whatsapp'),\n    v.literal('generic'),\n    v.literal('chat')\n  ),\n  isEnabled: v.boolean(),\n  displayName: v.optional(v.string()),\n  config: v.optional(v.string()),  \u002F\u002F JSON: provider-specific (API keys, phone numbers, etc.)\n  \u002F\u002F Health monitoring — folded directly onto the config row\n  healthStatus: v.optional(v.union(\n    v.literal('healthy'),\n    v.literal('degraded'),\n    v.literal('down')\n  )),\n  lastHealthCheckAt: v.optional(v.number()),\n  lastSuccessfulSend: v.optional(v.number()),\n  lastError: v.optional(v.string()),\n  createdAt: v.number(),\n  updatedAt: v.number(),\n})\n  .index('by_channel', ['channel'])\n",[29,1237,1238,1248,1256,1268,1280,1292,1304,1316,1320,1330,1343,1360,1365,1378,1391,1404,1417,1422,1435,1448,1461,1469,1478,1482],{"__ignoreMap":81},[85,1239,1240,1242,1244,1246],{"class":87,"line":88},[85,1241,39],{"class":136},[85,1243,797],{"class":140},[85,1245,800],{"class":201},[85,1247,803],{"class":140},[85,1249,1250,1252,1254],{"class":87,"line":95},[85,1251,824],{"class":140},[85,1253,827],{"class":201},[85,1255,830],{"class":140},[85,1257,1258,1260,1262,1264,1266],{"class":87,"line":101},[85,1259,835],{"class":140},[85,1261,838],{"class":201},[85,1263,205],{"class":140},[85,1265,843],{"class":364},[85,1267,819],{"class":140},[85,1269,1270,1272,1274,1276,1278],{"class":87,"line":107},[85,1271,835],{"class":140},[85,1273,838],{"class":201},[85,1275,205],{"class":140},[85,1277,856],{"class":364},[85,1279,819],{"class":140},[85,1281,1282,1284,1286,1288,1290],{"class":87,"line":113},[85,1283,835],{"class":140},[85,1285,838],{"class":201},[85,1287,205],{"class":140},[85,1289,869],{"class":364},[85,1291,819],{"class":140},[85,1293,1294,1296,1298,1300,1302],{"class":87,"line":122},[85,1295,835],{"class":140},[85,1297,838],{"class":201},[85,1299,205],{"class":140},[85,1301,882],{"class":364},[85,1303,819],{"class":140},[85,1305,1306,1308,1310,1312,1314],{"class":87,"line":129},[85,1307,835],{"class":140},[85,1309,838],{"class":201},[85,1311,205],{"class":140},[85,1313,895],{"class":364},[85,1315,898],{"class":140},[85,1317,1318],{"class":87,"line":144},[85,1319,903],{"class":140},[85,1321,1322,1325,1328],{"class":87,"line":150},[85,1323,1324],{"class":140},"  isEnabled: v.",[85,1326,1327],{"class":201},"boolean",[85,1329,1132],{"class":140},[85,1331,1332,1335,1337,1339,1341],{"class":87,"line":156},[85,1333,1334],{"class":140},"  displayName: v.",[85,1336,941],{"class":201},[85,1338,913],{"class":140},[85,1340,519],{"class":201},[85,1342,1014],{"class":140},[85,1344,1345,1348,1350,1352,1354,1357],{"class":87,"line":162},[85,1346,1347],{"class":140},"  config: v.",[85,1349,941],{"class":201},[85,1351,913],{"class":140},[85,1353,519],{"class":201},[85,1355,1356],{"class":140},"()),  ",[85,1358,1359],{"class":91},"\u002F\u002F JSON: provider-specific (API keys, phone numbers, etc.)\n",[85,1361,1362],{"class":87,"line":168},[85,1363,1364],{"class":91},"  \u002F\u002F Health monitoring — folded directly onto the config row\n",[85,1366,1367,1370,1372,1374,1376],{"class":87,"line":174},[85,1368,1369],{"class":140},"  healthStatus: v.",[85,1371,941],{"class":201},[85,1373,913],{"class":140},[85,1375,827],{"class":201},[85,1377,830],{"class":140},[85,1379,1380,1382,1384,1386,1389],{"class":87,"line":187},[85,1381,835],{"class":140},[85,1383,838],{"class":201},[85,1385,205],{"class":140},[85,1387,1388],{"class":364},"'healthy'",[85,1390,819],{"class":140},[85,1392,1393,1395,1397,1399,1402],{"class":87,"line":192},[85,1394,835],{"class":140},[85,1396,838],{"class":201},[85,1398,205],{"class":140},[85,1400,1401],{"class":364},"'degraded'",[85,1403,819],{"class":140},[85,1405,1406,1408,1410,1412,1415],{"class":87,"line":198},[85,1407,835],{"class":140},[85,1409,838],{"class":201},[85,1411,205],{"class":140},[85,1413,1414],{"class":364},"'down'",[85,1416,898],{"class":140},[85,1418,1419],{"class":87,"line":233},[85,1420,1421],{"class":140},"  )),\n",[85,1423,1424,1427,1429,1431,1433],{"class":87,"line":238},[85,1425,1426],{"class":140},"  lastHealthCheckAt: v.",[85,1428,941],{"class":201},[85,1430,913],{"class":140},[85,1432,994],{"class":201},[85,1434,1014],{"class":140},[85,1436,1437,1440,1442,1444,1446],{"class":87,"line":244},[85,1438,1439],{"class":140},"  lastSuccessfulSend: v.",[85,1441,941],{"class":201},[85,1443,913],{"class":140},[85,1445,994],{"class":201},[85,1447,1014],{"class":140},[85,1449,1450,1453,1455,1457,1459],{"class":87,"line":274},[85,1451,1452],{"class":140},"  lastError: v.",[85,1454,941],{"class":201},[85,1456,913],{"class":140},[85,1458,519],{"class":201},[85,1460,1014],{"class":140},[85,1462,1463,1465,1467],{"class":87,"line":279},[85,1464,1127],{"class":140},[85,1466,994],{"class":201},[85,1468,1132],{"class":140},[85,1470,1471,1474,1476],{"class":87,"line":285},[85,1472,1473],{"class":140},"  updatedAt: v.",[85,1475,994],{"class":201},[85,1477,1132],{"class":140},[85,1479,1480],{"class":87,"line":305},[85,1481,1137],{"class":140},[85,1483,1484,1486,1488,1490,1492,1494,1496],{"class":87,"line":311},[85,1485,1142],{"class":140},[85,1487,1145],{"class":201},[85,1489,205],{"class":140},[85,1491,1170],{"class":364},[85,1493,1153],{"class":140},[85,1495,1175],{"class":364},[85,1497,1159],{"class":140},[16,1499,1500,1501,1504],{},"Das Feld ",[29,1502,1503],{},"config"," enthält eine anbieterspezifische JSON-Zeichenkette (API-Keys, Telefonnummern usw.). Wer selbst hostet, braucht für SMS und WhatsApp API-Keys der jeweiligen Anbieter — das sind die einzigen externen Abhängigkeiten, die sich nicht selbst hosten lassen.",[727,1506,1509],{"title":1507,"type":1508},"Kanal-Zugangsdaten sind im Ruhezustand verschlüsselt","success",[16,1510,1511,47,1514,1517,1518,1520,1521,47,1524,1526,1527,1530,1531,1534,1535,1537,1538,1541,1542,1544,1545,1548,1549,1552,1553,1556],{},[29,1512,1513],{},"updateChannelConfig",[29,1515,1516],{},"apps\u002Fapi\u002Fconvex\u002FunifiedMessages.ts",") schreibt den ",[29,1519,1503],{},"-Blob niemals im Klartext. Die Funktion läuft in der v8-Laufzeitumgebung und plant deshalb die Node-Action ",[29,1522,1523],{},"encryptAndPersistConfig",[29,1525,721],{},") ein, die den Klartext über ",[29,1528,1529],{},"apps\u002Fapi\u002Fconvex\u002Flib\u002FcredentialCrypto.ts"," in einen AES-256-GCM-Umschlag hüllt und die Zeile über ",[29,1532,1533],{},"setChannelConfigSecret"," patcht — den einzigen Schreiber der Spalte ",[29,1536,1503],{},". ",[29,1539,1540],{},"getChannelConfigInternal"," entschlüsselt sie innerhalb von ",[29,1543,718],{}," für den Versand wieder. Die Spalte ",[29,1546,1547],{},"channelConfigs.config"," hält auf der Platte daher stets nur einen verschlüsselten Umschlag, nach demselben Schema, das bereits für ",[29,1550,1551],{},"externalMailAccounts"," verwendet wird. Die eingehenden Anbieter-Secrets (Twilio-Auth-Token, Meta-App-Secret und Verify-Token, generisches gemeinsames Secret) stammen aus derselben Zeile: Die v8-Webhook-Routen können sie nicht selbst entschlüsseln, lesen sie deshalb über die Node-Action ",[29,1554,1555],{},"channels.credentials.getInboundSecret"," und fallen auf die Umgebungsvariable des Deployments zurück, wenn der Kanal keines hinterlegt hat.",[567,1558,1560],{"id":1559},"health-monitoring-der-adapter","Health-Monitoring der Adapter",[16,1562,1563,1564,1566,1567,741,1570,1572,1573,1575,1576,1579,1580,51,1583,36,1586,1589,1590,1593,1594,1597,1598,1601,1602,1605,1606,1609,1610,1613,1614,1616],{},"Jeder Adapter stellt eine Methode ",[29,1565,700],{}," bereit. Ein Convex-Cronjob (",[29,1568,1569],{},"channel health checks",[29,1571,58],{},") läuft alle 5 Minuten, iteriert über die aktivierten ",[29,1574,39],{},"-Zeilen und schreibt das Ergebnis über ",[29,1577,1578],{},"updateChannelHealth"," zurück, das ",[29,1581,1582],{},"healthStatus",[29,1584,1585],{},"lastHealthCheckAt",[29,1587,1588],{},"lastError"," patcht. E-Mail und Chat haben keinen Adapter, weshalb allein das Vorhandensein einer Konfiguration als Signal dient und sie als ",[29,1591,1592],{},"healthy"," markiert werden; eine SMS-\u002FWhatsApp-\u002Fgenerische Zeile ohne Zugangsdaten wird ohne Netzwerkaufruf als ",[29,1595,1596],{},"down"," markiert; eine konfigurierte Zeile führt die echte Sonde des Adapters aus (",[29,1599,1600],{},"probeChannelHealth",", das ",[29,1603,1604],{},"node:crypto"," zum Entschlüsseln der hinterlegten Zugangsdaten braucht und deshalb in einer ",[29,1607,1608],{},"'use node'","-Action lebt) und meldet ",[29,1611,1612],{},"degraded","\u002F",[29,1615,1596],{},", wenn der Anbieter sie abweist.",[16,1618,1619,1622,1623,1626,1627,1630,1631,1633,1634,36,1637,565],{},[29,1620,1621],{},"lastSuccessfulSend"," ist ",[774,1624,1625],{},"kein"," Feld des Health-Crons, obwohl es in derselben Zeile sitzt: ",[29,1628,1629],{},"unifiedMessages.recordOutbound"," stempelt es anhand eines tatsächlich zugestellten Versands. Eine Sonde meldet Erreichbarkeit, keinen Versand — deshalb trägt ",[29,1632,300],{}," nur ",[29,1635,1636],{},"status",[29,1638,1588],{},[16,1640,1641],{},"Das angestrebte gesundheitsgesteuerte Verhalten, sobald der ausgehende Versand vollständig verdrahtet ist, sieht so aus:",[1643,1644,1645,1652],"ul",{},[1646,1647,1648,1651],"li",{},[534,1649,1650],{},"Degraded"," — ausgehende Nachrichten werden mit Backoff eingereiht statt sofort gesendet, und das Dashboard zeigt eine Warnung.",[1646,1653,1654,1657],{},[534,1655,1656],{},"Down"," — ausgehende Nachrichten werden für eine spätere Zustellung eingereiht; eingehende Webhooks nehmen weiterhin Nachrichten an und speichern sie (der Anbieter liefert weiterhin an uns aus, auch wenn wir nicht zurücksenden können), und das Dashboard zeigt eine Warnmeldung mit dem letzten Fehler.",[16,1659,1660],{},"Ziel ist es, stille Fehlschläge zu verhindern — fällt die Twilio-API aus, sehen die Support-Mitarbeitenden die Beeinträchtigung in ihrem Dashboard, bevor Kundinnen und Kunden fehlende SMS-Antworten melden.",[727,1662,1664],{"title":1663,"type":730},"Noch nicht aktiv: gesundheitsgesteuertes Einreihen ausgehender Nachrichten",[16,1665,1666,1667,1613,1669,1671,1672,1675,1676,1678,1679,1681],{},"Der 5-Minuten-Cron für Health-Checks und die Felder ",[29,1668,1582],{},[29,1670,1588],{}," sind aktiv. Das oben beschriebene Verhalten ",[534,1673,1674],{},"Einreihen mit Backoff"," bei degraded\u002Fdown beschreibt den angestrebten Entwurf — es ist noch nicht implementiert. Der ausgehende Versandpfad (",[29,1677,718],{},") hat inzwischen aktive Aufrufer — den Antwortpfad des Agenten und den manuellen Composer (siehe „Der ausgehende Versand ist verdrahtet, bleibt aber ohne Zugangsdaten untätig“ unter „Eingebaute Adapter“) —, aber kein Versandpfad zieht vor dem Absenden ",[29,1680,1582],{}," heran, sodass der Gesundheitszustand Sendungen bislang weder blockiert noch einreiht.",[61,1683,1685],{"id":1684},"muster-für-eingehende-webhooks","Muster für eingehende Webhooks",[16,1687,1688],{},"Die eingehenden Nachrichten jedes Kanals treffen über einen HTTP-Webhook ein. Das Muster ist identisch mit den bestehenden Webhook-Handlern für MTA und Resend:",[76,1690,1695],{"className":1691,"code":1693,"language":1694},[1692],"language-text","External provider (Twilio, Meta, etc.)\n  → POST \u002Fwebhooks\u002F{channel}\n  → Inbound pipeline rate-limits the request\n  → Adapter verifySignature() verifies webhook authenticity (Twilio HMAC, Meta X-Hub-Signature-256, generic shared secret)\n  → Adapter parseEvent() normalizes the payload\n  → Store in unifiedMessages (find-or-create contact + thread first)\n  → Feed into the Agent Pipeline (live — project onto an inboundMessages row and start the agent walker)\n","text",[29,1696,1693],{"__ignoreMap":81},[16,1698,1699,1700,1703,1704,1707,1708,47,1711,1714,1715,1718,1719,51,1722,51,1725,1728,1729,1732,1733,1735,1736,1739,1740,1743,1744,1747,1748,1751],{},"Die HTTP-Routen liegen in ",[29,1701,1702],{},"apps\u002Fapi\u002Fconvex\u002Fhttp.ts","; ihre Handler in ",[29,1705,1706],{},"apps\u002Fapi\u002Fconvex\u002Fwebhooks\u002Fchannels.ts"," sind dünne Hüllen, die ",[29,1709,1710],{},"runInboundPipeline()",[29,1712,1713],{},"apps\u002Fapi\u002Fconvex\u002Fwebhooks\u002Fpipeline.ts",") aufrufen, was Signaturprüfung und Parsing an die anbieterspezifischen Eingangsadapter unter ",[29,1716,1717],{},"apps\u002Fapi\u002Fconvex\u002Fwebhooks\u002Fadapters\u002F"," delegiert (",[29,1720,1721],{},"twilio.ts",[29,1723,1724],{},"meta.ts",[29,1726,1727],{},"generic.ts","). Die Signaturprüfungen werden durchgesetzt — eine fehlende oder ungültige Signatur wird abgewiesen, bevor irgendeine Nachricht gespeichert wird. (Hinweis: Diese Eingangsadapter sind von den oben beschriebenen ausgehenden ",[29,1730,1731],{},"ChannelAdapter","-Klassen verschieden und sind die einzige Stelle, an der die Signatur eines eingehenden Kanals geprüft wird.) Nach dem Speichern der ",[29,1734,35],{},"-Zeile projiziert ",[29,1737,1738],{},"processInboundChannel"," die Kanalnachricht auf eine ",[29,1741,1742],{},"inboundMessages","-Zeile und plant ",[29,1745,1746],{},"internal.agent.walker.start"," ein — eingehende SMS\u002FWhatsApp\u002Fgenerische Nachrichten laufen also nun durch dieselbe Agenten-Strecke wie E-Mail. Dieser Schritt ist Best-Effort und über das Flag ",[29,1749,1750],{},"ai.agent"," abgesichert: Ein deaktivierter oder falsch konfigurierter Agent verwandelt eine gespeicherte eingehende Nachricht nie in einen 5xx, der den Anbieter zu einem erneuten Versuch bewegen würde. Die Routenregistrierungen:",[76,1753,1755],{"className":78,"code":1754,"language":80,"meta":81,"style":81},"\u002F\u002F SMS inbound (Twilio)\nhttp.route({\n  path: '\u002Fwebhooks\u002Fsms',\n  method: 'POST',\n  handler: handleSmsWebhook,\n})\n\n\u002F\u002F WhatsApp inbound (Meta)\nhttp.route({\n  path: '\u002Fwebhooks\u002Fwhatsapp',\n  method: 'POST',\n  handler: handleWhatsAppWebhook,\n})\n\n\u002F\u002F WhatsApp verification challenge — Meta requires a GET handshake\nhttp.route({\n  path: '\u002Fwebhooks\u002Fwhatsapp',\n  method: 'GET',\n  handler: handleWhatsAppWebhook,\n})\n\n\u002F\u002F Generic shared-secret webhook (for custom integrations)\nhttp.route({\n  path: '\u002Fwebhooks\u002Fchannel',\n  method: 'POST',\n  handler: handleGenericWebhook,\n})\n",[29,1756,1757,1762,1772,1783,1793,1798,1802,1806,1811,1819,1828,1836,1841,1845,1849,1854,1862,1870,1879,1883,1887,1891,1896,1904,1913,1921,1926],{"__ignoreMap":81},[85,1758,1759],{"class":87,"line":88},[85,1760,1761],{"class":91},"\u002F\u002F SMS inbound (Twilio)\n",[85,1763,1764,1767,1770],{"class":87,"line":95},[85,1765,1766],{"class":140},"http.",[85,1768,1769],{"class":201},"route",[85,1771,803],{"class":140},[85,1773,1774,1777,1780],{"class":87,"line":101},[85,1775,1776],{"class":140},"  path: ",[85,1778,1779],{"class":364},"'\u002Fwebhooks\u002Fsms'",[85,1781,1782],{"class":140},",\n",[85,1784,1785,1788,1791],{"class":87,"line":107},[85,1786,1787],{"class":140},"  method: ",[85,1789,1790],{"class":364},"'POST'",[85,1792,1782],{"class":140},[85,1794,1795],{"class":87,"line":113},[85,1796,1797],{"class":140},"  handler: handleSmsWebhook,\n",[85,1799,1800],{"class":87,"line":122},[85,1801,1137],{"class":140},[85,1803,1804],{"class":87,"line":129},[85,1805,126],{"emptyLinePlaceholder":125},[85,1807,1808],{"class":87,"line":144},[85,1809,1810],{"class":91},"\u002F\u002F WhatsApp inbound (Meta)\n",[85,1812,1813,1815,1817],{"class":87,"line":150},[85,1814,1766],{"class":140},[85,1816,1769],{"class":201},[85,1818,803],{"class":140},[85,1820,1821,1823,1826],{"class":87,"line":156},[85,1822,1776],{"class":140},[85,1824,1825],{"class":364},"'\u002Fwebhooks\u002Fwhatsapp'",[85,1827,1782],{"class":140},[85,1829,1830,1832,1834],{"class":87,"line":162},[85,1831,1787],{"class":140},[85,1833,1790],{"class":364},[85,1835,1782],{"class":140},[85,1837,1838],{"class":87,"line":168},[85,1839,1840],{"class":140},"  handler: handleWhatsAppWebhook,\n",[85,1842,1843],{"class":87,"line":174},[85,1844,1137],{"class":140},[85,1846,1847],{"class":87,"line":187},[85,1848,126],{"emptyLinePlaceholder":125},[85,1850,1851],{"class":87,"line":192},[85,1852,1853],{"class":91},"\u002F\u002F WhatsApp verification challenge — Meta requires a GET handshake\n",[85,1855,1856,1858,1860],{"class":87,"line":198},[85,1857,1766],{"class":140},[85,1859,1769],{"class":201},[85,1861,803],{"class":140},[85,1863,1864,1866,1868],{"class":87,"line":233},[85,1865,1776],{"class":140},[85,1867,1825],{"class":364},[85,1869,1782],{"class":140},[85,1871,1872,1874,1877],{"class":87,"line":238},[85,1873,1787],{"class":140},[85,1875,1876],{"class":364},"'GET'",[85,1878,1782],{"class":140},[85,1880,1881],{"class":87,"line":244},[85,1882,1840],{"class":140},[85,1884,1885],{"class":87,"line":274},[85,1886,1137],{"class":140},[85,1888,1889],{"class":87,"line":279},[85,1890,126],{"emptyLinePlaceholder":125},[85,1892,1893],{"class":87,"line":285},[85,1894,1895],{"class":91},"\u002F\u002F Generic shared-secret webhook (for custom integrations)\n",[85,1897,1898,1900,1902],{"class":87,"line":305},[85,1899,1766],{"class":140},[85,1901,1769],{"class":201},[85,1903,803],{"class":140},[85,1905,1906,1908,1911],{"class":87,"line":311},[85,1907,1776],{"class":140},[85,1909,1910],{"class":364},"'\u002Fwebhooks\u002Fchannel'",[85,1912,1782],{"class":140},[85,1914,1915,1917,1919],{"class":87,"line":316},[85,1916,1787],{"class":140},[85,1918,1790],{"class":364},[85,1920,1782],{"class":140},[85,1922,1923],{"class":87,"line":322},[85,1924,1925],{"class":140},"  handler: handleGenericWebhook,\n",[85,1927,1928],{"class":87,"line":328},[85,1929,1137],{"class":140},[61,1931,1933],{"id":1932},"crm-hub","CRM-Hub",[16,1935,1936,1937,1940],{},"Der CRM-Hub erweitert die bestehende ",[29,1938,1939],{},"contacts","-Tabelle um eine vereinheitlichte Identitätsverwaltung und Beziehungsintelligenz.",[567,1942,1944],{"id":1943},"vereinheitlichung-von-kontaktidentitäten","Vereinheitlichung von Kontaktidentitäten",[16,1946,1947],{},"Dieselbe Person kommuniziert oft über mehrere Kanäle — geschäftliche E-Mail, private E-Mail, Telefon, WhatsApp. Das CRM vereinigt diese zu einem einzigen Kontaktprofil:",[76,1949,1951],{"className":78,"code":1950,"language":80,"meta":81,"style":81},"contactIdentities: defineTable({\n  contactId: v.id('contacts'),\n  channel: v.string(),         \u002F\u002F 'email', 'phone', 'whatsapp', 'twitter', etc.\n  identifier: v.string(),      \u002F\u002F email address, phone number, handle\n  isPrimary: v.boolean(),\n  verifiedAt: v.optional(v.number()),\n  createdAt: v.number(),\n})\n  .index('by_contact', ['contactId'])\n  .index('by_identifier', ['channel', 'identifier'])\n",[29,1952,1953,1963,1975,1987,2000,2009,2022,2030,2034,2050],{"__ignoreMap":81},[85,1954,1955,1957,1959,1961],{"class":87,"line":88},[85,1956,43],{"class":136},[85,1958,797],{"class":140},[85,1960,800],{"class":201},[85,1962,803],{"class":140},[85,1964,1965,1967,1969,1971,1973],{"class":87,"line":95},[85,1966,938],{"class":140},[85,1968,811],{"class":201},[85,1970,205],{"class":140},[85,1972,950],{"class":364},[85,1974,819],{"class":140},[85,1976,1977,1979,1981,1984],{"class":87,"line":101},[85,1978,824],{"class":140},[85,1980,519],{"class":201},[85,1982,1983],{"class":140},"(),         ",[85,1985,1986],{"class":91},"\u002F\u002F 'email', 'phone', 'whatsapp', 'twitter', etc.\n",[85,1988,1989,1992,1994,1997],{"class":87,"line":107},[85,1990,1991],{"class":140},"  identifier: v.",[85,1993,519],{"class":201},[85,1995,1996],{"class":140},"(),      ",[85,1998,1999],{"class":91},"\u002F\u002F email address, phone number, handle\n",[85,2001,2002,2005,2007],{"class":87,"line":113},[85,2003,2004],{"class":140},"  isPrimary: v.",[85,2006,1327],{"class":201},[85,2008,1132],{"class":140},[85,2010,2011,2014,2016,2018,2020],{"class":87,"line":122},[85,2012,2013],{"class":140},"  verifiedAt: v.",[85,2015,941],{"class":201},[85,2017,913],{"class":140},[85,2019,994],{"class":201},[85,2021,1014],{"class":140},[85,2023,2024,2026,2028],{"class":87,"line":129},[85,2025,1127],{"class":140},[85,2027,994],{"class":201},[85,2029,1132],{"class":140},[85,2031,2032],{"class":87,"line":144},[85,2033,1137],{"class":140},[85,2035,2036,2038,2040,2042,2044,2046,2048],{"class":87,"line":150},[85,2037,1142],{"class":140},[85,2039,1145],{"class":201},[85,2041,205],{"class":140},[85,2043,1188],{"class":364},[85,2045,1153],{"class":140},[85,2047,1193],{"class":364},[85,2049,1159],{"class":140},[85,2051,2052,2054,2056,2058,2061,2063,2065,2067,2070],{"class":87,"line":156},[85,2053,1142],{"class":140},[85,2055,1145],{"class":201},[85,2057,205],{"class":140},[85,2059,2060],{"class":364},"'by_identifier'",[85,2062,1153],{"class":140},[85,2064,1175],{"class":364},[85,2066,51],{"class":140},[85,2068,2069],{"class":364},"'identifier'",[85,2071,1159],{"class":140},[16,2073,2074,2075,2077],{},"Trifft eine eingehende Nachricht ein, prüft die Strecke ",[29,2076,43],{}," auf einen passenden Bezeichner:",[2079,2080,2081,2087,2093,2113],"ol",{},[1646,2082,2083,2086],{},[534,2084,2085],{},"Treffer gefunden"," → mit bestehendem Kontakt verknüpfen, Thread aktualisieren",[1646,2088,2089,2092],{},[534,2090,2091],{},"Kein Treffer"," → einen neuen Kontakt samt Identität anlegen",[1646,2094,2095,2098,2099,2102,2103,47,2106,741,2109,2112],{},[534,2096,2097],{},"Automatisches Zusammenführen bei exaktem Bezeichner"," → teilen sich zwei lebende Kontakte einen ",[774,2100,2101],{},"exakt"," übereinstimmenden starken Bezeichner (E-Mail \u002F Telefon \u002F SMS \u002F WhatsApp), faltet der Cron ",[29,2104,2105],{},"auto-merge duplicate contacts",[29,2107,2108],{},"autoMergeDuplicates",[29,2110,2111],{},"apps\u002Fapi\u002Fconvex\u002Fcontacts\u002Fidentities.ts",", alle 6 Stunden) die Dubletten ohne menschliche Bestätigung in den ältesten Kontakt hinein. Starke Kanäle sind die eindeutige Teilmenge; schwache bzw. Social-Handles (Twitter, generisch, Chat) sind bewusst ausgenommen.",[1646,2114,2115,2118,2119,2122],{},[534,2116,2117],{},"Vorgeschlagene Zusammenführung"," → für alles außerhalb dieser Teilmenge exakter starker Bezeichner (etwa unscharfe Überschneidungen oder solche bei Social-Handles) zeigt ",[29,2120,2121],{},"getMergeSuggestions"," im Tab „Identitäten“ des Kontakts einen Zusammenführungsvorschlag an, den ein Mensch bestätigen muss, statt automatisch zusammenzuführen.",[567,2124,2126],{"id":2125},"beziehungsintelligenz","Beziehungsintelligenz",[16,2128,2129,2130,2132,2133,2136,2137,2140],{},"Die Vision sieht vor, dass der Wissensgraph Beziehungserkenntnisse in das CRM einspeist. Die Tabelle ",[29,2131,46],{}," und ihr ",[29,2134,2135],{},"manual","-Upsert sind heute ausgeliefert; Beziehungen werden von Betreiberinnen und Betreibern eingetragen. Ein automatisierter Extraktionspfad würde einen neuen ",[29,2138,2139],{},"source","-Zweig und einen Schreiber ergänzen, der Konversationen auswertet — beides existiert noch nicht:",[76,2142,2144],{"className":78,"code":2143,"language":80,"meta":81,"style":81},"contactRelationships: defineTable({\n  fromContactId: v.id('contacts'),\n  toContactId: v.id('contacts'),\n  relationship: v.string(),    \u002F\u002F \"manager_of\", \"colleague\", \"reports_to\", etc.\n  confidence: v.number(),      \u002F\u002F 0-1\n  source: v.literal('manual'),\n  createdAt: v.number(),\n})\n  .index('by_from', ['fromContactId'])\n  .index('by_to', ['toContactId'])\n",[29,2145,2146,2156,2169,2182,2195,2207,2221,2229,2233,2251],{"__ignoreMap":81},[85,2147,2148,2150,2152,2154],{"class":87,"line":88},[85,2149,46],{"class":136},[85,2151,797],{"class":140},[85,2153,800],{"class":201},[85,2155,803],{"class":140},[85,2157,2158,2161,2163,2165,2167],{"class":87,"line":95},[85,2159,2160],{"class":140},"  fromContactId: v.",[85,2162,811],{"class":201},[85,2164,205],{"class":140},[85,2166,950],{"class":364},[85,2168,819],{"class":140},[85,2170,2171,2174,2176,2178,2180],{"class":87,"line":101},[85,2172,2173],{"class":140},"  toContactId: v.",[85,2175,811],{"class":201},[85,2177,205],{"class":140},[85,2179,950],{"class":364},[85,2181,819],{"class":140},[85,2183,2184,2187,2189,2192],{"class":87,"line":107},[85,2185,2186],{"class":140},"  relationship: v.",[85,2188,519],{"class":201},[85,2190,2191],{"class":140},"(),    ",[85,2193,2194],{"class":91},"\u002F\u002F \"manager_of\", \"colleague\", \"reports_to\", etc.\n",[85,2196,2197,2200,2202,2204],{"class":87,"line":113},[85,2198,2199],{"class":140},"  confidence: v.",[85,2201,994],{"class":201},[85,2203,1996],{"class":140},[85,2205,2206],{"class":91},"\u002F\u002F 0-1\n",[85,2208,2209,2212,2214,2216,2219],{"class":87,"line":122},[85,2210,2211],{"class":140},"  source: v.",[85,2213,838],{"class":201},[85,2215,205],{"class":140},[85,2217,2218],{"class":364},"'manual'",[85,2220,819],{"class":140},[85,2222,2223,2225,2227],{"class":87,"line":129},[85,2224,1127],{"class":140},[85,2226,994],{"class":201},[85,2228,1132],{"class":140},[85,2230,2231],{"class":87,"line":144},[85,2232,1137],{"class":140},[85,2234,2235,2237,2239,2241,2244,2246,2249],{"class":87,"line":150},[85,2236,1142],{"class":140},[85,2238,1145],{"class":201},[85,2240,205],{"class":140},[85,2242,2243],{"class":364},"'by_from'",[85,2245,1153],{"class":140},[85,2247,2248],{"class":364},"'fromContactId'",[85,2250,1159],{"class":140},[85,2252,2253,2255,2257,2259,2262,2264,2267],{"class":87,"line":156},[85,2254,1142],{"class":140},[85,2256,1145],{"class":201},[85,2258,205],{"class":140},[85,2260,2261],{"class":364},"'by_to'",[85,2263,1153],{"class":140},[85,2265,2266],{"class":364},"'toContactId'",[85,2268,1159],{"class":140},[16,2270,2271,2272,181],{},"Die CRM-Ansicht für jeden Kontakt soll Folgendes zeigen — ",[534,2273,2274],{},"heute schon aktiv",[1643,2276,2277,2283],{},[1646,2278,2279,2282],{},[534,2280,2281],{},"Vereinheitlichter Zeitstrahl"," — Konversationsnachrichten über alle Kanäle hinweg (SMS\u002FWhatsApp\u002Fgenerisch\u002FChat plus eingehende E-Mail und Agentenantworten) in chronologischer Reihenfolge; E-Mail aus Massenkampagnen, Transaktionen und Automationen lebt im Tab „Aktivität“, nicht hier",[1646,2284,2285,2288],{},[534,2286,2287],{},"Wissenszusammenfassung"," — zentrale Fakten, Präferenzen und Ziele aus dem Wissensgraphen",[16,2290,2291,2292,2295],{},"und ",[534,2293,2294],{},"auf der Roadmap"," (diese hängen von der Beziehungs- und Stimmungsextraktion ab, die noch nicht verdrahtet ist — siehe den Kasten unten):",[1643,2297,2298,2304,2310,2316],{},[1646,2299,2300,2303],{},[534,2301,2302],{},"Beziehungskarte"," — Verbindungen zu anderen Kontakten (Kolleginnen und Kollegen, unterstellte Personen usw.)",[1646,2305,2306,2309],{},[534,2307,2308],{},"Stimmungsverlauf"," — wie sich die Stimmung des Kontakts im Zeitverlauf entwickelt hat",[1646,2311,2312,2315],{},[534,2313,2314],{},"Offene Zusagen"," — in Konversationen gemachte Versprechen („Sie sagten, Sie schicken das Angebot bis Dienstag“)",[1646,2317,2318,2321],{},[534,2319,2320],{},"Kommunikationspräferenzen"," — bevorzugter Kanal, Antwortmuster, aktive Zeiten",[567,2323,2325],{"id":2324},"kommunikationsnative-aktualisierungen","Kommunikationsnative Aktualisierungen",[16,2327,2328],{},"Klassische CRMs verlangen manuelle Dateneingabe. In Owlat baut sich das CRM aus der tatsächlichen Kommunikation selbst auf:",[1643,2330,2331,2334,2337,2340],{},[1646,2332,2333],{},"Schreiben Sie einer Investorin oder einem Investor eine E-Mail → die Interaktion wird automatisch protokolliert",[1646,2335,2336],{},"Kippt die Stimmung einer Kundin oder eines Kunden ins Negative → der Beziehungs-Gesundheitswert wird aktualisiert",[1646,2338,2339],{},"Wird in einem Thread über einen Abschluss gesprochen → der Pipeline-Status spiegelt die Konversation wider",[1646,2341,2342],{},"Wechselt ein Kontakt den Job (erkannt an geänderten E-Mail-Signaturen) → das Profil wird aktualisiert",[16,2344,2345],{},"Angestrebt ist, dass all dies über die Extraktionsstrecke des Wissensgraphen geschieht — das CRM als Sicht auf den Wissensgraphen, nicht als separater Datenspeicher.",[727,2347,2349],{"title":2348,"type":730},"Teilweise: Die Wissensextraktion ist aktiv, die automatische Beziehungsextraktion nicht",[16,2350,2351,2352,2355,2356,2359,2360,2363,2364,2367,2368,2371,2372,2375,2376,2379,2380,2383,2384,2387,2388,2390,2391,47,2393,2396,2397,2399],{},"Der vereinheitlichte Zeitstrahl (",[29,2353,2354],{},"apps\u002Fweb"," ",[29,2357,2358],{},"UnifiedTimelineTab"," \u002F ",[29,2361,2362],{},"useUnifiedContactTimeline"," über ",[29,2365,2366],{},"getContactTimeline","), Kontaktidentitäten, das automatische Zusammenführen bei exaktem Bezeichner, manuelle Beziehungen und die Oberfläche für Zusammenführungsvorschläge sind allesamt aktiv. Auch die ",[534,2369,2370],{},"Wissens","-Extraktion pro Nachricht ist inzwischen in die Eingangsstrecke verdrahtet: Sobald eine Nachricht klassifiziert ist, plant ",[29,2373,2374],{},"processingLifecycle.ts"," sowohl für eingehende E-Mail als auch für eingehende Kanalnachrichten ",[29,2377,2378],{},"internal.knowledge.extraction.extractFromMessage"," ein (idempotent, Best-Effort). ",[534,2381,2382],{},"Nicht"," verdrahtet ist die ",[534,2385,2386],{},"Beziehungs","-Hälfte des sich selbst aufbauenden CRM-Verhaltens — das automatische Protokollieren extrahierter Fakten in ",[29,2389,46],{},", Stimmungsverläufe, Abschluss-\u002FPipeline-Status und die Erkennung von Jobwechseln aus E-Mail-Signaturen. ",[29,2392,46],{},[29,2394,2395],{},"apps\u002Fapi\u002Fconvex\u002Fcontacts\u002Frelationships.ts",") trägt heute nur die Quelle ",[29,2398,2135],{},", Beziehungen bleiben also von Betreiberinnen und Betreibern eingetragen. Betrachten Sie die Schicht zur Beziehungs- und CRM-Ableitung als Zielentwurf.",[2401,2402,2403],"style",{},"html pre.shiki code .sDN9O, html code.shiki .sDN9O{--shiki-default:#6A737D;--shiki-dark:#768390}html pre.shiki code .s7YZ4, html code.shiki .s7YZ4{--shiki-default:#D73A49;--shiki-dark:#F47067}html pre.shiki code .sOLd2, html code.shiki .sOLd2{--shiki-default:#6F42C1;--shiki-dark:#F69D50}html pre.shiki code .sYgZi, html code.shiki .sYgZi{--shiki-default:#24292E;--shiki-dark:#ADBAC7}html pre.shiki code .stnAF, html code.shiki .stnAF{--shiki-default:#E36209;--shiki-dark:#F69D50}html pre.shiki code .sPO5f, html code.shiki .sPO5f{--shiki-default:#6F42C1;--shiki-dark:#DCBDFB}html pre.shiki code .sviXB, html code.shiki .sviXB{--shiki-default:#005CC5;--shiki-dark:#6CB6FF}html pre.shiki code .s-HuK, html code.shiki .s-HuK{--shiki-default:#032F62;--shiki-dark:#96D0FF}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);}",{"title":81,"searchDepth":95,"depth":95,"links":2405},[2406,2409,2413,2414],{"id":63,"depth":95,"text":64,"children":2407},[2408],{"id":569,"depth":101,"text":570},{"id":765,"depth":95,"text":766,"children":2410},[2411,2412],{"id":1228,"depth":101,"text":1229},{"id":1559,"depth":101,"text":1560},{"id":1684,"depth":95,"text":1685},{"id":1932,"depth":95,"text":1933,"children":2415},[2416,2417,2418],{"id":1943,"depth":101,"text":1944},{"id":2125,"depth":101,"text":2126},{"id":2324,"depth":101,"text":2325},"Technische Architektur für Kanaladapter, vereinheitlichtes Messaging, die Vereinheitlichung von Kontaktidentitäten und den CRM-Hub.","md",{},"\u002Fvision\u002Fmulti-channel",{"title":6,"description":2419},"5.vision\u002F4.multi-channel","oKWPt4yo2-Dfmz3K_jCm8Hl2PN4ZDVYL5OHlKj--tKc",[2427,2431],{"title":2428,"path":2429,"stem":2430,"children":-1},"Wissensgraph","\u002Fvision\u002Fknowledge-graph","5.vision\u002F3.knowledge-graph",{"title":2432,"path":2433,"stem":2434,"children":-1},"Semantisches Dateisystem","\u002Fvision\u002Ffile-system","5.vision\u002F5.file-system",1786915111319]