3535router = APIRouter (prefix = "/api/threads" , tags = ["threads" ])
3636
3737
38+ def _sanitize_log_param (value : str ) -> str :
39+ """Strip control characters to prevent log injection."""
40+ return value .replace ("\n " , "" ).replace ("\r " , "" ).replace ("\x00 " , "" )
41+
42+
3843# ---------------------------------------------------------------------------
3944# Response / request models
4045# ---------------------------------------------------------------------------
@@ -136,13 +141,13 @@ def _delete_thread_data(thread_id: str, paths: Paths | None = None) -> ThreadDel
136141 raise HTTPException (status_code = 422 , detail = str (exc )) from exc
137142 except FileNotFoundError :
138143 # Not critical — thread data may not exist on disk
139- logger .debug ("No local thread data to delete for %s" , thread_id )
144+ logger .debug ("No local thread data to delete for %s" , _sanitize_log_param ( thread_id ) )
140145 return ThreadDeleteResponse (success = True , message = f"No local data for { thread_id } " )
141146 except Exception as exc :
142- logger .exception ("Failed to delete thread data for %s" , thread_id )
147+ logger .exception ("Failed to delete thread data for %s" , _sanitize_log_param ( thread_id ) )
143148 raise HTTPException (status_code = 500 , detail = "Failed to delete local thread data." ) from exc
144149
145- logger .info ("Deleted local thread data for %s" , thread_id )
150+ logger .info ("Deleted local thread data for %s" , _sanitize_log_param ( thread_id ) )
146151 return ThreadDeleteResponse (success = True , message = f"Deleted local thread data for { thread_id } " )
147152
148153
@@ -231,7 +236,7 @@ async def delete_thread_data(thread_id: str, request: Request) -> ThreadDeleteRe
231236 try :
232237 await store .adelete (THREADS_NS , thread_id )
233238 except Exception :
234- logger .debug ("Could not delete store record for thread %s (not critical)" , thread_id )
239+ logger .debug ("Could not delete store record for thread %s (not critical)" , _sanitize_log_param ( thread_id ) )
235240
236241 # Remove checkpoints (best-effort)
237242 checkpointer = getattr (request .app .state , "checkpointer" , None )
@@ -240,7 +245,7 @@ async def delete_thread_data(thread_id: str, request: Request) -> ThreadDeleteRe
240245 if hasattr (checkpointer , "adelete_thread" ):
241246 await checkpointer .adelete_thread (thread_id )
242247 except Exception :
243- logger .debug ("Could not delete checkpoints for thread %s (not critical)" , thread_id )
248+ logger .debug ("Could not delete checkpoints for thread %s (not critical)" , _sanitize_log_param ( thread_id ) )
244249
245250 return response
246251
@@ -284,7 +289,7 @@ async def create_thread(body: ThreadCreateRequest, request: Request) -> ThreadRe
284289 },
285290 )
286291 except Exception :
287- logger .exception ("Failed to write thread %s to store" , thread_id )
292+ logger .exception ("Failed to write thread %s to store" , _sanitize_log_param ( thread_id ) )
288293 raise HTTPException (status_code = 500 , detail = "Failed to create thread" )
289294
290295 # Write an empty checkpoint so state endpoints work immediately
@@ -302,10 +307,24 @@ async def create_thread(body: ThreadCreateRequest, request: Request) -> ThreadRe
302307 }
303308 await checkpointer .aput (config , empty_checkpoint (), ckpt_metadata , {})
304309 except Exception :
305- logger .exception ("Failed to create checkpoint for thread %s" , thread_id )
310+ logger .exception ("Failed to create checkpoint for thread %s" , _sanitize_log_param ( thread_id ) )
306311 raise HTTPException (status_code = 500 , detail = "Failed to create thread" )
307312
308- logger .info ("Thread created: %s" , thread_id )
313+ # Write thread_meta so the thread appears in /threads/search immediately
314+ from app .gateway .deps import get_thread_meta_repo
315+
316+ thread_meta_repo = get_thread_meta_repo (request )
317+ if thread_meta_repo is not None :
318+ try :
319+ await thread_meta_repo .create (
320+ thread_id ,
321+ assistant_id = getattr (body , "assistant_id" , None ),
322+ metadata = body .metadata ,
323+ )
324+ except Exception :
325+ logger .debug ("Failed to upsert thread_meta on create for %s (non-fatal)" , _sanitize_log_param (thread_id ))
326+
327+ logger .info ("Thread created: %s" , _sanitize_log_param (thread_id ))
309328 return ThreadResponse (
310329 thread_id = thread_id ,
311330 status = "idle" ,
@@ -372,7 +391,7 @@ async def patch_thread(thread_id: str, body: ThreadPatchRequest, request: Reques
372391 try :
373392 await _store_put (store , updated )
374393 except Exception :
375- logger .exception ("Failed to patch thread %s" , thread_id )
394+ logger .exception ("Failed to patch thread %s" , _sanitize_log_param ( thread_id ) )
376395 raise HTTPException (status_code = 500 , detail = "Failed to update thread" )
377396
378397 return ThreadResponse (
@@ -404,7 +423,7 @@ async def get_thread(thread_id: str, request: Request) -> ThreadResponse:
404423 try :
405424 checkpoint_tuple = await checkpointer .aget_tuple (config )
406425 except Exception :
407- logger .exception ("Failed to get checkpoint for thread %s" , thread_id )
426+ logger .exception ("Failed to get checkpoint for thread %s" , _sanitize_log_param ( thread_id ) )
408427 raise HTTPException (status_code = 500 , detail = "Failed to get thread" )
409428
410429 if record is None and checkpoint_tuple is None :
@@ -452,7 +471,7 @@ async def get_thread_state(thread_id: str, request: Request) -> ThreadStateRespo
452471 try :
453472 checkpoint_tuple = await checkpointer .aget_tuple (config )
454473 except Exception :
455- logger .exception ("Failed to get state for thread %s" , thread_id )
474+ logger .exception ("Failed to get state for thread %s" , _sanitize_log_param ( thread_id ) )
456475 raise HTTPException (status_code = 500 , detail = "Failed to get thread state" )
457476
458477 if checkpoint_tuple is None :
@@ -514,7 +533,7 @@ async def update_thread_state(thread_id: str, body: ThreadStateUpdateRequest, re
514533 try :
515534 checkpoint_tuple = await checkpointer .aget_tuple (read_config )
516535 except Exception :
517- logger .exception ("Failed to get state for thread %s" , thread_id )
536+ logger .exception ("Failed to get state for thread %s" , _sanitize_log_param ( thread_id ) )
518537 raise HTTPException (status_code = 500 , detail = "Failed to get thread state" )
519538
520539 if checkpoint_tuple is None :
@@ -548,7 +567,7 @@ async def update_thread_state(thread_id: str, body: ThreadStateUpdateRequest, re
548567 try :
549568 new_config = await checkpointer .aput (write_config , checkpoint , metadata , {})
550569 except Exception :
551- logger .exception ("Failed to update state for thread %s" , thread_id )
570+ logger .exception ("Failed to update state for thread %s" , _sanitize_log_param ( thread_id ) )
552571 raise HTTPException (status_code = 500 , detail = "Failed to update thread state" )
553572
554573 new_checkpoint_id : str | None = None
@@ -560,7 +579,7 @@ async def update_thread_state(thread_id: str, body: ThreadStateUpdateRequest, re
560579 try :
561580 await _store_upsert (store , thread_id , values = {"title" : body .values ["title" ]})
562581 except Exception :
563- logger .debug ("Failed to sync title to store for thread %s (non-fatal)" , thread_id )
582+ logger .debug ("Failed to sync title to store for thread %s (non-fatal)" , _sanitize_log_param ( thread_id ) )
564583
565584 return ThreadStateResponse (
566585 values = serialize_channel_values (channel_values ),
@@ -594,16 +613,12 @@ async def get_thread_history(thread_id: str, body: ThreadHistoryRequest, request
594613 try :
595614 all_messages = await event_store .list_messages (thread_id , limit = 10_000 )
596615 except Exception :
597- logger .warning ("Failed to load messages from event store for thread %s" , thread_id , exc_info = True )
616+ logger .warning ("Failed to load messages from event store for thread %s" , _sanitize_log_param ( thread_id ) , exc_info = True )
598617 all_messages = []
599618
600- # Group messages by run_id for per-checkpoint assembly
601- messages_by_run : dict [str , list [dict ]] = {}
602- for msg in all_messages :
603- run_id = msg .get ("run_id" , "" )
604- messages_by_run .setdefault (run_id , []).append (msg .get ("content" , {}))
605619
606620 entries : list [HistoryEntry ] = []
621+ is_latest_checkpoint = True
607622 try :
608623 async for checkpoint_tuple in checkpointer .alist (config , limit = body .limit ):
609624 ckpt_config = getattr (checkpoint_tuple , "config" , {})
@@ -625,9 +640,10 @@ async def get_thread_history(thread_id: str, body: ThreadHistoryRequest, request
625640 if thread_data := channel_values .get ("thread_data" ):
626641 values ["thread_data" ] = thread_data
627642
628- # Attach all messages from event store (not just this checkpoint's run)
629- if all_messages :
643+ # Attach all messages only to the latest (first) checkpoint entry
644+ if is_latest_checkpoint and all_messages :
630645 values ["messages" ] = [m .get ("content" , {}) for m in all_messages ]
646+ is_latest_checkpoint = False
631647
632648 # Derive next tasks
633649 tasks_raw = getattr (checkpoint_tuple , "tasks" , []) or []
@@ -650,7 +666,7 @@ async def get_thread_history(thread_id: str, body: ThreadHistoryRequest, request
650666 )
651667 )
652668 except Exception :
653- logger .exception ("Failed to get history for thread %s" , thread_id )
669+ logger .exception ("Failed to get history for thread %s" , _sanitize_log_param ( thread_id ) )
654670 raise HTTPException (status_code = 500 , detail = "Failed to get thread history" )
655671
656672 return entries
0 commit comments