@@ -631,7 +631,7 @@ export async function postExecutionHistoryRoute(request: Request) {
631631}
632632
633633export async function getExecutionHistoryRunRoute (
634- _request : Request ,
634+ request : Request ,
635635 params : { runId ?: string | null | undefined } ,
636636) {
637637 const userId = await requireExecutionRouteUserId ( ) ;
@@ -644,6 +644,19 @@ export async function getExecutionHistoryRunRoute(
644644 return createJsonResponse ( { error : 'Missing runId parameter' } , 400 ) ;
645645 }
646646
647+ // `?tail=N` — last N events only (for failure hover previews). Full history
648+ // when omitted (paginated past the PostgREST 1000-row default).
649+ let tail : number | null = null ;
650+ try {
651+ const raw = new URL ( request . url ) . searchParams . get ( 'tail' ) ;
652+ if ( raw ) {
653+ const n = Number . parseInt ( raw , 10 ) ;
654+ if ( Number . isFinite ( n ) && n > 0 ) tail = Math . min ( n , 50 ) ;
655+ }
656+ } catch {
657+ tail = null ;
658+ }
659+
647660 const { data : run , error : runError } = await supabaseAdmin
648661 . from ( 'executions' )
649662 . select (
@@ -663,21 +676,18 @@ export async function getExecutionHistoryRunRoute(
663676 return createJsonResponse ( { error : 'Execution not found or access denied' } , 404 ) ;
664677 }
665678
666- // PostgREST/Supabase defaults to max 1000 rows. Deposit synthesis runs produce
667- // multi-thousand event streams (QA: refresh dropped the second half of a
668- // ~30m run and halved the clock because only the first 1000 events loaded).
669- // Page through the full event history in order.
670- const EVENT_PAGE_SIZE = 1000 ;
671679 const events : ExecutionEventRow [ ] = [ ] ;
672- let eventOffset = 0 ;
673- for ( ; ; ) {
680+ let eventsTruncated = false ;
681+
682+ if ( tail !== null ) {
683+ // Newest-first page, then reverse so clients get chronological order.
674684 const { data : page , error : eventsError } = await supabaseAdmin
675685 . from ( 'execution_events' )
676686 . select ( 'id, run_id, event_type, event_data, created_at, agent_name, phase' )
677687 . eq ( 'run_id' , runId )
678- . order ( 'created_at' , { ascending : true } )
679- . order ( 'id' , { ascending : true } )
680- . range ( eventOffset , eventOffset + EVENT_PAGE_SIZE - 1 ) ;
688+ . order ( 'created_at' , { ascending : false } )
689+ . order ( 'id' , { ascending : false } )
690+ . range ( 0 , tail - 1 ) ;
681691
682692 if ( eventsError ) {
683693 return createJsonResponse (
@@ -686,25 +696,56 @@ export async function getExecutionHistoryRunRoute(
686696 ) ;
687697 }
688698 const batch = Array . isArray ( page ) ? ( page as ExecutionEventRow [ ] ) : [ ] ;
689- events . push ( ...batch ) ;
690- if ( batch . length < EVENT_PAGE_SIZE ) break ;
691- eventOffset += EVENT_PAGE_SIZE ;
692- // Hard ceiling so a runaway table cannot unbounded-read the API process.
693- if ( eventOffset >= 50_000 ) break ;
699+ events . push ( ...batch . reverse ( ) ) ;
700+ } else {
701+ // PostgREST/Supabase defaults to max 1000 rows. Deposit synthesis runs produce
702+ // multi-thousand event streams (QA: refresh dropped the second half of a
703+ // ~30m run and halved the clock because only the first 1000 events loaded).
704+ // Page through the full event history in order.
705+ const EVENT_PAGE_SIZE = 1000 ;
706+ let eventOffset = 0 ;
707+ for ( ; ; ) {
708+ const { data : page , error : eventsError } = await supabaseAdmin
709+ . from ( 'execution_events' )
710+ . select ( 'id, run_id, event_type, event_data, created_at, agent_name, phase' )
711+ . eq ( 'run_id' , runId )
712+ . order ( 'created_at' , { ascending : true } )
713+ . order ( 'id' , { ascending : true } )
714+ . range ( eventOffset , eventOffset + EVENT_PAGE_SIZE - 1 ) ;
715+
716+ if ( eventsError ) {
717+ return createJsonResponse (
718+ { error : toErrorMessage ( eventsError , 'Failed to fetch execution event history' ) } ,
719+ 500 ,
720+ ) ;
721+ }
722+ const batch = Array . isArray ( page ) ? ( page as ExecutionEventRow [ ] ) : [ ] ;
723+ events . push ( ...batch ) ;
724+ if ( batch . length < EVENT_PAGE_SIZE ) break ;
725+ eventOffset += EVENT_PAGE_SIZE ;
726+ // Hard ceiling so a runaway table cannot unbounded-read the API process.
727+ if ( eventOffset >= 50_000 ) {
728+ eventsTruncated = true ;
729+ break ;
730+ }
731+ }
694732 }
695733
696734 const normalizedRun = normalizeExecutionHistoryRow ( run ) ;
697- const terminalJournal = await fetchTerminalJournalReadback ( run . id , normalizedRun ) ;
735+ // Skip heavy journal join for lightweight tail previews (table hover).
736+ const terminalJournal =
737+ tail !== null ? null : await fetchTerminalJournalReadback ( run . id , normalizedRun ) ;
698738
699739 return createJsonResponse ( {
700740 run : {
701741 ...normalizedRun ,
702- terminal_journal : terminalJournal ,
742+ ... ( terminalJournal ? { terminal_journal : terminalJournal } : { } ) ,
703743 } ,
704744 events : events . map ( normalizeExecutionEventRow ) ,
705745 eventCount : events . length ,
706- eventsTruncated : eventOffset >= 50_000 ,
707- terminal_journal : terminalJournal ,
746+ eventsTruncated,
747+ tail,
748+ ...( terminalJournal ? { terminal_journal : terminalJournal } : { } ) ,
708749 } ) ;
709750}
710751
0 commit comments