From 0f19d591833ac0fc461f54897be046a40ddcb1de Mon Sep 17 00:00:00 2001 From: Ruben Fiszel Date: Sat, 16 May 2026 12:32:13 +0000 Subject: [PATCH] fix: don't re-pulse already-running jobs after they finish MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The catch-up pulse re-added a completed job to the active set if its start was within the (lagging) lookback window — even one we'd already animated the whole time it ran — keeping its edges lit ~a poll interval past completion (~5s after a 3.5s test). Track job ids seen in-flight and skip the pulse for them; it still fires for hops whose whole lifetime fell between two polls. Bound the set in lockstep with eventsById; cleared on dispose. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../AssetGraph/activeRunnables.svelte.ts | 23 +++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/frontend/src/lib/components/assets/AssetGraph/activeRunnables.svelte.ts b/frontend/src/lib/components/assets/AssetGraph/activeRunnables.svelte.ts index a085771fd9..fdf1866bfd 100644 --- a/frontend/src/lib/components/assets/AssetGraph/activeRunnables.svelte.ts +++ b/frontend/src/lib/components/assets/AssetGraph/activeRunnables.svelte.ts @@ -94,6 +94,11 @@ export function useActiveRunnableIds( { runs: number; lastStatus: RunStatus; lastTs: string } >() const countedJobIds = new Set() + // Job ids we've observed in-flight at least once. The catch-up pulse is + // only for jobs whose whole start→finish fell between two polls (never + // seen running); re-pulsing a job we already animated while it ran just + // keeps its edges lit ~one extra poll interval after it finished. + const seenInFlightJobIds = new Set() // jobId → latest known state of that job, for the activity log. Survives // `stop()` (log keeps history while idle); cleared by `dispose()`. const eventsById = new Map() @@ -144,10 +149,15 @@ export function useActiveRunnableIds( next.add(id) inFlightThisTick.add(id) anyInFlight = true + if (jobId) seenInFlightJobIds.add(jobId) } else { - // completed: catch-up — a hop that started after the last - // poll (and already finished) still gets one pulse. - if (startedTs && startedTs >= since) next.add(id) + // completed: catch-up — a hop whose whole lifetime fell + // between two polls (never observed in-flight) still gets + // one pulse. Jobs we already animated while running are + // NOT re-pulsed, else their edges linger ~a poll interval + // past completion. + if (startedTs && startedTs >= since && (!jobId || !seenInFlightJobIds.has(jobId))) + next.add(id) // Tally distinct completed jobs for the node badge — only // those since the graph was opened (older = pre-existing // history). Jobs come newest-first; only a strictly newer @@ -208,7 +218,11 @@ export function useActiveRunnableIds( if (sorted.length > MAX_EVENTS * 4) { for (const e of sorted.slice(MAX_EVENTS * 4)) eventsById.delete(e.id) countedJobIds.clear() - for (const id of eventsById.keys()) countedJobIds.add(id) + seenInFlightJobIds.clear() + for (const id of eventsById.keys()) { + countedJobIds.add(id) + seenInFlightJobIds.add(id) + } } const nextEvents = sorted.slice(0, MAX_EVENTS) if (!eventsEq(events, nextEvents)) events = nextEvents @@ -287,6 +301,7 @@ export function useActiveRunnableIds( stop() completedHistory.clear() countedJobIds.clear() + seenInFlightJobIds.clear() eventsById.clear() if (states.size > 0) states = new Map() if (events.length > 0) events = []