@@ -94,6 +94,8 @@ public sealed class OpenClawChatDataProvider : IChatDataProvider
9494 private readonly object _attachmentMetaSaveGate = new ( ) ;
9595 private readonly string _toolMetaCacheFilePath ;
9696 private readonly string _attachmentMetaCacheFilePath ;
97+ private readonly string _lastChatStateFilePath ;
98+ private readonly TimeSpan _lastChatStateSaveDelay ;
9799 private System . Threading . Timer ? _toolMetaSaveTimer ; // debounce cache writes
98100 private long _toolMetaSaveVersion ;
99101 private bool _toolMetaCacheDirty ;
@@ -242,7 +244,9 @@ internal OpenClawChatDataProvider(
242244 IChatGatewayBridge bridge ,
243245 Action < Action > ? post ,
244246 string toolMetaCacheFilePath ,
245- string ? attachmentMetaCacheFilePath = null )
247+ string ? attachmentMetaCacheFilePath = null ,
248+ string ? lastChatStateFilePath = null ,
249+ TimeSpan ? lastChatStateSaveDelay = null )
246250 {
247251 _bridge = bridge ?? throw new ArgumentNullException ( nameof ( bridge ) ) ;
248252 _post = post ;
@@ -252,11 +256,15 @@ internal OpenClawChatDataProvider(
252256 _attachmentMetaCacheFilePath = ! string . IsNullOrWhiteSpace ( attachmentMetaCacheFilePath )
253257 ? attachmentMetaCacheFilePath
254258 : DefaultAttachmentMetaCacheFilePath ( _toolMetaCacheFilePath ) ;
259+ _lastChatStateFilePath = ! string . IsNullOrWhiteSpace ( lastChatStateFilePath )
260+ ? lastChatStateFilePath
261+ : LastChatStateFilePath ;
262+ _lastChatStateSaveDelay = lastChatStateSaveDelay ?? TimeSpan . FromSeconds ( 2 ) ;
255263 _status = bridge . CurrentStatus ;
256264 _persistedAbortedIds = LoadAbortedIds ( ) ;
257265 _toolMetaCache = LoadToolMetaCache ( _toolMetaCacheFilePath ) ;
258266 _attachmentMetaCache = LoadAttachmentMetaCache ( _attachmentMetaCacheFilePath ) ;
259- _lastChatState = LoadLastChatState ( ) ;
267+ _lastChatState = LoadLastChatState ( _lastChatStateFilePath ) ;
260268
261269 // Seed models from whatever the bridge already knows about (a connect
262270 // that completed before the provider was constructed will have its
@@ -306,6 +314,34 @@ public Task<ChatDataSnapshot> LoadAsync(CancellationToken cancellationToken = de
306314 }
307315 }
308316
317+ internal void RememberSelectedThread ( string ? threadId )
318+ {
319+ if ( string . IsNullOrWhiteSpace ( threadId ) )
320+ return ;
321+
322+ LastChatState ? state ;
323+ lock ( _gate )
324+ {
325+ if ( ! TryGetSessionLocked ( threadId , out var session ) )
326+ return ;
327+
328+ state = new LastChatState
329+ {
330+ DefaultThreadId = threadId ,
331+ ThreadTitle = BuildSessionTitle ( session ) ,
332+ Model = session . Model ,
333+ ModelProvider = session . Provider ,
334+ AvailableModels = _availableModels ,
335+ } ;
336+ _lastChatState = state ;
337+ _lastChatStateSaveVersion ++ ;
338+ _lastChatStateSaveTimer ? . Dispose ( ) ;
339+ _lastChatStateSaveTimer = null ;
340+ }
341+
342+ SaveLastChatState ( state , _lastChatStateFilePath ) ;
343+ }
344+
309345 // Explicit interface implementation (no attachments).
310346 Task IChatDataProvider . SendMessageAsync ( string threadId , string message , CancellationToken cancellationToken )
311347 => SendMessageAsync ( threadId , message , cancellationToken , attachments : null ) ;
@@ -5678,6 +5714,12 @@ private ChatDataSnapshot BuildSnapshotLocked()
56785714
56795715 private string ? ResolveDefaultThreadIdLocked ( )
56805716 {
5717+ if ( _lastChatState ? . DefaultThreadId is { Length : > 0 } rememberedThreadId )
5718+ {
5719+ if ( TryGetSessionLocked ( rememberedThreadId , out _ ) || ! _sessionsListReceived )
5720+ return rememberedThreadId ;
5721+ }
5722+
56815723 // Prefer the gateway's canonical main session (IsMain on SessionInfo)
56825724 // so we never have to guess from a literal like "main". Only fall back
56835725 // to the compose target (pre-materialization) or the first available
@@ -5700,8 +5742,11 @@ private ChatDataSnapshot BuildSnapshotLocked()
57005742 private void RememberLastSessionStateLocked ( )
57015743 {
57025744 if ( _sessions . Length == 0 ) return ;
5703- var session = _sessions . FirstOrDefault ( s => s . IsMain && ! string . IsNullOrEmpty ( s . Key ) )
5704- ?? _sessions . FirstOrDefault ( s => ! string . IsNullOrEmpty ( s . Key ) ) ;
5745+ var defaultThreadId = ResolveDefaultThreadIdLocked ( ) ;
5746+ var session = defaultThreadId is { Length : > 0 } && TryGetSessionLocked ( defaultThreadId , out var selected )
5747+ ? selected
5748+ : _sessions . FirstOrDefault ( s => s . IsMain && ! string . IsNullOrEmpty ( s . Key ) )
5749+ ?? _sessions . FirstOrDefault ( s => ! string . IsNullOrEmpty ( s . Key ) ) ;
57055750 if ( session is null ) return ;
57065751
57075752 _lastChatState = new LastChatState
@@ -5714,6 +5759,22 @@ private void RememberLastSessionStateLocked()
57145759 } ;
57155760 }
57165761
5762+ private bool TryGetSessionLocked ( string threadId , out SessionInfo session )
5763+ {
5764+ for ( int i = 0 ; i < _sessions . Length ; i ++ )
5765+ {
5766+ var candidate = _sessions [ i ] ;
5767+ if ( string . Equals ( candidate . Key , threadId , StringComparison . Ordinal ) )
5768+ {
5769+ session = candidate ;
5770+ return true ;
5771+ }
5772+ }
5773+
5774+ session = default ! ;
5775+ return false ;
5776+ }
5777+
57175778 private static ChatThread ToThread ( SessionInfo s )
57185779 {
57195780 var title = BuildSessionTitle ( s ) ;
@@ -5811,6 +5872,7 @@ private void Publish(ChatDataSnapshot snapshot)
58115872 AppIdentity . ResolveLocalDataDirectory ( ) , "last-chat-state.json" ) ;
58125873
58135874 private System . Threading . Timer ? _lastChatStateSaveTimer ;
5875+ private long _lastChatStateSaveVersion ;
58145876
58155877 internal sealed class LastChatState
58165878 {
@@ -5861,21 +5923,38 @@ private void DebounceSaveLastChatState(ChatDataSnapshot snapshot)
58615923 lock ( _gate )
58625924 {
58635925 _lastChatState = state ;
5926+ _lastChatStateSaveVersion ++ ;
5927+ var saveVersion = _lastChatStateSaveVersion ;
58645928 _lastChatStateSaveTimer ? . Dispose ( ) ;
5865- _lastChatStateSaveTimer = new System . Threading . Timer ( _ => SaveLastChatState ( state ) , null , 2000 , Timeout . Infinite ) ;
5929+ var path = _lastChatStateFilePath ;
5930+ _lastChatStateSaveTimer = new System . Threading . Timer ( _ => SaveLastChatStateIfCurrent ( state , path , saveVersion ) , null , _lastChatStateSaveDelay , Timeout . InfiniteTimeSpan ) ;
58665931 }
58675932 }
58685933
5869- private static void SaveLastChatState ( LastChatState state )
5934+ private void SaveLastChatStateIfCurrent ( LastChatState state , string path , long saveVersion )
58705935 {
5936+ lock ( _gate )
5937+ {
5938+ if ( saveVersion != _lastChatStateSaveVersion )
5939+ return ;
5940+
5941+ SaveLastChatState ( state , path ) ;
5942+ _lastChatStateSaveTimer ? . Dispose ( ) ;
5943+ _lastChatStateSaveTimer = null ;
5944+ }
5945+ }
5946+
5947+ private static void SaveLastChatState ( LastChatState state , string ? pathOverride = null )
5948+ {
5949+ var path = pathOverride ?? LastChatStateFilePath ;
58715950 try
58725951 {
5873- var dir = Path . GetDirectoryName ( LastChatStateFilePath ) ;
5952+ var dir = Path . GetDirectoryName ( path ) ;
58745953 if ( ! string . IsNullOrEmpty ( dir ) ) Directory . CreateDirectory ( dir ) ;
58755954 var json = System . Text . Json . JsonSerializer . Serialize ( state ) ;
5876- var tmp = LastChatStateFilePath + ".tmp" ;
5955+ var tmp = path + ".tmp" ;
58775956 File . WriteAllText ( tmp , json ) ;
5878- File . Move ( tmp , LastChatStateFilePath , overwrite : true ) ;
5957+ File . Move ( tmp , path , overwrite : true ) ;
58795958 }
58805959 catch ( Exception ex ) { Logger . Debug ( $ "ChatDataProvider: persist LastChatState failed: { ex . Message } ") ; }
58815960 }
0 commit comments