@@ -40,6 +40,7 @@ import {
4040 type QueryHistoryRecord ,
4141 type RunRecord ,
4242 type RunEventRecord ,
43+ type SessionBranchRecord ,
4344 type SessionRecord ,
4445 type UserRecord
4546} from "@datafoundry/metadata" ;
@@ -53,6 +54,14 @@ import { resolveEffectiveRunConfig } from "./run-input.js";
5354import { handleCapabilitiesRequest } from "./routes/capabilities.js" ;
5455import type { ConfigApiContext , ConfigApiResponse } from "./routes/types.js" ;
5556import { sessionTitleDto } from "./session-title.js" ;
57+ import {
58+ createSessionBranch ,
59+ latestVisibleConversationSummary ,
60+ listConversationBranchOptions ,
61+ listVisibleConversationMessages ,
62+ resolveSessionLineage ,
63+ type ConversationBranchOption
64+ } from "./session-branching.js" ;
5665import { readMultipartFiles , readMultipartUpload } from "./upload-parser.js" ;
5766
5867const MAX_JSON_BODY_BYTES = 1024 * 1024 ;
@@ -359,6 +368,22 @@ const handleSessionRequest = async (
359368 } ) ;
360369 return ok ( sessionTitleDto ( session ) ) ;
361370 }
371+ if ( action === "branches" && request . method === "POST" ) {
372+ const body = await readJsonBody ( request ) ;
373+ const runId = stringValue ( body . runId ?? body . run_id ) ;
374+ if ( ! runId ) {
375+ throw new Error ( "RUN_ID_REQUIRED" ) ;
376+ }
377+ const title = stringValue ( body . title ) ;
378+ const created = createSessionBranch ( {
379+ activeSessionId : sessionId ,
380+ metadataStore : context . metadataStore ,
381+ runId,
382+ ...( title ? { title } : { } ) ,
383+ userId : context . userId
384+ } ) ;
385+ return ok ( sessionBranchCreatedDto ( created ) , 201 ) ;
386+ }
362387 if ( action !== "conversation" ) {
363388 return methodNotAllowed ( ) ;
364389 }
@@ -367,16 +392,24 @@ const handleSessionRequest = async (
367392 }
368393
369394 const session = context . metadataStore . sessions . get ( { user_id : context . userId , session_id : sessionId } ) ;
395+ const lineage = resolveSessionLineage ( {
396+ metadataStore : context . metadataStore ,
397+ sessionId,
398+ userId : context . userId
399+ } ) ;
370400 const requestUrl = new URL ( request . url ?? "/" , "http://127.0.0.1" ) ;
371401 const limit = clampInteger ( Number . parseInt ( requestUrl . searchParams . get ( "limit" ) ?? "" , 10 ) , 1 , 200 , 80 ) ;
372- const messages = context . metadataStore . conversationMessages . listRecent ( {
373- user_id : context . userId ,
374- session_id : sessionId ,
375- limit
402+ const messages = listVisibleConversationMessages ( {
403+ lineage,
404+ limit,
405+ metadataStore : context . metadataStore ,
406+ userId : context . userId
376407 } ) ;
377- const latestSummary = context . metadataStore . conversationSummaries . latest ( {
378- user_id : context . userId ,
379- session_id : sessionId
408+ const latestSummary = latestVisibleConversationSummary ( {
409+ lineage,
410+ metadataStore : context . metadataStore ,
411+ sessionId,
412+ userId : context . userId
380413 } ) ;
381414 const runIds = [ ...new Set ( [
382415 ...messages . map ( ( message ) => message . run_id ) ,
@@ -386,11 +419,19 @@ const handleSessionRequest = async (
386419 runId,
387420 events : context . metadataStore . runEvents . listByRun ( { user_id : context . userId , run_id : runId } )
388421 } ) ) ;
422+ const visiblePositions = new Map ( messages . map ( ( message , index ) => [ message . id , index + 1 ] ) ) ;
389423 const checkpoints = runCheckpointDtos ( {
390424 context,
391425 messages,
392426 runEventGroups,
393- runIds
427+ runIds,
428+ visiblePositions
429+ } ) ;
430+ const branchOptions = listConversationBranchOptions ( {
431+ lineage,
432+ metadataStore : context . metadataStore ,
433+ sessionId,
434+ userId : context . userId
394435 } ) ;
395436 const pendingInteractions = context . metadataStore . interactions . listPendingBySession ( {
396437 user_id : context . userId ,
@@ -402,10 +443,12 @@ const handleSessionRequest = async (
402443 title : session . title ?? "" ,
403444 titleSource : session . title_source ?? "fallback" ,
404445 updatedAt : session . updated_at ,
405- messages : messages . map ( conversationMessageDto ) ,
446+ messages : messages . map ( ( message , index ) => conversationMessageDto ( message , index + 1 ) ) ,
406447 ...( latestSummary ? { summary : conversationSummaryDto ( latestSummary ) } : { } ) ,
407448 runEventRefs : runEventGroups . map ( ( { runId, events } ) => runEventRefDto ( runId , events ) ) ,
408449 ...( checkpoints . length > 0 ? { checkpoints } : { } ) ,
450+ ...( lineage . branch ? { branch : sessionBranchDto ( lineage . branch , session ) } : { } ) ,
451+ ...( branchOptions . length > 0 ? { branches : branchOptions . map ( conversationBranchOptionDto ) } : { } ) ,
409452 toolCalls : runEventGroups . flatMap ( ( { runId, events } ) => toolCallPairDtos ( runId , events ) ) ,
410453 pendingInteractions : pendingInteractions . map ( pendingInteractionDto ) ,
411454 restorableCustomEvents : runEventGroups . flatMap ( ( { runId, events } ) =>
@@ -1616,14 +1659,14 @@ const handleArtifactRequest = async (
16161659 } ) ;
16171660} ;
16181661
1619- const conversationMessageDto = ( message : ConversationMessageRecord ) : Record < string , unknown > => ( {
1662+ const conversationMessageDto = ( message : ConversationMessageRecord , visiblePosition = message . position ) : Record < string , unknown > => ( {
16201663 id : message . id ,
16211664 runId : message . run_id ,
16221665 role : message . role ,
16231666 source : message . source ,
16241667 ...( message . message_id ? { messageId : message . message_id } : { } ) ,
16251668 contentText : message . content_text ,
1626- position : message . position ,
1669+ position : visiblePosition ,
16271670 createdAt : message . created_at
16281671} ) ;
16291672
@@ -1637,6 +1680,38 @@ const sessionListDto = (session: SessionRecord): Record<string, unknown> => ({
16371680 lastMessageAt : session . last_message_at ?? session . updated_at
16381681} ) ;
16391682
1683+ const sessionBranchCreatedDto = ( input : { branch : SessionBranchRecord ; session : SessionRecord } ) : Record < string , unknown > => ( {
1684+ ...sessionBranchDto ( input . branch , input . session ) ,
1685+ session : sessionListDto ( input . session )
1686+ } ) ;
1687+
1688+ const sessionBranchDto = (
1689+ branch : SessionBranchRecord ,
1690+ session ?: SessionRecord
1691+ ) : Record < string , unknown > => ( {
1692+ id : branch . id ,
1693+ sessionId : branch . child_session_id ,
1694+ threadId : branch . child_session_id ,
1695+ parentSessionId : branch . parent_session_id ,
1696+ rootSessionId : branch . root_session_id ,
1697+ forkRunId : branch . fork_run_id ,
1698+ forkMessageEndPosition : branch . fork_message_end_position ,
1699+ createdAt : branch . created_at ,
1700+ ...( session ?. title ? { title : session . title } : { } )
1701+ } ) ;
1702+
1703+ const conversationBranchOptionDto = ( branch : ConversationBranchOption ) : Record < string , unknown > => ( {
1704+ sessionId : branch . sessionId ,
1705+ threadId : branch . sessionId ,
1706+ parentSessionId : branch . parentSessionId ,
1707+ rootSessionId : branch . rootSessionId ,
1708+ forkRunId : branch . forkRunId ,
1709+ forkMessageEndPosition : branch . forkMessageEndPosition ,
1710+ isOriginal : branch . isOriginal ,
1711+ createdAt : branch . createdAt ,
1712+ ...( branch . title ? { title : branch . title } : { } )
1713+ } ) ;
1714+
16401715const queryHistoryDto = ( record : QueryHistoryRecord ) : Record < string , unknown > => ( {
16411716 id : record . id ,
16421717 sessionId : record . session_id ,
@@ -1689,6 +1764,7 @@ const runCheckpointDtos = (input: {
16891764 messages : ConversationMessageRecord [ ] ;
16901765 runEventGroups : Array < { events : RunEventRecord [ ] ; runId : string } > ;
16911766 runIds : string [ ] ;
1767+ visiblePositions ?: Map < string , number > ;
16921768} ) : Record < string , unknown > [ ] => {
16931769 const eventsByRun = new Map ( input . runEventGroups . map ( ( group ) => [ group . runId , group . events ] ) ) ;
16941770 return input . runIds . flatMap ( ( runId ) => {
@@ -1703,7 +1779,8 @@ const runCheckpointDtos = (input: {
17031779 runCheckpointDto ( {
17041780 events : eventsByRun . get ( runId ) ?? [ ] ,
17051781 messages : input . messages . filter ( ( message ) => message . run_id === runId ) ,
1706- run
1782+ run,
1783+ ...( input . visiblePositions ? { visiblePositions : input . visiblePositions } : { } )
17071784 } )
17081785 ] ;
17091786 } ) ;
@@ -1713,8 +1790,9 @@ const runCheckpointDto = (input: {
17131790 events : RunEventRecord [ ] ;
17141791 messages : ConversationMessageRecord [ ] ;
17151792 run : RunRecord ;
1793+ visiblePositions ?: Map < string , number > ;
17161794} ) : Record < string , unknown > => {
1717- const positions = input . messages . map ( ( message ) => message . position ) ;
1795+ const positions = input . messages . map ( ( message ) => input . visiblePositions ?. get ( message . id ) ?? message . position ) ;
17181796 const firstEvent = input . events [ 0 ] ;
17191797 const lastEvent = input . events . at ( - 1 ) ;
17201798 return {
@@ -2670,6 +2748,18 @@ const errorResponse = (error: unknown): ConfigApiResponse => {
26702748 if ( message . startsWith ( "REVISION_CONFLICT" ) ) {
26712749 return fail ( 409 , "REVISION_CONFLICT" , message ) ;
26722750 }
2751+ if ( message . startsWith ( "RUN_NOT_BRANCHABLE" ) ) {
2752+ return fail ( 409 , "RUN_NOT_BRANCHABLE" , message ) ;
2753+ }
2754+ if ( message . startsWith ( "RUN_NOT_VISIBLE" ) ) {
2755+ return fail ( 404 , "RESOURCE_NOT_FOUND" , message ) ;
2756+ }
2757+ if ( message . startsWith ( "SESSION_BRANCH_CYCLE" ) ) {
2758+ return fail ( 409 , "REVISION_CONFLICT" , message ) ;
2759+ }
2760+ if ( message . startsWith ( "SESSION_BRANCH_PARENT_NOT_VISIBLE" ) ) {
2761+ return fail ( 404 , "RESOURCE_NOT_FOUND" , message ) ;
2762+ }
26732763 if ( message . includes ( "NOT_FOUND" ) || message . includes ( "not found" ) ) {
26742764 return fail ( 404 , "RESOURCE_NOT_FOUND" , message ) ;
26752765 }
0 commit comments