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