@@ -148,40 +148,80 @@ def close(
148148
149149 self ._closed = True
150150
151- # Flush and sync file to disk before closing to ensure all data is written
152- # This is critical on CI systems where file system operations may be slower
153- self ._trace_file .flush ()
151+ if not blocking :
152+ # Fire-and-forget background finalize+upload.
153+ #
154+ # IMPORTANT: for truly non-blocking close, we avoid synchronous work here
155+ # (flush/fsync/index generation). That work happens in the background thread.
156+ thread = threading .Thread (
157+ target = self ._close_and_upload_background ,
158+ args = (on_progress ,),
159+ daemon = True ,
160+ )
161+ thread .start ()
162+ return # Return immediately
163+
164+ # Blocking mode: finalize trace file and upload now.
165+ if not self ._finalize_trace_file_for_upload ():
166+ return
167+ self ._do_upload (on_progress )
168+
169+ def _finalize_trace_file_for_upload (self ) -> bool :
170+ """
171+ Finalize the local trace file so it is ready for upload.
172+
173+ Returns:
174+ True if there is data to upload, False if the trace is empty/missing.
175+ """
176+ # Flush and sync file to disk before closing to ensure all data is written.
177+ # This can be slow on CI file systems; in non-blocking close we do this in background.
178+ try :
179+ self ._trace_file .flush ()
180+ except Exception :
181+ pass
154182 try :
155- # Force OS to write buffered data to disk
156183 os .fsync (self ._trace_file .fileno ())
157184 except (OSError , AttributeError ):
158- # Some file handles don't support fsync (e.g., StringIO in tests)
159- # This is fine - flush() is usually sufficient
185+ # Some file handles don't support fsync; flush is usually sufficient.
186+ pass
187+ try :
188+ self ._trace_file .close ()
189+ except Exception :
160190 pass
161- self ._trace_file .close ()
162191
163192 # Ensure file exists and has content before proceeding
164- if not self ._path .exists () or self ._path .stat ().st_size == 0 :
165- # No events were emitted, nothing to upload
166- if self .logger :
167- self .logger .warning ("No trace events to upload (file is empty or missing)" )
168- return
193+ try :
194+ if not self ._path .exists () or self ._path .stat ().st_size == 0 :
195+ if self .logger :
196+ self .logger .warning ("No trace events to upload (file is empty or missing)" )
197+ return False
198+ except Exception :
199+ # If we can't stat, don't attempt upload
200+ return False
169201
170202 # Generate index after closing file
171203 self ._generate_index ()
204+ return True
172205
173- if not blocking :
174- # Fire-and-forget background upload
175- thread = threading .Thread (
176- target = self ._do_upload ,
177- args = (on_progress ,),
178- daemon = True ,
179- )
180- thread .start ()
181- return # Return immediately
206+ def _close_and_upload_background (
207+ self , on_progress : Callable [[int , int ], None ] | None = None
208+ ) -> None :
209+ """
210+ Background worker for non-blocking close.
182211
183- # Blocking mode
184- self ._do_upload (on_progress )
212+ Performs file finalization + index generation + upload.
213+ """
214+ try :
215+ if not self ._finalize_trace_file_for_upload ():
216+ return
217+ self ._do_upload (on_progress )
218+ except Exception as e :
219+ # Non-fatal: preserve trace locally
220+ self ._upload_successful = False
221+ print (f"❌ [Sentience] Error uploading trace (background): { e } " )
222+ print (f" Local trace preserved at: { self ._path } " )
223+ if self .logger :
224+ self .logger .error (f"Error uploading trace (background): { e } " )
185225
186226 def _do_upload (self , on_progress : Callable [[int , int ], None ] | None = None ) -> None :
187227 """
0 commit comments