@@ -218,6 +218,171 @@ const driftRoutes: FastifyPluginAsyncTypebox = async (fastify) => {
218218 }
219219 ) ;
220220
221+ // Delete a conversation and everything belonging to it
222+ fastify . delete (
223+ '/conversations/:conversationId' ,
224+ {
225+ schema : {
226+ description :
227+ 'Delete a conversation and all of its branches, messages, and facts. Use this to implement client "Clear history" flows. Returns 404 when the conversation does not exist (or belongs to another user).' ,
228+ tags : [ 'Drift' ] ,
229+ params : Type . Object ( {
230+ conversationId : Type . String ( ) ,
231+ } ) ,
232+ response : {
233+ 200 : Type . Object ( {
234+ success : Type . Literal ( true ) ,
235+ data : Type . Object ( {
236+ deletedBranches : Type . Number ( ) ,
237+ deletedMessages : Type . Number ( ) ,
238+ deletedFacts : Type . Number ( ) ,
239+ } ) ,
240+ } ) ,
241+ 404 : Type . Object ( {
242+ success : Type . Literal ( false ) ,
243+ error : Type . Object ( { message : Type . String ( ) } ) ,
244+ } ) ,
245+ } ,
246+ } ,
247+ } ,
248+ async ( request , reply ) => {
249+ const { conversationId } = request . params ;
250+ const userId = request . userId ?? '' ;
251+
252+ const conversation = await fastify . prisma . conversation . findUnique ( {
253+ where : { userId_id : { userId, id : conversationId } } ,
254+ select : { id : true } ,
255+ } ) ;
256+
257+ if ( ! conversation ) {
258+ return reply . status ( 404 ) . send ( {
259+ success : false ,
260+ error : { message : 'Conversation not found' } ,
261+ } ) ;
262+ }
263+
264+ // Count what's about to go so we can report it back
265+ const [ deletedBranches , deletedMessages , deletedFacts ] = await Promise . all ( [
266+ fastify . prisma . branch . count ( { where : { userId, conversationId } } ) ,
267+ fastify . prisma . message . count ( { where : { userId, conversationId } } ) ,
268+ fastify . prisma . fact . count ( {
269+ where : { branch : { userId, conversationId } } ,
270+ } ) ,
271+ ] ) ;
272+
273+ // Atomic cascade. Prisma cascades Conversation -> Branches/Clusters/Messages
274+ // and Branch -> Facts/ClusterMemberships/TailRoutes/MergeEdges. TailRoutes
275+ // anchored to these messages (anchorMessageId has no cascade) are cleared
276+ // explicitly; DriftLog has no FK and is cleaned by conversationId.
277+ await fastify . prisma . $transaction ( async ( tx ) => {
278+ const msgs = await tx . message . findMany ( {
279+ where : { userId, conversationId } ,
280+ select : { id : true } ,
281+ } ) ;
282+ const messageIds = msgs . map ( ( m ) => m . id ) ;
283+ if ( messageIds . length > 0 ) {
284+ await tx . tailRoute . deleteMany ( {
285+ where : { anchorMessageId : { in : messageIds } } ,
286+ } ) ;
287+ }
288+ await tx . driftLog . deleteMany ( { where : { conversationId } } ) ;
289+ await tx . conversation . delete ( {
290+ where : { userId_id : { userId, id : conversationId } } ,
291+ } ) ;
292+ } ) ;
293+
294+ return reply . send ( {
295+ success : true ,
296+ data : { deletedBranches, deletedMessages, deletedFacts } ,
297+ } ) ;
298+ }
299+ ) ;
300+
301+ // Delete a single branch. Children cascade: deleting a branch also deletes
302+ // every descendant branch plus their messages and facts. Document this
303+ // behaviour in the SDK / API reference.
304+ fastify . delete (
305+ '/branches/:branchId' ,
306+ {
307+ schema : {
308+ description :
309+ 'Delete a branch and every descendant branch (cascading), including their messages and facts. Returns 404 when the branch does not exist or belongs to another user.' ,
310+ tags : [ 'Drift' ] ,
311+ params : Type . Object ( {
312+ branchId : Type . String ( ) ,
313+ } ) ,
314+ response : {
315+ 200 : Type . Object ( {
316+ success : Type . Literal ( true ) ,
317+ data : Type . Object ( {
318+ deletedMessages : Type . Number ( ) ,
319+ deletedFacts : Type . Number ( ) ,
320+ } ) ,
321+ } ) ,
322+ 404 : Type . Object ( {
323+ success : Type . Literal ( false ) ,
324+ error : Type . Object ( { message : Type . String ( ) } ) ,
325+ } ) ,
326+ } ,
327+ } ,
328+ } ,
329+ async ( request , reply ) => {
330+ const { branchId } = request . params ;
331+ const userId = request . userId ?? '' ;
332+
333+ const branch = await fastify . prisma . branch . findUnique ( {
334+ where : { id : branchId } ,
335+ select : { userId : true } ,
336+ } ) ;
337+
338+ // 404 covers both "does not exist" and "belongs to someone else" to avoid
339+ // leaking branch ID existence across tenants.
340+ if ( ! branch || branch . userId !== userId ) {
341+ return reply . status ( 404 ) . send ( {
342+ success : false ,
343+ error : { message : 'Branch not found' } ,
344+ } ) ;
345+ }
346+
347+ // Walk the subtree rooted at branchId via recursive CTE, scoped to user.
348+ const rows = await fastify . prisma . $queryRaw < { id : string } [ ] > `
349+ WITH RECURSIVE branch_tree AS (
350+ SELECT id FROM branches WHERE id = ${ branchId } AND "userId" = ${ userId }
351+ UNION
352+ SELECT b.id FROM branches b
353+ INNER JOIN branch_tree bt ON b."parentId" = bt.id
354+ WHERE b."userId" = ${ userId }
355+ )
356+ SELECT id FROM branch_tree;
357+ ` ;
358+ const branchIds = rows . map ( ( r ) => r . id ) ;
359+
360+ const [ deletedMessages , deletedFacts ] = await Promise . all ( [
361+ fastify . prisma . message . count ( { where : { branchId : { in : branchIds } } } ) ,
362+ fastify . prisma . fact . count ( { where : { branchId : { in : branchIds } } } ) ,
363+ ] ) ;
364+
365+ await fastify . prisma . $transaction ( async ( tx ) => {
366+ const msgs = await tx . message . findMany ( {
367+ where : { branchId : { in : branchIds } } ,
368+ select : { id : true } ,
369+ } ) ;
370+ const messageIds = msgs . map ( ( m ) => m . id ) ;
371+ if ( messageIds . length > 0 ) {
372+ await tx . tailRoute . deleteMany ( {
373+ where : { anchorMessageId : { in : messageIds } } ,
374+ } ) ;
375+ }
376+ await tx . branch . deleteMany ( { where : { id : { in : branchIds } } } ) ;
377+ } ) ;
378+
379+ return reply . send ( {
380+ success : true ,
381+ data : { deletedMessages, deletedFacts } ,
382+ } ) ;
383+ }
384+ ) ;
385+
221386 // Health check
222387 fastify . get (
223388 '/health' ,
0 commit comments