@@ -42,6 +42,8 @@ type WireChunkResponse = {
4242 events : WireChunkEvent [ ] ;
4343 nextCursor : TraceChunkCursor | null ;
4444 hasMore : boolean ;
45+ // The server capped the result: errors-only matches, or paging under the emergency cap.
46+ isTruncated ?: boolean ;
4547 // Server time (ms) taken before the page was read.
4648 readAt ?: number ;
4749} ;
@@ -53,7 +55,6 @@ type ProgressiveMeta = {
5355 hasMore : boolean ;
5456 buildOptions : BuildTraceViewOptions ;
5557 showDebug : boolean ;
56- totalSpans ?: number ;
5758 maxSpans ?: number ;
5859 liveTailEnabled ?: boolean ;
5960 // Server time (ms) before the first chunk was read; the first tail reads back to it.
@@ -79,6 +80,8 @@ export type ProgressiveTraceState = {
7980 linkedRunIdBySpanId : Record < string , string > ;
8081 isComplete : boolean ;
8182 isTruncated : boolean ;
83+ // Errors-only matches were capped; separate from `isTruncated`, which stops live reload.
84+ errorsTruncated : boolean ;
8285 loadFailed : boolean ;
8386} ;
8487
@@ -96,6 +99,7 @@ function initialState(trace: ProgressiveTraceInput): ProgressiveTraceState {
9699 linkedRunIdBySpanId : trace . linkedRunIdBySpanId ?? { } ,
97100 isComplete : ! trace . progressive ?. hasMore ,
98101 isTruncated : false ,
102+ errorsTruncated : false ,
99103 loadFailed : false ,
100104 } ;
101105}
@@ -192,6 +196,8 @@ export function useProgressiveTrace(
192196 const errorsFetchedRef = useRef ( false ) ;
193197 // The deep-link payload already merged, so a kept tree only merges new ones.
194198 const mergedSupplementaryRef = useRef < WireChunkEvent [ ] | null > ( null ) ;
199+ // The first chunk already merged; a same-run revalidate merges only a new one.
200+ const mergedFirstEventsRef = useRef < WireChunkEvent [ ] | null > ( null ) ;
195201 // Assembler the in-flight tail targets; a stale tail never touches the current one.
196202 const tailingRef = useRef < TraceChunkAssembler | null > ( null ) ;
197203 // Timestamp of the last tail read, for the size-aware cadence governor.
@@ -220,6 +226,7 @@ export function useProgressiveTrace(
220226 linkedRunIdBySpanId : view . linkedRunIdBySpanId ,
221227 isComplete : prev . isComplete ,
222228 isTruncated : prev . isTruncated ,
229+ errorsTruncated : prev . errorsTruncated ,
223230 loadFailed : prev . loadFailed ,
224231 } ) ) ;
225232 } , [ ] ) ;
@@ -249,6 +256,7 @@ export function useProgressiveTrace(
249256
250257 const assembler = new TraceChunkAssembler ( ) ;
251258 assembler . mergeChunk ( meta . firstEvents . map ( toChunkEvent ) ) ;
259+ mergedFirstEventsRef . current = meta . firstEvents ;
252260 if ( meta . supplementaryFirstEvents ?. length ) {
253261 assembler . mergeChunk ( meta . supplementaryFirstEvents . map ( toChunkEvent ) , {
254262 source : "deeplink" ,
@@ -338,7 +346,7 @@ export function useProgressiveTrace(
338346 requestRebuild ( ) ;
339347 cursor = data . hasMore ? data . nextCursor : null ;
340348
341- if ( assembler . size > maxSpans ) {
349+ if ( assembler . size > maxSpans || data . isTruncated ) {
342350 markComplete ( true ) ;
343351 return ;
344352 }
@@ -360,9 +368,12 @@ export function useProgressiveTrace(
360368 } ;
361369 } , [ identity , chunkPath , retryGeneration , rebuild ] ) ;
362370
371+ // Every chunk is loaded, so the client-side filter already sees every error.
372+ const fullyLoaded = state . isComplete && ! state . loadFailed && ! state . isTruncated ;
373+
363374 useEffect ( ( ) => {
364375 const meta = latestTraceRef . current . progressive ;
365- if ( ! errorsOnly || ! meta || errorsFetchedRef . current ) {
376+ if ( ! errorsOnly || ! meta || errorsFetchedRef . current || fullyLoaded ) {
366377 return ;
367378 }
368379
@@ -374,12 +385,15 @@ export function useProgressiveTrace(
374385 errorsFetchedRef . current = true ;
375386 assembler . mergeChunk ( data . events . map ( toChunkEvent ) , { source : "errors" } ) ;
376387 rebuild ( meta , assembler ) ;
388+ if ( data . isTruncated ) {
389+ setState ( ( prev ) => ( prev . errorsTruncated ? prev : { ...prev , errorsTruncated : true } ) ) ;
390+ }
377391 } ) ( ) ;
378392
379393 return ( ) => {
380394 cancelled = true ;
381395 } ;
382- } , [ errorsOnly , identity , chunkPath , retryGeneration , rebuild ] ) ;
396+ } , [ errorsOnly , fullyLoaded , identity , chunkPath , retryGeneration , rebuild ] ) ;
383397
384398 const runTail = useCallback ( ( ) => {
385399 const meta = latestTraceRef . current . progressive ;
@@ -477,6 +491,20 @@ export function useProgressiveTrace(
477491 rebuild ( meta , assembler ) ;
478492 } , [ supplementary , rebuild ] ) ;
479493
494+ // A kept tree merges a revalidated first chunk, e.g. the root's final row after the
495+ // tail stopped at the view ceiling.
496+ const firstEvents = progressive ?. firstEvents ;
497+ useEffect ( ( ) => {
498+ const meta = latestTraceRef . current . progressive ;
499+ const assembler = assemblerRef . current ;
500+ if ( ! meta ?. liveTailEnabled || ! assembler || ! firstEvents ) return ;
501+ if ( firstEvents === mergedFirstEventsRef . current ) return ;
502+ mergedFirstEventsRef . current = firstEvents ;
503+ assembler . mergeChunk ( firstEvents . map ( toChunkEvent ) , { source : "revalidate" } ) ;
504+ if ( ! assembler . changedSinceRender ) return ;
505+ rebuild ( meta , assembler ) ;
506+ } , [ firstEvents , rebuild ] ) ;
507+
480508 // Stable trigger so the route's SSE effect doesn't re-fire on chunkPath changes.
481509 useEffect ( ( ) => {
482510 tailLiveRef . current = runTail ;
0 commit comments