@@ -249,6 +249,20 @@ function useAgentSessionMessages({
249249 [ initialMessages ]
250250 ) ;
251251
252+ // The snapshot URL is re-signed by the loader on every navigation
253+ // (tab switches in the inspector pane re-run the session loader),
254+ // which would otherwise re-trigger the subscription effect below
255+ // and replay post-snapshot `.out` chunks on top of the messages we
256+ // already accumulated — duplicating any assistant content that
257+ // lives past `snapshot.lastOutEventId` (e.g., a canceled run whose
258+ // turn never completed). Hold the URL behind a ref and keep it
259+ // out of the effect's deps so the effect runs exactly once per
260+ // mount.
261+ const snapshotUrlRef = useRef ( snapshotPresignedUrl ) ;
262+ useEffect ( ( ) => {
263+ snapshotUrlRef . current = snapshotPresignedUrl ;
264+ } , [ snapshotPresignedUrl ] ) ;
265+
252266 // `pendingRef` is the authoritative, eagerly-updated message state:
253267 // chunks mutate this synchronously as they arrive. A throttled flush
254268 // copies it into React state so UI updates are capped at ~10x/sec.
@@ -309,9 +323,10 @@ function useAgentSessionMessages({
309323 * have never completed a turn).
310324 */
311325 const loadSnapshot = async ( ) : Promise < string | undefined > => {
312- if ( ! snapshotPresignedUrl ) return undefined ;
326+ const url = snapshotUrlRef . current ;
327+ if ( ! url ) return undefined ;
313328 try {
314- const resp = await fetch ( snapshotPresignedUrl , { signal : abort . signal } ) ;
329+ const resp = await fetch ( url , { signal : abort . signal } ) ;
315330 if ( ! resp . ok ) return undefined ;
316331 const json = ( await resp . json ( ) ) as unknown ;
317332 const parsed = ChatSnapshotV1Schema . safeParse ( json ) ;
@@ -550,7 +565,12 @@ function useAgentSessionMessages({
550565 pendingTimerRef . current = null ;
551566 }
552567 } ;
553- } , [ sessionId , apiOrigin , orgSlug , projectSlug , envSlug , snapshotPresignedUrl ] ) ;
568+ // `snapshotPresignedUrl` is intentionally NOT in this dep list — see
569+ // `snapshotUrlRef` above for the reasoning. Including it caused the
570+ // subscription to tear down + replay on every inspector tab click,
571+ // which appended duplicate parts to any assistant message whose
572+ // chunks lived past `snapshot.lastOutEventId`.
573+ } , [ sessionId , apiOrigin , orgSlug , projectSlug , envSlug ] ) ;
554574
555575 return useMemo ( ( ) => {
556576 const timestamps = timestampsRef . current ;
0 commit comments