@@ -149,11 +149,11 @@ struct cbm_daemon_application_job {
149149 cbm_daemon_application_job_t * next ;
150150};
151151
152- /* A watcher-triggered physical job is owned by the exact live sessions that
153- * currently subscribe to its project/root watch. The callback waiting for the
154- * job is only a storage waiter; it is deliberately not an ownership
155- * subscription, so the worker is cancelled when the last matching session
156- * disconnects even while unrelated daemon sessions remain alive . */
152+ /* A watcher-triggered physical job is normally owned by the exact live
153+ * sessions subscribed to its project/root watch. In permanent daemon mode the
154+ * watcher itself additionally owns the physical job so background indexing can
155+ * continue while no client session is connected. The callback remains a
156+ * storage waiter in both modes and releases daemon ownership on completion . */
157157struct cbm_daemon_application_watch_job_subscription {
158158 cbm_daemon_application_session_t * session ;
159159 cbm_daemon_application_job_t * job ;
@@ -424,7 +424,19 @@ static void application_release_session_watch_locked(cbm_daemon_application_sess
424424 if (watch -> subscribers > 0 ) {
425425 watch -> subscribers -- ;
426426 }
427- if (watch -> subscribers == 0 ) {
427+
428+ /*
429+ * A permanent daemon owns the physical project watch independently of
430+ * individual client sessions. Session disconnect releases only that
431+ * session's logical subscription; the watcher remains available for
432+ * background change detection while the daemon generation is alive.
433+ *
434+ * Non-permanent applications retain the historical session-scoped
435+ * lifecycle and remove the physical watch after the final subscriber.
436+ * Project deletion/pruning and daemon shutdown remove persistent watches
437+ * through their dedicated lifecycle paths.
438+ */
439+ if (!session -> application -> permanent && watch -> subscribers == 0 ) {
428440 application_remove_watch_locked (session -> application , watch );
429441 }
430442}
@@ -3375,27 +3387,42 @@ static int application_background_index(cbm_daemon_application_t *application,
33753387 cbm_mutex_lock (& application -> mutex );
33763388 cbm_daemon_application_watch_t * watch =
33773389 require_live_watch ? application_find_watch_locked (application , project_name ) : NULL ;
3378- bool watch_live = !require_live_watch ||
3379- (watch && watch -> subscribers > 0 && strcmp (watch -> root , canonical_root ) == 0 );
3390+ bool watch_live =
3391+ !require_live_watch ||
3392+ (watch && strcmp (watch -> root , canonical_root ) == 0 &&
3393+ (watch -> subscribers > 0 || application -> permanent ));
33803394 size_t watch_owner_count = 0 ;
33813395 bool watch_subscriptions_ok = true;
3396+ bool watcher_owns_job = false;
33823397 cbm_daemon_application_job_t * job =
33833398 watch_live ? application_job_subscribe_locked (application , project_key , canonical_root ,
33843399 args , & subscribe_status )
33853400 : NULL ;
33863401 if (job && require_live_watch ) {
33873402 watch_subscriptions_ok = application_watch_job_subscribe_sessions_locked (
33883403 application , watch , job , & watch_owner_count );
3389- if (watch_subscriptions_ok && watch_owner_count > 0 ) {
3390- job -> watcher_waiters ++ ;
3391- } else if (!watch_subscriptions_ok ) {
3404+
3405+ if (!watch_subscriptions_ok ) {
33923406 subscribe_status = APPLICATION_JOB_SUBSCRIBE_ALLOCATION_FAILED ;
3393- }
3394- /* application_job_subscribe_locked() lends the caller one ordinary
3395- * subscriber. A watcher callback is only a storage waiter: exact live
3396- * session subscriptions above own the physical work. */
3397- application_job_unsubscribe_locked (job );
3398- if (!watch_subscriptions_ok || watch_owner_count == 0 ) {
3407+ application_job_unsubscribe_locked (job );
3408+ job = NULL ;
3409+ } else if (application -> permanent ) {
3410+ /*
3411+ * The ordinary subscription acquired above is retained as daemon
3412+ * ownership. This allows the physical watcher to run indexing even
3413+ * while no client session currently owns the project watch.
3414+ */
3415+ watcher_owns_job = true;
3416+ job -> watcher_waiters ++ ;
3417+ } else if (watch_owner_count > 0 ) {
3418+ /*
3419+ * Historical non-permanent behavior: exact live sessions own the
3420+ * physical work; the watcher callback itself is only a waiter.
3421+ */
3422+ job -> watcher_waiters ++ ;
3423+ application_job_unsubscribe_locked (job );
3424+ } else {
3425+ application_job_unsubscribe_locked (job );
33993426 job = NULL ;
34003427 }
34013428 }
@@ -3428,6 +3455,10 @@ static int application_background_index(cbm_daemon_application_t *application,
34283455 if (job -> watcher_waiters > 0 ) {
34293456 job -> watcher_waiters -- ;
34303457 }
3458+ if (watcher_owns_job ) {
3459+ application_job_unsubscribe_locked (job );
3460+ watcher_owns_job = false;
3461+ }
34313462 } else {
34323463 application_job_unsubscribe_locked (job );
34333464 }
0 commit comments