@@ -146,6 +146,8 @@ struct cbm_daemon_application_job {
146146 bool cancelled ;
147147 bool cancel_requested ;
148148 bool supervision_failed ;
149+ bool attempt_recorded ;
150+ char attempt_id [33 ];
149151 cbm_daemon_application_job_t * next ;
150152};
151153
@@ -1212,6 +1214,63 @@ static void application_record_cancelled(cbm_index_worker_result_t *last_result,
12121214 last_log [0 ] = '\0' ;
12131215}
12141216
1217+ static cbm_index_resource_t application_resource_from_name (const char * name ) {
1218+ for (int resource = CBM_INDEX_RESOURCE_FILES ;
1219+ resource <= CBM_INDEX_RESOURCE_DISCOVERY_DURATION_MS ; resource ++ ) {
1220+ if (name && strcmp (name , cbm_index_resource_name ((cbm_index_resource_t )resource )) == 0 ) {
1221+ return (cbm_index_resource_t )resource ;
1222+ }
1223+ }
1224+ return CBM_INDEX_RESOURCE_NONE ;
1225+ }
1226+
1227+ static bool application_response_error_metadata (const char * response , char * code , size_t code_size ,
1228+ cbm_index_resource_violation_t * violation ) {
1229+ if (code && code_size > 0 ) {
1230+ code [0 ] = '\0' ;
1231+ }
1232+ if (violation ) {
1233+ * violation = (cbm_index_resource_violation_t ){0 };
1234+ }
1235+ yyjson_doc * outer = response ? yyjson_read (response , strlen (response ), 0 ) : NULL ;
1236+ yyjson_val * outer_root = outer ? yyjson_doc_get_root (outer ) : NULL ;
1237+ yyjson_val * is_error =
1238+ outer_root && yyjson_is_obj (outer_root ) ? yyjson_obj_get (outer_root , "isError" ) : NULL ;
1239+ yyjson_val * content =
1240+ outer_root && yyjson_is_obj (outer_root ) ? yyjson_obj_get (outer_root , "content" ) : NULL ;
1241+ yyjson_val * first = content && yyjson_is_arr (content ) ? yyjson_arr_get_first (content ) : NULL ;
1242+ yyjson_val * text = first && yyjson_is_obj (first ) ? yyjson_obj_get (first , "text" ) : NULL ;
1243+ yyjson_doc * inner = text && yyjson_is_str (text )
1244+ ? yyjson_read (yyjson_get_str (text ), yyjson_get_len (text ), 0 )
1245+ : NULL ;
1246+ yyjson_val * inner_root = inner ? yyjson_doc_get_root (inner ) : NULL ;
1247+ yyjson_val * inner_status =
1248+ inner_root && yyjson_is_obj (inner_root ) ? yyjson_obj_get (inner_root , "status" ) : NULL ;
1249+ bool failed = (is_error && yyjson_is_bool (is_error ) && yyjson_get_bool (is_error )) ||
1250+ (inner_status && yyjson_is_str (inner_status ) &&
1251+ strcmp (yyjson_get_str (inner_status ), "error" ) == 0 );
1252+ if (failed && inner_root && yyjson_is_obj (inner_root )) {
1253+ yyjson_val * failure_code = yyjson_obj_get (inner_root , "code" );
1254+ if (code && code_size > 0 && failure_code && yyjson_is_str (failure_code )) {
1255+ (void )snprintf (code , code_size , "%s" , yyjson_get_str (failure_code ));
1256+ }
1257+ yyjson_val * resource = yyjson_obj_get (inner_root , "resource" );
1258+ yyjson_val * observed = yyjson_obj_get (inner_root , "observed" );
1259+ yyjson_val * limit = yyjson_obj_get (inner_root , "limit" );
1260+ if (violation && resource && yyjson_is_str (resource )) {
1261+ violation -> resource = application_resource_from_name (yyjson_get_str (resource ));
1262+ violation -> probe_failed = code && strcmp (code , "resource_probe_failed" ) == 0 ;
1263+ if (observed && yyjson_is_uint (observed ) && limit && yyjson_is_uint (limit )) {
1264+ violation -> observed = yyjson_get_uint (observed );
1265+ violation -> limit = yyjson_get_uint (limit );
1266+ }
1267+ }
1268+ }
1269+ yyjson_doc_free (inner );
1270+ yyjson_doc_free (outer );
1271+ return failed ;
1272+ }
1273+
12151274static bool application_result_is_attributable_failure (
12161275 const application_attempt_t * attempt , cbm_mcp_supervised_result_disposition_t disposition ) {
12171276 return disposition == CBM_MCP_SUPERVISED_RESULT_CONTAINED_FAILURE && attempt -> has_result &&
@@ -1227,6 +1286,7 @@ typedef enum {
12271286typedef struct {
12281287 char * response ;
12291288 bool successful ;
1289+ bool response_is_error ;
12301290 bool unsafe_terminal ;
12311291 bool supervision_failed ;
12321292 cbm_index_worker_result_t last_result ;
@@ -1256,12 +1316,17 @@ static application_attempt_decision_t application_consume_attempt(
12561316 execution -> response = attempt -> result .response ;
12571317 attempt -> result .response = NULL ;
12581318 execution -> successful = execution -> response != NULL ;
1319+ execution -> response_is_error =
1320+ execution -> response &&
1321+ application_response_error_metadata (execution -> response , NULL , 0 , NULL );
12591322 application_attempt_free (attempt );
12601323 return APPLICATION_ATTEMPT_DECISION_SUCCESS ;
12611324 }
12621325 if (disposition == CBM_MCP_SUPERVISED_RESULT_RESOURCE_FAILURE ) {
12631326 execution -> response =
12641327 cbm_mcp_index_worker_resource_response (job -> args_json , & attempt -> result );
1328+ execution -> successful = execution -> response != NULL ;
1329+ execution -> response_is_error = execution -> response != NULL ;
12651330 application_attempt_free (attempt );
12661331 return APPLICATION_ATTEMPT_DECISION_STOP ;
12671332 }
@@ -1461,6 +1526,46 @@ static void application_job_publish(cbm_daemon_application_job_t *job,
14611526 execution -> have_last_result ? & execution -> last_result : NULL ,
14621527 execution -> last_log [0 ] ? execution -> last_log : NULL );
14631528 }
1529+ if (job -> attempt_recorded ) {
1530+ bool cancelled =
1531+ execution -> have_last_result && execution -> last_result .cancellation_requested ;
1532+ bool attempt_completed = execution -> successful && !execution -> response_is_error ;
1533+ const char * state = attempt_completed ? "completed" : cancelled ? "cancelled" : "failed" ;
1534+ const char * failure_code = NULL ;
1535+ const cbm_index_resource_violation_t * violation = NULL ;
1536+ char response_failure_code [CBM_SZ_128 ] = {0 };
1537+ cbm_index_resource_violation_t response_violation = {0 };
1538+ if (!attempt_completed && !cancelled ) {
1539+ if (application_response_error_metadata (execution -> response , response_failure_code ,
1540+ sizeof (response_failure_code ),
1541+ & response_violation )) {
1542+ failure_code = response_failure_code [0 ] ? response_failure_code : "index_failed" ;
1543+ if (response_violation .resource != CBM_INDEX_RESOURCE_NONE ) {
1544+ violation = & response_violation ;
1545+ }
1546+ } else if (execution -> have_last_result &&
1547+ execution -> last_result .resource_violation .resource !=
1548+ CBM_INDEX_RESOURCE_NONE ) {
1549+ violation = & execution -> last_result .resource_violation ;
1550+ failure_code =
1551+ violation -> probe_failed ? "resource_probe_failed" : "resource_limit_exceeded" ;
1552+ } else if (execution -> supervision_failed || execution -> unsafe_terminal ) {
1553+ failure_code = "supervision_failed" ;
1554+ } else if (!execution -> have_last_result ||
1555+ execution -> last_result .outcome == CBM_PROC_SPAWN_FAILED ) {
1556+ failure_code = "worker_start_failed" ;
1557+ } else {
1558+ failure_code = "index_failed" ;
1559+ }
1560+ } else if (cancelled ) {
1561+ failure_code = "cancelled" ;
1562+ }
1563+ if (!cbm_mcp_index_attempt_transition (job -> project_key , job -> root_path , job -> attempt_id ,
1564+ state , failure_code , violation , attempt_completed )) {
1565+ cbm_log_warn ("daemon.index.attempt_record" , "project" , job -> project_key , "state" ,
1566+ "terminal_write_failed" );
1567+ }
1568+ }
14641569 cbm_daemon_application_t * application = job -> application ;
14651570 cbm_mutex_lock (& application -> mutex );
14661571 job -> response = execution -> response ;
@@ -1508,6 +1613,12 @@ static void *application_job_thread(void *opaque) {
15081613 if (!application_job_wait_for_mutations (job )) {
15091614 application_job_execution_cancel (& execution );
15101615 } else {
1616+ if (job -> attempt_recorded &&
1617+ !cbm_mcp_index_attempt_transition (job -> project_key , job -> root_path , job -> attempt_id ,
1618+ "running" , NULL , NULL , false)) {
1619+ cbm_log_warn ("daemon.index.attempt_record" , "project" , job -> project_key , "state" ,
1620+ "running_write_failed" );
1621+ }
15111622 application_attempt_t attempt ;
15121623 application_attempt_status_t status =
15131624 application_job_run_attempt (job , NULL , NULL , & attempt );
@@ -1556,6 +1667,18 @@ static char *application_index_project_key(const char *root_path, const char *ar
15561667 return key ;
15571668}
15581669
1670+ static const char * application_index_origin (const char * args_json , char * * owned_origin ) {
1671+ * owned_origin = cbm_mcp_get_string_arg (args_json , "_cbm_index_origin" );
1672+ if (* owned_origin &&
1673+ (strcmp (* owned_origin , "auto" ) == 0 || strcmp (* owned_origin , "watcher" ) == 0 ||
1674+ strcmp (* owned_origin , "explicit" ) == 0 )) {
1675+ return * owned_origin ;
1676+ }
1677+ free (* owned_origin );
1678+ * owned_origin = NULL ;
1679+ return "explicit" ;
1680+ }
1681+
15591682static size_t application_active_job_count_locked (cbm_daemon_application_t * application ) {
15601683 size_t count = 0 ;
15611684 for (cbm_daemon_application_job_t * job = application -> jobs ; job ; job = job -> next ) {
@@ -1587,6 +1710,7 @@ static bool application_index_args_normalize_defaults(yyjson_mut_val *root) {
15871710 if (name && (!yyjson_mut_is_str (name ) || yyjson_mut_get_len (name ) == 0 )) {
15881711 (void )yyjson_mut_obj_remove_key (root , "name" );
15891712 }
1713+ (void )yyjson_mut_obj_remove_key (root , "_cbm_index_origin" );
15901714 return true;
15911715}
15921716
@@ -1619,6 +1743,9 @@ static cbm_daemon_application_job_t *application_job_subscribe_locked(
16191743 if (application -> stopping ) {
16201744 return NULL ;
16211745 }
1746+ char * requested_origin = cbm_mcp_get_string_arg (args_json , "_cbm_index_origin" );
1747+ bool watcher_request = requested_origin && strcmp (requested_origin , "watcher" ) == 0 ;
1748+ free (requested_origin );
16221749 cbm_daemon_application_job_t * job =
16231750 application_find_active_job_locked (application , project_key );
16241751 if (job ) {
@@ -1631,6 +1758,9 @@ static cbm_daemon_application_job_t *application_job_subscribe_locked(
16311758 return NULL ;
16321759 }
16331760 job -> subscribers ++ ;
1761+ if (watcher_request && job -> attempt_recorded ) {
1762+ (void )cbm_mcp_index_attempt_mark_watcher_change (job -> project_key , job -> attempt_id );
1763+ }
16341764 * status_out = APPLICATION_JOB_SUBSCRIBE_OK ;
16351765 return job ;
16361766 }
@@ -1656,6 +1786,20 @@ static cbm_daemon_application_job_t *application_job_subscribe_locked(
16561786 }
16571787 job -> application = application ;
16581788 job -> subscribers = 1 ;
1789+ cbm_index_resource_policy_t policy ;
1790+ char policy_error [CBM_SZ_256 ] = {0 };
1791+ char * owned_origin = NULL ;
1792+ const char * origin = application_index_origin (args_json , & owned_origin );
1793+ if (cbm_mcp_index_policy_from_internal_args (args_json , & policy , policy_error ,
1794+ sizeof (policy_error ))) {
1795+ job -> attempt_recorded =
1796+ cbm_mcp_index_attempt_begin (project_key , root_path , origin , & policy ,
1797+ strcmp (origin , "watcher" ) == 0 , job -> attempt_id );
1798+ }
1799+ free (owned_origin );
1800+ if (!job -> attempt_recorded ) {
1801+ cbm_log_warn ("daemon.index.attempt_record" , "project" , project_key , "state" , "unavailable" );
1802+ }
16591803 job -> next = application -> jobs ;
16601804 application -> jobs = job ;
16611805 if (application_job_thread_create (& job -> thread , job ) == 0 ) {
@@ -1666,6 +1810,11 @@ static cbm_daemon_application_job_t *application_job_subscribe_locked(
16661810 * reservation back synchronously and let background callers retry. */
16671811 application -> jobs = job -> next ;
16681812 job -> next = NULL ;
1813+ if (job -> attempt_recorded ) {
1814+ (void )cbm_mcp_index_attempt_transition (job -> project_key , job -> root_path ,
1815+ job -> attempt_id , "failed" , "worker_start_failed" ,
1816+ NULL , false);
1817+ }
16691818 application_job_free (job );
16701819 * status_out = APPLICATION_JOB_SUBSCRIBE_UNAVAILABLE ;
16711820 cbm_log_warn ("daemon.index.thread_start_failed" , "action" , "retry" );
@@ -1985,6 +2134,7 @@ static char *application_auto_index_args(cbm_daemon_application_t *application,
19852134 }
19862135 yyjson_mut_doc_set_root (document , root );
19872136 char * args = yyjson_mut_obj_add_strcpy (document , root , "repo_path" , root_path ) &&
2137+ yyjson_mut_obj_add_str (document , root , "_cbm_index_origin" , "auto" ) &&
19882138 application_index_args_add_policy (application , document , root )
19892139 ? yyjson_mut_write (document , 0 , NULL )
19902140 : NULL ;
@@ -3013,6 +3163,7 @@ cbm_daemon_application_t *cbm_daemon_application_new(
30133163 application -> watcher , application_watcher_mutation_begin ,
30143164 application_watcher_mutation_end , application_watcher_project_pruned , application );
30153165 }
3166+ cbm_mcp_index_attempt_recover_abandoned ();
30163167 return application ;
30173168}
30183169
@@ -3437,6 +3588,8 @@ static int application_background_index(cbm_daemon_application_t *application,
34373588 }
34383589 yyjson_mut_doc_set_root (document , root );
34393590 bool encoded = yyjson_mut_obj_add_strcpy (document , root , "repo_path" , canonical_root ) &&
3591+ yyjson_mut_obj_add_str (document , root , "_cbm_index_origin" ,
3592+ require_live_watch ? "watcher" : "explicit" ) &&
34403593 application_index_args_add_policy (application , document , root );
34413594 char * default_project = cbm_project_name_from_path (canonical_root );
34423595 bool custom_project =
0 commit comments