diff --git a/src/mcp/mcp.c b/src/mcp/mcp.c index 8102b1e77..80bf44c05 100644 --- a/src/mcp/mcp.c +++ b/src/mcp/mcp.c @@ -1811,6 +1811,11 @@ static char *handle_delete_project(cbm_mcp_server_t *srv, const char *args) { } cbm_pipeline_unlock(); + + if (srv->watcher) { + cbm_watcher_unwatch(srv->watcher, name); + } + cbm_mem_collect(); /* return freed pages to OS after closing database */ yyjson_mut_doc *doc = yyjson_mut_doc_new(NULL); diff --git a/src/watcher/watcher.c b/src/watcher/watcher.c index 04f27f127..cad396318 100644 --- a/src/watcher/watcher.c +++ b/src/watcher/watcher.c @@ -53,6 +53,10 @@ struct cbm_watcher { CBMHashTable *projects; /* name → project_state_t* */ cbm_mutex_t projects_lock; atomic_int stopped; + /* Deferred-free list: freed after the next poll_once. */ + project_state_t **pending_free; + int pending_free_count; + int pending_free_cap; }; /* ── Constants ─────────────────────────────────────────────────── */ @@ -275,6 +279,10 @@ void cbm_watcher_free(cbm_watcher_t *w) { cbm_mutex_lock(&w->projects_lock); cbm_ht_foreach(w->projects, free_state_entry, NULL); cbm_ht_free(w->projects); + for (int i = 0; i < w->pending_free_count; i++) { + state_free(w->pending_free[i]); + } + free(w->pending_free); cbm_mutex_unlock(&w->projects_lock); cbm_mutex_destroy(&w->projects_lock); free(w); @@ -322,7 +330,23 @@ void cbm_watcher_unwatch(cbm_watcher_t *w, const char *project_name) { project_state_t *s = cbm_ht_get(w->projects, project_name); if (s) { cbm_ht_delete(w->projects, project_name); - state_free(s); + /* Defer free: the state may still be referenced by a poll_once + * snapshot taken before we acquired the lock. poll_once will + * drain this list at the start of its next cycle. */ + if (w->pending_free_count >= w->pending_free_cap) { + int new_cap = w->pending_free_cap ? w->pending_free_cap * 2 : 8; + project_state_t **tmp = + realloc(w->pending_free, (size_t)new_cap * sizeof(project_state_t *)); + if (tmp) { + w->pending_free = tmp; + w->pending_free_cap = new_cap; + } + } + if (w->pending_free_count < w->pending_free_cap) { + w->pending_free[w->pending_free_count++] = s; + } else { + state_free(s); /* realloc failed — fall back to immediate free */ + } removed = true; } cbm_mutex_unlock(&w->projects_lock); @@ -484,6 +508,13 @@ int cbm_watcher_poll_once(cbm_watcher_t *w) { * This keeps the critical section small — poll_project does git I/O * and may invoke index_fn which runs the full pipeline. */ cbm_mutex_lock(&w->projects_lock); + + /* Free deferred entries from the previous cycle. */ + for (int i = 0; i < w->pending_free_count; i++) { + state_free(w->pending_free[i]); + } + w->pending_free_count = 0; + int n = cbm_ht_count(w->projects); if (n == 0) { cbm_mutex_unlock(&w->projects_lock);