88 * - Windowed line storage (memory efficient)
99 */
1010
11- import { createSignal , createEffect , onCleanup } from "solid-js"
11+ import { createSignal , createEffect , onCleanup , on } from "solid-js"
1212import { AgentMonitorService } from "../../../../../agents/monitoring/monitor.js"
1313import type { AgentRecord } from "../../../../../agents/monitoring/types.js"
1414import { existsSync , statSync , openSync , readSync , closeSync , readFileSync } from "fs"
@@ -224,6 +224,38 @@ interface WindowedLinesState {
224224 startOffset : number
225225}
226226
227+ /**
228+ * Deduplicate new lines by checking for overlap with current lines
229+ * Returns the portion of newLines that don't already exist at the end of current
230+ */
231+ function deduplicateNewLines ( currentLines : string [ ] , newLines : string [ ] ) : string [ ] {
232+ if ( currentLines . length === 0 || newLines . length === 0 ) {
233+ return newLines
234+ }
235+
236+ // Check for overlap: find if any prefix of newLines matches the suffix of currentLines
237+ // Start with larger potential overlaps and work down
238+ const maxOverlap = Math . min ( currentLines . length , newLines . length )
239+
240+ for ( let overlapSize = maxOverlap ; overlapSize > 0 ; overlapSize -- ) {
241+ // Check if last 'overlapSize' lines of current match first 'overlapSize' of new
242+ let isMatch = true
243+ for ( let i = 0 ; i < overlapSize ; i ++ ) {
244+ const currentIdx = currentLines . length - overlapSize + i
245+ if ( currentLines [ currentIdx ] !== newLines [ i ] ) {
246+ isMatch = false
247+ break
248+ }
249+ }
250+ if ( isMatch ) {
251+ logDebug ( 'deduplicateNewLines: found overlap of %d lines, removing duplicates' , overlapSize )
252+ return newLines . slice ( overlapSize )
253+ }
254+ }
255+
256+ return newLines
257+ }
258+
227259/**
228260 * Update windowed lines with new data, trimming from front if needed
229261 * @param skipTrim - If true, don't trim lines (used when user has scrolled away)
@@ -246,8 +278,16 @@ function updateWindowedLines(
246278 }
247279 }
248280
249- // Append new lines
250- const allLines = [ ...current . lines , ...newLines ]
281+ // Deduplicate new lines before appending
282+ const dedupedNewLines = deduplicateNewLines ( current . lines , newLines )
283+
284+ if ( dedupedNewLines . length === 0 ) {
285+ // All new lines were duplicates
286+ return current
287+ }
288+
289+ // Append deduplicated new lines
290+ const allLines = [ ...current . lines , ...dedupedNewLines ]
251291
252292 // Trim from the front if exceeding window (unless skipTrim is true)
253293 if ( ! skipTrim && allLines . length > maxWindow ) {
@@ -388,11 +428,12 @@ export function useLogStream(
388428 }
389429 }
390430
391- createEffect ( ( ) => {
392- const agentId = opts . monitoringAgentId ( )
393- const agentStatus = opts . agentStatus ?.( )
394-
395- logDebug ( 'Effect triggered, agentId=%s status=%s' , agentId , agentStatus )
431+ // Use on() to explicitly track ONLY agentId changes, not status changes
432+ // Status changes are handled within the polling loop to avoid full re-initialization
433+ createEffect ( on (
434+ ( ) => opts . monitoringAgentId ( ) ,
435+ ( agentId ) => {
436+ logDebug ( 'Effect triggered, agentId=%s (status changes handled in polling loop)' , agentId )
396437
397438 if ( agentId === undefined ) {
398439 logDebug ( 'No agentId, resetting state' )
@@ -498,9 +539,10 @@ export function useLogStream(
498539 const thinking = extractLatestThinking ( fileLines )
499540 const currentFileSize = getFileSize ( logPath )
500541
501- // Update incremental state
542+ // Update incremental state - use RAW line count for consistency
543+ // The incremental read function uses raw line counts to detect new lines
502544 incrementalState . lastFileSize = currentFileSize
503- incrementalState . lastLineCount = filteredLines . length
545+ incrementalState . lastLineCount = fileLines . length
504546
505547 // Update windowed state
506548 const trimCount = Math . max ( 0 , filteredLines . length - maxWindow )
@@ -722,12 +764,10 @@ export function useLogStream(
722764 // Check if we need to adjust polling interval
723765 const newStatus = opts . agentStatus ?.( )
724766 if ( newStatus !== lastStatus ) {
725- // Immediate poll when transitioning TO running (from slow polling state)
726- if ( ( newStatus === 'running' || newStatus === 'retrying' ) &&
727- ( lastStatus === 'paused' || lastStatus === 'awaiting' || lastStatus === 'pending' || lastStatus === 'delegated' ) ) {
728- logDebug ( 'Status changed to running, triggering immediate poll' )
729- updateLogs ( logPath )
730- }
767+ // Status changed - just adjust the polling interval
768+ // Don't do an extra updateLogs here since we just did one above
769+ // The new polling interval will naturally handle faster updates
770+ logDebug ( 'Status changed from %s to %s, adjusting poll interval' , lastStatus , newStatus )
731771 lastStatus = newStatus
732772 updatePollingInterval ( logPath , newStatus )
733773 }
@@ -751,7 +791,7 @@ export function useLogStream(
751791 clearTimeout ( graceTimeout )
752792 }
753793 } )
754- } )
794+ } ) )
755795
756796 return {
757797 get lines ( ) {
0 commit comments