@@ -884,6 +884,15 @@ static const tool_def_t TOOLS[] = {
884884 "\"description\":\"Include graph neighbors of touched symbols that have not been "
885885 "examined yet.\"},\"limit\":{\"type\":\"integer\",\"default\":10,"
886886 "\"description\":\"Max related_untouched items.\"}},\"required\":[]}" },
887+
888+ {"get_session_summary" ,
889+ "Compact markdown session summary for context recovery after compaction. "
890+ "Shows files touched, symbols investigated with PageRank, areas explored, "
891+ "and suggested next steps." ,
892+ "{\"type\":\"object\",\"properties\":{\"project\":{\"type\":\"string\","
893+ "\"description\":\"Project name (needed for PageRank enrichment and next-step "
894+ "suggestions).\"},\"max_tokens\":{\"type\":\"integer\",\"default\":2000,"
895+ "\"description\":\"Maximum output size.\"}},\"required\":[]}" },
887896};
888897
889898static const int TOOL_COUNT = sizeof (TOOLS ) / sizeof (TOOLS [0 ]);
@@ -5962,7 +5971,7 @@ static void maybe_add_session_hint(yyjson_mut_doc *doc, yyjson_mut_val *root, co
59625971 }
59635972}
59645973
5965- /* ── Session context (Phase 7A) ────────── ──────────────────────── */
5974+ /* ── Session helpers (shared by 7A + 7C) ──────────────────────── */
59665975
59675976/* Callback: free a strdup'd hash table key (for temporary candidate sets). */
59685977static void free_ht_key_cb (const char * key , void * value , void * userdata ) {
@@ -5971,7 +5980,7 @@ static void free_ht_key_cb(const char *key, void *value, void *userdata) {
59715980 free ((void * )key );
59725981}
59735982
5974- /* Callback: append key to a yyjson array. */
5983+ /* Callback: append key to a yyjson array (used by get_session_context) . */
59755984typedef struct {
59765985 yyjson_mut_doc * doc ;
59775986 yyjson_mut_val * arr ;
@@ -5982,7 +5991,7 @@ static void append_key_to_json_arr(const char *key, void *userdata) {
59825991 yyjson_mut_arr_add_strcpy (ctx -> doc , ctx -> arr , key );
59835992}
59845993
5985- /* Callback: collect symbol names into a list for related_untouched lookup. */
5994+ /* Callback: collect symbol names into a list for neighbor lookup. */
59865995typedef struct {
59875996 const char * * names ;
59885997 int count ;
@@ -5996,6 +6005,215 @@ static void collect_symbol_name(const char *key, void *userdata) {
59966005 }
59976006}
59986007
6008+ /* ── Session summary (Phase 7C) ────────────────────────────────── */
6009+
6010+ /* Callback context for iterating session sets into markdown. */
6011+ typedef struct {
6012+ markdown_builder_t * md ;
6013+ int count ; /* items emitted so far */
6014+ } md_list_ctx_t ;
6015+
6016+ static void append_key_comma_separated (const char * key , void * userdata ) {
6017+ md_list_ctx_t * ctx = (md_list_ctx_t * )userdata ;
6018+ if (ctx -> count > 0 ) {
6019+ (void )markdown_builder_append_raw (ctx -> md , ", " );
6020+ }
6021+ (void )markdown_builder_append_raw (ctx -> md , key );
6022+ ctx -> count ++ ;
6023+ }
6024+
6025+ static void append_key_bullet (const char * key , void * userdata ) {
6026+ md_list_ctx_t * ctx = (md_list_ctx_t * )userdata ;
6027+ (void )markdown_builder_appendf (ctx -> md , "- %s\n" , key );
6028+ ctx -> count ++ ;
6029+ }
6030+
6031+ static char * handle_get_session_summary (cbm_mcp_server_t * srv , const char * args ) {
6032+ char * project = cbm_mcp_get_string_arg (args , "project" );
6033+ int max_tokens = cbm_mcp_get_int_arg (args , "max_tokens" , DEFAULT_MAX_TOKENS );
6034+
6035+ cbm_session_state_t * ss = ensure_session (srv );
6036+ cbm_store_t * store = project ? resolve_store (srv , project ) : NULL ;
6037+
6038+ size_t char_budget = max_tokens_to_char_budget (max_tokens );
6039+ markdown_builder_t md ;
6040+ markdown_builder_init (& md , char_budget );
6041+
6042+ /* ── Header ──────────────────────────────────────────────── */
6043+ time_t start = cbm_session_start_time (ss );
6044+ time_t now = time (NULL );
6045+ int elapsed = (int )(now - start );
6046+ if (elapsed < 0 ) elapsed = 0 ;
6047+ int minutes = elapsed / 60 ;
6048+ int seconds = elapsed % 60 ;
6049+ int qc = cbm_session_query_count (ss );
6050+
6051+ if (minutes > 0 ) {
6052+ (void )markdown_builder_appendf (& md , "## Session Summary (%d queries, %dm%ds)\n\n" ,
6053+ qc , minutes , seconds );
6054+ } else {
6055+ (void )markdown_builder_appendf (& md , "## Session Summary (%d queries, %ds)\n\n" ,
6056+ qc , seconds );
6057+ }
6058+
6059+ /* ── Files touched ───────────────────────────────────────── */
6060+ int read_count = cbm_session_files_read_count (ss );
6061+ int edited_count = cbm_session_files_edited_count (ss );
6062+
6063+ if (read_count > 0 || edited_count > 0 ) {
6064+ (void )markdown_builder_append_raw (& md , "### Files touched\n" );
6065+ if (read_count > 0 ) {
6066+ (void )markdown_builder_append_raw (& md , "- **Read:** " );
6067+ md_list_ctx_t ctx = {.md = & md , .count = 0 };
6068+ cbm_session_foreach_file_read (ss , append_key_comma_separated , & ctx );
6069+ (void )markdown_builder_append_raw (& md , "\n" );
6070+ }
6071+ if (edited_count > 0 ) {
6072+ (void )markdown_builder_append_raw (& md , "- **Edited:** " );
6073+ md_list_ctx_t ctx = {.md = & md , .count = 0 };
6074+ cbm_session_foreach_file_edited (ss , append_key_comma_separated , & ctx );
6075+ (void )markdown_builder_append_raw (& md , "\n" );
6076+ }
6077+ (void )markdown_builder_append_raw (& md , "\n" );
6078+ }
6079+
6080+ /* ── Symbols investigated ────────────────────────────────── */
6081+ int sym_count = cbm_session_symbols_count (ss );
6082+ int impact_count = cbm_session_impacts_count (ss );
6083+
6084+ if (sym_count > 0 || impact_count > 0 ) {
6085+ (void )markdown_builder_append_raw (& md , "### Symbols investigated\n" );
6086+
6087+ /* Collect queried symbol names */
6088+ const char * sym_names [30 ];
6089+ name_collector_t sc = {.names = sym_names , .count = 0 , .cap = 30 };
6090+ cbm_session_foreach_symbol (ss , collect_symbol_name , & sc );
6091+
6092+ for (int i = 0 ; i < sc .count ; i ++ ) {
6093+ const char * name = sc .names [i ];
6094+
6095+ /* Look up PageRank if store available */
6096+ if (store ) {
6097+ cbm_key_symbol_t * ks = NULL ;
6098+ int ks_count = 0 ;
6099+ cbm_store_get_key_symbols (store , project , name , 1 , & ks , & ks_count );
6100+ if (ks_count > 0 && ks [0 ].name && strcmp (ks [0 ].name , name ) == 0 ) {
6101+ (void )markdown_builder_appendf (& md , "- %s (%d callers, PageRank %.4f)" ,
6102+ name , ks [0 ].in_degree , ks [0 ].pagerank );
6103+ } else {
6104+ (void )markdown_builder_appendf (& md , "- %s" , name );
6105+ }
6106+ cbm_store_key_symbols_free (ks , ks_count );
6107+ } else {
6108+ (void )markdown_builder_appendf (& md , "- %s" , name );
6109+ }
6110+ (void )markdown_builder_append_raw (& md , "\n" );
6111+ }
6112+ (void )markdown_builder_append_raw (& md , "\n" );
6113+ }
6114+
6115+ /* ── Impact analyses ─────────────────────────────────────── */
6116+ if (impact_count > 0 ) {
6117+ (void )markdown_builder_append_raw (& md , "### Impact analyses run\n" );
6118+ md_list_ctx_t ctx = {.md = & md , .count = 0 };
6119+ cbm_session_foreach_impact (ss , append_key_bullet , & ctx );
6120+ (void )markdown_builder_append_raw (& md , "\n" );
6121+ }
6122+
6123+ /* ── Areas explored ──────────────────────────────────────── */
6124+ int area_count = cbm_session_areas_count (ss );
6125+ if (area_count > 0 ) {
6126+ (void )markdown_builder_append_raw (& md , "### Areas explored\n" );
6127+ md_list_ctx_t ctx = {.md = & md , .count = 0 };
6128+ cbm_session_foreach_area (ss , append_key_bullet , & ctx );
6129+ (void )markdown_builder_append_raw (& md , "\n" );
6130+ }
6131+
6132+ /* ── Suggested next steps ────────────────────────────────── */
6133+ if (store && sym_count > 0 ) {
6134+ {
6135+ /* Collect symbols for neighbor lookup */
6136+ const char * lookup_names [20 ];
6137+ name_collector_t nc = {.names = lookup_names , .count = 0 , .cap = 20 };
6138+ cbm_session_foreach_symbol (ss , collect_symbol_name , & nc );
6139+ cbm_session_foreach_impact (ss , collect_symbol_name , & nc );
6140+
6141+ /* Temporary dedup set for candidates */
6142+ CBMHashTable * candidates = cbm_ht_create (64 );
6143+ for (int i = 0 ; i < nc .count ; i ++ ) {
6144+ cbm_node_t * nodes = NULL ;
6145+ int ncount = 0 ;
6146+ cbm_store_find_nodes_by_name (store , project , lookup_names [i ], & nodes , & ncount );
6147+ for (int j = 0 ; j < ncount ; j ++ ) {
6148+ char * * callers = NULL ;
6149+ char * * callees = NULL ;
6150+ int caller_count = 0 , callee_count = 0 ;
6151+ cbm_store_node_neighbor_names (store , nodes [j ].id , 10 , & callers , & caller_count ,
6152+ & callees , & callee_count );
6153+ for (int k = 0 ; k < caller_count ; k ++ ) {
6154+ if (callers [k ] && !cbm_session_has_symbol (ss , callers [k ]) &&
6155+ !cbm_ht_has (candidates , callers [k ])) {
6156+ char * key = strdup (callers [k ]);
6157+ if (key ) cbm_ht_set (candidates , key , (void * )lookup_names [i ]);
6158+ }
6159+ }
6160+ for (int k = 0 ; k < callee_count ; k ++ ) {
6161+ if (callees [k ] && !cbm_session_has_symbol (ss , callees [k ]) &&
6162+ !cbm_ht_has (candidates , callees [k ])) {
6163+ char * key = strdup (callees [k ]);
6164+ if (key ) cbm_ht_set (candidates , key , (void * )lookup_names [i ]);
6165+ }
6166+ }
6167+ for (int k = 0 ; k < caller_count ; k ++ ) free (callers [k ]);
6168+ free (callers );
6169+ for (int k = 0 ; k < callee_count ; k ++ ) free (callees [k ]);
6170+ free (callees );
6171+ }
6172+ cbm_store_free_nodes (nodes , ncount );
6173+ }
6174+
6175+ if (cbm_ht_count (candidates ) > 0 ) {
6176+ cbm_key_symbol_t * key_syms = NULL ;
6177+ int ks_count = 0 ;
6178+ cbm_store_get_key_symbols (store , project , NULL , 200 , & key_syms , & ks_count );
6179+
6180+ bool header_emitted = false;
6181+ int emitted = 0 ;
6182+ for (int i = 0 ; i < ks_count && emitted < 5 ; i ++ ) {
6183+ if (key_syms [i ].name && cbm_ht_has (candidates , key_syms [i ].name )) {
6184+ if (!header_emitted ) {
6185+ (void )markdown_builder_append_raw (& md , "### Suggested next steps\n" );
6186+ header_emitted = true;
6187+ }
6188+ const char * reason =
6189+ (const char * )cbm_ht_get (candidates , key_syms [i ].name );
6190+ (void )markdown_builder_appendf (
6191+ & md , "- Examine %s%s%s (neighbor of %s, not yet examined)\n" ,
6192+ key_syms [i ].name ,
6193+ key_syms [i ].file_path ? " in " : "" ,
6194+ key_syms [i ].file_path ? key_syms [i ].file_path : "" ,
6195+ reason ? reason : "queried symbol" );
6196+ emitted ++ ;
6197+ }
6198+ }
6199+ cbm_store_key_symbols_free (key_syms , ks_count );
6200+ }
6201+
6202+ cbm_ht_foreach (candidates , free_ht_key_cb , NULL );
6203+ cbm_ht_free (candidates );
6204+ }
6205+ }
6206+
6207+ char * markdown = markdown_builder_finish (& md );
6208+ free (project );
6209+
6210+ char * result = cbm_mcp_text_result (markdown ? markdown : "" , false);
6211+ free (markdown );
6212+ return result ;
6213+ }
6214+
6215+ /* ── Session context (Phase 7A) ────────────────────────────────── */
6216+
59996217static char * handle_get_session_context (cbm_mcp_server_t * srv , const char * args ) {
60006218 char * project = cbm_mcp_get_string_arg (args , "project" );
60016219 bool include_related = cbm_mcp_get_bool_arg_default (args , "include_related" , true);
@@ -6222,6 +6440,9 @@ char *cbm_mcp_handle_tool(cbm_mcp_server_t *srv, const char *tool_name, const ch
62226440 if (strcmp (tool_name , "get_session_context" ) == 0 ) {
62236441 return handle_get_session_context (srv , args_json );
62246442 }
6443+ if (strcmp (tool_name , "get_session_summary" ) == 0 ) {
6444+ return handle_get_session_summary (srv , args_json );
6445+ }
62256446
62266447 char msg [256 ];
62276448 snprintf (msg , sizeof (msg ), "unknown tool: %s" , tool_name );
0 commit comments