@@ -77,21 +77,26 @@ fn days_to_ymd(mut days: u64) -> (u64, u64, u64) {
7777 ( year, month, days + 1 )
7878}
7979
80- /// In-memory session tracking — tool call count and the set of symbols/files
81- /// touched, for the `session_context` tool. Reset only when the server restarts.
80+ /// In-memory session tracking — tool call count and the symbols/files
81+ /// touched, for the `session_context` tool. Reset only when the server
82+ /// restarts. Values are the `tool_calls` count at the most recent touch (not
83+ /// a boolean "seen"): `apply_personalization_boost` uses that to decay a
84+ /// result's proximity boost by how long ago (in tool-calls, not wall-clock)
85+ /// the connecting file/symbol was last explored — a re-touch refreshes it,
86+ /// same "attention" semantics as re-reading something brings it back to mind.
8287struct SessionLog {
8388 tool_calls : u64 ,
84- explored_symbols : std:: collections:: HashSet < String > ,
85- explored_files : std:: collections:: HashSet < String > ,
89+ explored_symbols : std:: collections:: HashMap < String , u64 > ,
90+ explored_files : std:: collections:: HashMap < String , u64 > ,
8691 session_started_at : String ,
8792}
8893
8994impl Default for SessionLog {
9095 fn default ( ) -> Self {
9196 Self {
9297 tool_calls : 0 ,
93- explored_symbols : std:: collections:: HashSet :: new ( ) ,
94- explored_files : std:: collections:: HashSet :: new ( ) ,
98+ explored_symbols : std:: collections:: HashMap :: new ( ) ,
99+ explored_files : std:: collections:: HashMap :: new ( ) ,
95100 session_started_at : utc_now_iso8601 ( ) ,
96101 }
97102 }
@@ -2471,6 +2476,112 @@ mod tests {
24712476 let _ = std:: fs:: remove_dir_all ( & dir) ;
24722477 }
24732478
2479+ #[ test]
2480+ fn locate_boosts_result_near_recently_explored_file ( ) {
2481+ let dir = std:: env:: temp_dir ( ) . join ( format ! ( "ci_locate_personalize_{}" , std:: process:: id( ) ) ) ;
2482+ let _ = std:: fs:: remove_dir_all ( & dir) ;
2483+ std:: fs:: create_dir_all ( & dir) . unwrap ( ) ;
2484+ let server = CodeIntelligenceServer :: new ( dir. clone ( ) , dir. join ( "index.db" ) ) . unwrap ( ) ;
2485+
2486+ {
2487+ let conn = server. db ( ) ;
2488+ conn. execute (
2489+ "INSERT INTO symbols (name, qualified_name, kind, language, path, line_start, line_end,
2490+ signature, docstring, name_tokens, caller_count, is_hub, is_entry_point)
2491+ VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13)" ,
2492+ rusqlite:: params![
2493+ "helper_fn" , "mod::helper_fn" , "function" , "rust" , "b.rs" ,
2494+ 1i64 , 5i64 , "fn helper_fn()" , "" , "helper fn" ,
2495+ 0i64 , 0i64 , 0i64
2496+ ] ,
2497+ ) . unwrap ( ) ;
2498+ // a.rs imports b.rs — tracking a.rs should boost a search hit in b.rs.
2499+ conn. execute (
2500+ "INSERT INTO import_edges (from_path, to_path, module_name) VALUES ('a.rs', 'b.rs', 'b')" ,
2501+ [ ] ,
2502+ ) . unwrap ( ) ;
2503+ }
2504+
2505+ let params = || LocateParams {
2506+ query : "helper_fn" . into ( ) ,
2507+ kind : None ,
2508+ depth : Some ( "search_only" . into ( ) ) ,
2509+ limit : None ,
2510+ } ;
2511+
2512+ let baseline = server. locate ( params ( ) ) ;
2513+ let bv: serde_json:: Value = serde_json:: from_str ( & baseline) . unwrap ( ) ;
2514+ assert_eq ! (
2515+ bv[ "personalized" ] , false ,
2516+ "a session that hasn't explored anything must not personalize"
2517+ ) ;
2518+ let baseline_score = bv[ "results" ] [ 0 ] [ "score" ] . as_f64 ( ) . unwrap ( ) ;
2519+
2520+ server. track_file ( "a.rs" ) ;
2521+
2522+ let boosted = server. locate ( params ( ) ) ;
2523+ let boostv: serde_json:: Value = serde_json:: from_str ( & boosted) . unwrap ( ) ;
2524+ assert_eq ! ( boostv[ "personalized" ] , true ) ;
2525+ let boosted_score = boostv[ "results" ] [ 0 ] [ "score" ] . as_f64 ( ) . unwrap ( ) ;
2526+
2527+ // track_file ran between two `locate` calls (each a tool call, so
2528+ // tool_calls is now 2); a.rs was touched at tool_calls=1 — distance 1,
2529+ // decay 1/(1+1)=0.5, default personalization_weight=0.15.
2530+ let expected_delta = 0.15 * 0.5 ;
2531+ assert ! (
2532+ ( boosted_score - baseline_score - expected_delta) . abs( ) < 1e-9 ,
2533+ "expected +{expected_delta}, got baseline={baseline_score} boosted={boosted_score}"
2534+ ) ;
2535+
2536+ let _ = std:: fs:: remove_dir_all ( & dir) ;
2537+ }
2538+
2539+ #[ test]
2540+ fn locate_personalization_weight_zero_disables_boost ( ) {
2541+ let dir = std:: env:: temp_dir ( ) . join ( format ! ( "ci_locate_personalize_off_{}" , std:: process:: id( ) ) ) ;
2542+ let _ = std:: fs:: remove_dir_all ( & dir) ;
2543+ std:: fs:: create_dir_all ( & dir) . unwrap ( ) ;
2544+ std:: fs:: write (
2545+ dir. join ( "config.json" ) ,
2546+ r#"{"search": {"personalization_weight": 0.0}}"# ,
2547+ )
2548+ . unwrap ( ) ;
2549+ let server = CodeIntelligenceServer :: new ( dir. clone ( ) , dir. join ( "index.db" ) ) . unwrap ( ) ;
2550+
2551+ {
2552+ let conn = server. db ( ) ;
2553+ conn. execute (
2554+ "INSERT INTO symbols (name, qualified_name, kind, language, path, line_start, line_end,
2555+ signature, docstring, name_tokens, caller_count, is_hub, is_entry_point)
2556+ VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13)" ,
2557+ rusqlite:: params![
2558+ "helper_fn" , "mod::helper_fn" , "function" , "rust" , "b.rs" ,
2559+ 1i64 , 5i64 , "fn helper_fn()" , "" , "helper fn" ,
2560+ 0i64 , 0i64 , 0i64
2561+ ] ,
2562+ ) . unwrap ( ) ;
2563+ conn. execute (
2564+ "INSERT INTO import_edges (from_path, to_path, module_name) VALUES ('a.rs', 'b.rs', 'b')" ,
2565+ [ ] ,
2566+ ) . unwrap ( ) ;
2567+ }
2568+
2569+ server. track_file ( "a.rs" ) ;
2570+ let output = server. locate ( LocateParams {
2571+ query : "helper_fn" . into ( ) ,
2572+ kind : None ,
2573+ depth : Some ( "search_only" . into ( ) ) ,
2574+ limit : None ,
2575+ } ) ;
2576+ let v: serde_json:: Value = serde_json:: from_str ( & output) . unwrap ( ) ;
2577+ assert_eq ! (
2578+ v[ "personalized" ] , false ,
2579+ "personalization_weight=0.0 must fully disable boosting"
2580+ ) ;
2581+
2582+ let _ = std:: fs:: remove_dir_all ( & dir) ;
2583+ }
2584+
24742585 /// Regression for Task 15: `session_context` had no config knob bounding
24752586 /// `explored_symbols`/`explored_files` — a long session dumped an
24762587 /// unbounded list into every call.
0 commit comments