@@ -46,9 +46,15 @@ type PairingQrExpiryNotice = {
4646 title : string ;
4747 reason : string ;
4848} ;
49+ type PairingQrExpiryRefreshTimer = {
50+ expiresAtMs : number ;
51+ onRequestUpdate : ( ) => void ;
52+ timer : ReturnType < typeof setTimeout > ;
53+ } ;
4954
5055const assistantAttachmentAvailabilityCache = new Map < string , AssistantAttachmentAvailability > ( ) ;
5156const assistantAttachmentRefreshTimers = new Map < string , ReturnType < typeof setTimeout > > ( ) ;
57+ const pairingQrExpiryRefreshTimers = new Map < string , PairingQrExpiryRefreshTimer > ( ) ;
5258const ASSISTANT_ATTACHMENT_UNAVAILABLE_RETRY_MS = 5_000 ;
5359const ASSISTANT_ATTACHMENT_MEDIA_TICKET_REFRESH_SKEW_MS = 30_000 ;
5460const PAIRING_QR_EXPIRED_NOTICE : PairingQrExpiryNotice = {
@@ -114,6 +120,10 @@ export function resetAssistantAttachmentAvailabilityCacheForTest() {
114120 clearTimeout ( timer ) ;
115121 }
116122 assistantAttachmentRefreshTimers . clear ( ) ;
123+ for ( const { timer } of pairingQrExpiryRefreshTimers . values ( ) ) {
124+ clearTimeout ( timer ) ;
125+ }
126+ pairingQrExpiryRefreshTimers . clear ( ) ;
117127 for ( const blobUrl of managedImageBlobUrlResolvedCache . values ( ) ) {
118128 URL . revokeObjectURL ( blobUrl ) ;
119129 }
@@ -383,6 +393,71 @@ function extractPairingQrExpiryNotices(
383393 return notices ;
384394}
385395
396+ function resolveNearestFuturePairingQrExpiresAtMs (
397+ message : unknown ,
398+ nowMs = Date . now ( ) ,
399+ ) : number | undefined {
400+ const m = message as Record < string , unknown > ;
401+ const content = m . content ;
402+ if ( ! Array . isArray ( content ) ) {
403+ return undefined ;
404+ }
405+ let nearestExpiresAtMs : number | undefined ;
406+ for ( const block of content ) {
407+ if ( ! block || typeof block !== "object" ) {
408+ continue ;
409+ }
410+ const b = block as Record < string , unknown > ;
411+ if ( b . type !== "openclaw_pairing_qr" ) {
412+ continue ;
413+ }
414+ const expiresAtMs = readPairingQrExpiresAtMs ( b ) ;
415+ if ( expiresAtMs === undefined || expiresAtMs <= nowMs ) {
416+ continue ;
417+ }
418+ nearestExpiresAtMs =
419+ nearestExpiresAtMs === undefined ? expiresAtMs : Math . min ( nearestExpiresAtMs , expiresAtMs ) ;
420+ }
421+ return nearestExpiresAtMs ;
422+ }
423+
424+ function clearPairingQrExpiryRefreshTimer ( messageKey : string ) {
425+ const existing = pairingQrExpiryRefreshTimers . get ( messageKey ) ;
426+ if ( ! existing ) {
427+ return ;
428+ }
429+ clearTimeout ( existing . timer ) ;
430+ pairingQrExpiryRefreshTimers . delete ( messageKey ) ;
431+ }
432+
433+ function schedulePairingQrExpiryRefresh (
434+ messageKey : string ,
435+ message : unknown ,
436+ onRequestUpdate : ( ( ) => void ) | undefined ,
437+ ) {
438+ const nowMs = Date . now ( ) ;
439+ const expiresAtMs = resolveNearestFuturePairingQrExpiresAtMs ( message , nowMs ) ;
440+ const existing = pairingQrExpiryRefreshTimers . get ( messageKey ) ;
441+ if ( ! expiresAtMs || ! onRequestUpdate ) {
442+ if ( existing ) {
443+ clearPairingQrExpiryRefreshTimer ( messageKey ) ;
444+ }
445+ return ;
446+ }
447+ if ( existing ?. expiresAtMs === expiresAtMs && existing . onRequestUpdate === onRequestUpdate ) {
448+ return ;
449+ }
450+ clearPairingQrExpiryRefreshTimer ( messageKey ) ;
451+ const timer = setTimeout (
452+ ( ) => {
453+ pairingQrExpiryRefreshTimers . delete ( messageKey ) ;
454+ onRequestUpdate ( ) ;
455+ } ,
456+ Math . max ( 0 , expiresAtMs - nowMs ) ,
457+ ) ;
458+ pairingQrExpiryRefreshTimers . set ( messageKey , { expiresAtMs, onRequestUpdate, timer } ) ;
459+ }
460+
386461function extractTranscriptAttachments ( message : unknown ) : AttachmentItem [ ] {
387462 const attachments : AttachmentItem [ ] = [ ] ;
388463 for ( const { path : mediaPath , mediaType } of extractTranscriptMediaEntries ( message ) ) {
@@ -1735,6 +1810,7 @@ function renderGroupedMessage(
17351810 authToken : opts . assistantAttachmentAuthToken ,
17361811 onRequestUpdate : opts . onRequestUpdate ,
17371812 } ;
1813+ schedulePairingQrExpiryRefresh ( messageKey , message , opts . onRequestUpdate ) ;
17381814 const images = resolveRenderableMessageImages ( extractImages ( message ) , imageRenderOptions ) ;
17391815 const hasImages = images . length > 0 ;
17401816 const pairingQrExpiryNotices = extractPairingQrExpiryNotices ( message ) ;
0 commit comments