Skip to main content

feagi_services/impls/
genome_service_impl.rs

1// Copyright 2025 Neuraville Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4/*!
5Genome service implementation.
6
7Copyright 2025 Neuraville Inc.
8Licensed under the Apache License, Version 2.0
9*/
10
11use crate::traits::GenomeService;
12use crate::types::*;
13use async_trait::async_trait;
14use feagi_brain_development::models::CorticalAreaExt;
15use feagi_brain_development::neuroembryogenesis::Neuroembryogenesis;
16use feagi_brain_development::ConnectomeManager;
17use feagi_evolutionary::{get_default_neural_properties, MemoryAreaProperties};
18use feagi_npu_burst_engine::{BurstLoopRunner, ParameterUpdateQueue};
19use feagi_structures::genomic::cortical_area::descriptors::{
20    CorticalSubUnitIndex, CorticalUnitIndex,
21};
22use feagi_structures::genomic::cortical_area::io_cortical_area_configuration_flag::{
23    FrameChangeHandling, PercentageNeuronPositioning,
24};
25use feagi_structures::genomic::cortical_area::{
26    CorticalArea, CorticalAreaDimensions, CorticalAreaType, CorticalID,
27    IOCorticalAreaConfigurationFlag,
28};
29use feagi_structures::genomic::{MotorCorticalUnit, SensoryCorticalUnit};
30use parking_lot::RwLock;
31use serde_json::Value;
32use std::collections::HashMap;
33use std::sync::Arc;
34use tracing::{info, trace, warn};
35
36use crate::genome::{ChangeType, CorticalChangeClassifier};
37
38fn frame_handling_label(frame: FrameChangeHandling) -> &'static str {
39    match frame {
40        FrameChangeHandling::Absolute => "Absolute",
41        FrameChangeHandling::Incremental => "Incremental",
42    }
43}
44
45fn positioning_label(positioning: PercentageNeuronPositioning) -> &'static str {
46    match positioning {
47        PercentageNeuronPositioning::Linear => "Linear",
48        PercentageNeuronPositioning::Fractional => "Fractional",
49    }
50}
51
52fn signage_label_from_flag(flag: &IOCorticalAreaConfigurationFlag) -> &'static str {
53    match flag {
54        IOCorticalAreaConfigurationFlag::SignedPercentage(..)
55        | IOCorticalAreaConfigurationFlag::SignedPercentage2D(..)
56        | IOCorticalAreaConfigurationFlag::SignedPercentage3D(..)
57        | IOCorticalAreaConfigurationFlag::SignedPercentage4D(..) => "Percentage Signed",
58        IOCorticalAreaConfigurationFlag::Percentage(..)
59        | IOCorticalAreaConfigurationFlag::Percentage2D(..)
60        | IOCorticalAreaConfigurationFlag::Percentage3D(..)
61        | IOCorticalAreaConfigurationFlag::Percentage4D(..) => "Percentage Unsigned",
62        IOCorticalAreaConfigurationFlag::CartesianPlane(..) => "Cartesian Plane",
63        IOCorticalAreaConfigurationFlag::Misc(..) => "Misc",
64        IOCorticalAreaConfigurationFlag::Boolean => "Boolean",
65    }
66}
67
68/// Merge default template and memory properties into provided values.
69/// Existing values always override defaults.
70fn merge_memory_area_properties(
71    base: HashMap<String, Value>,
72    extra: Option<&HashMap<String, Value>>,
73) -> HashMap<String, Value> {
74    let mut defaults = get_default_neural_properties();
75    let memory_defaults = MemoryAreaProperties::default();
76    defaults
77        .entry("cortical_group".to_string())
78        .or_insert(Value::from("MEMORY"));
79    defaults
80        .entry("is_mem_type".to_string())
81        .or_insert(Value::from(true));
82    defaults
83        .entry("temporal_depth".to_string())
84        .or_insert(Value::from(memory_defaults.temporal_depth));
85    defaults
86        .entry("longterm_mem_threshold".to_string())
87        .or_insert(Value::from(memory_defaults.longterm_threshold));
88    defaults
89        .entry("lifespan_growth_rate".to_string())
90        .or_insert(Value::from(memory_defaults.lifespan_growth_rate));
91    defaults
92        .entry("init_lifespan".to_string())
93        .or_insert(Value::from(memory_defaults.init_lifespan));
94    defaults
95        .entry("psp_uniform_distribution".to_string())
96        .or_insert(Value::from(true));
97
98    defaults.extend(base);
99    if let Some(extra_props) = extra {
100        defaults.extend(extra_props.clone());
101    }
102    defaults
103}
104
105fn behavior_label_from_flag(flag: &IOCorticalAreaConfigurationFlag) -> &'static str {
106    match flag {
107        IOCorticalAreaConfigurationFlag::Boolean => "Not Applicable",
108        IOCorticalAreaConfigurationFlag::CartesianPlane(frame)
109        | IOCorticalAreaConfigurationFlag::Misc(frame)
110        | IOCorticalAreaConfigurationFlag::Percentage(frame, _)
111        | IOCorticalAreaConfigurationFlag::Percentage2D(frame, _)
112        | IOCorticalAreaConfigurationFlag::Percentage3D(frame, _)
113        | IOCorticalAreaConfigurationFlag::Percentage4D(frame, _)
114        | IOCorticalAreaConfigurationFlag::SignedPercentage(frame, _)
115        | IOCorticalAreaConfigurationFlag::SignedPercentage2D(frame, _)
116        | IOCorticalAreaConfigurationFlag::SignedPercentage3D(frame, _)
117        | IOCorticalAreaConfigurationFlag::SignedPercentage4D(frame, _) => {
118            frame_handling_label(*frame)
119        }
120    }
121}
122
123fn coding_type_label_from_flag(flag: &IOCorticalAreaConfigurationFlag) -> &'static str {
124    match flag {
125        IOCorticalAreaConfigurationFlag::Percentage(_, positioning)
126        | IOCorticalAreaConfigurationFlag::Percentage2D(_, positioning)
127        | IOCorticalAreaConfigurationFlag::Percentage3D(_, positioning)
128        | IOCorticalAreaConfigurationFlag::Percentage4D(_, positioning)
129        | IOCorticalAreaConfigurationFlag::SignedPercentage(_, positioning)
130        | IOCorticalAreaConfigurationFlag::SignedPercentage2D(_, positioning)
131        | IOCorticalAreaConfigurationFlag::SignedPercentage3D(_, positioning)
132        | IOCorticalAreaConfigurationFlag::SignedPercentage4D(_, positioning) => {
133            positioning_label(*positioning)
134        }
135        IOCorticalAreaConfigurationFlag::CartesianPlane(..)
136        | IOCorticalAreaConfigurationFlag::Misc(..)
137        | IOCorticalAreaConfigurationFlag::Boolean => "Not Applicable",
138    }
139}
140
141fn io_unit_reference_from_cortical_id(cortical_id: &CorticalID) -> Option<[u8; 3]> {
142    let bytes = cortical_id.as_bytes();
143    if bytes[0] != b'i' && bytes[0] != b'o' {
144        return None;
145    }
146    Some([bytes[1], bytes[2], bytes[3]])
147}
148
149fn io_coding_options_for_unit(cortical_id: &CorticalID) -> Option<IOCodingOptions> {
150    let unit_ref = io_unit_reference_from_cortical_id(cortical_id)?;
151    let is_input = cortical_id.as_bytes()[0] == b'i';
152
153    let (accepted_type, allowed_frames) = if is_input {
154        let unit = SensoryCorticalUnit::list_all()
155            .iter()
156            .find(|u| u.get_cortical_id_unit_reference() == unit_ref)?;
157        (
158            unit.get_accepted_wrapped_io_data_type(),
159            unit.get_allowed_frame_change_handling(),
160        )
161    } else {
162        let unit = MotorCorticalUnit::list_all()
163            .iter()
164            .find(|u| u.get_cortical_id_unit_reference() == unit_ref)?;
165        (
166            unit.get_accepted_wrapped_io_data_type(),
167            unit.get_allowed_frame_change_handling(),
168        )
169    };
170
171    let mut signage_options = Vec::new();
172    let mut behavior_options = Vec::new();
173    let mut coding_type_options = Vec::new();
174
175    let io_flag = match cortical_id.extract_io_data_flag() {
176        Ok(flag) => flag,
177        Err(err) => {
178            warn!(
179                target: "feagi-services",
180                "[IO-CODING] {} failed to extract io_flag: {} (accepted_type={})",
181                cortical_id,
182                err,
183                accepted_type
184            );
185            return None;
186        }
187    };
188    signage_options.push(signage_label_from_flag(&io_flag).to_string());
189
190    let supports_frame_handling = !matches!(io_flag, IOCorticalAreaConfigurationFlag::Boolean);
191    if supports_frame_handling {
192        if let Some(frames) = allowed_frames {
193            for frame in frames {
194                behavior_options.push(frame_handling_label(*frame).to_string());
195            }
196        } else {
197            behavior_options.push("Absolute".to_string());
198            behavior_options.push("Incremental".to_string());
199        }
200    } else {
201        behavior_options.push("Not Applicable".to_string());
202    }
203
204    let supports_positioning = matches!(
205        io_flag,
206        IOCorticalAreaConfigurationFlag::Percentage(..)
207            | IOCorticalAreaConfigurationFlag::Percentage2D(..)
208            | IOCorticalAreaConfigurationFlag::Percentage3D(..)
209            | IOCorticalAreaConfigurationFlag::Percentage4D(..)
210            | IOCorticalAreaConfigurationFlag::SignedPercentage(..)
211            | IOCorticalAreaConfigurationFlag::SignedPercentage2D(..)
212            | IOCorticalAreaConfigurationFlag::SignedPercentage3D(..)
213            | IOCorticalAreaConfigurationFlag::SignedPercentage4D(..)
214    );
215    if supports_positioning {
216        coding_type_options.push("Linear".to_string());
217        coding_type_options.push("Fractional".to_string());
218    } else {
219        coding_type_options.push("Not Applicable".to_string());
220    }
221
222    if signage_options.is_empty() {
223        warn!(
224            target: "feagi-services",
225            "[IO-CODING] {} empty signage_options (accepted_type={}, io_flag={:?})",
226            cortical_id,
227            accepted_type,
228            io_flag
229        );
230    }
231    Some(IOCodingOptions {
232        signage_options,
233        behavior_options,
234        coding_type_options,
235    })
236}
237
238/// Default implementation of GenomeService
239pub struct GenomeServiceImpl {
240    connectome: Arc<RwLock<ConnectomeManager>>,
241    parameter_queue: Option<ParameterUpdateQueue>,
242    /// Currently loaded genome (source of truth for structural changes)
243    /// This is updated when genome is loaded or when cortical areas are modified
244    current_genome: Arc<RwLock<Option<feagi_evolutionary::RuntimeGenome>>>,
245    /// Counter tracking how many genomes have been loaded (increments on each load)
246    genome_load_counter: Arc<RwLock<i32>>,
247    /// Timestamp of when the current genome was loaded
248    genome_load_timestamp: Arc<RwLock<Option<i64>>>,
249    /// Optional burst runner for refreshing cortical_id cache
250    burst_runner: Option<Arc<RwLock<BurstLoopRunner>>>,
251}
252
253impl GenomeServiceImpl {
254    pub fn new(connectome: Arc<RwLock<ConnectomeManager>>) -> Self {
255        Self {
256            connectome,
257            parameter_queue: None,
258            current_genome: Arc::new(RwLock::new(None)),
259            genome_load_counter: Arc::new(RwLock::new(0)),
260            genome_load_timestamp: Arc::new(RwLock::new(None)),
261            burst_runner: None,
262        }
263    }
264
265    pub fn new_with_parameter_queue(
266        connectome: Arc<RwLock<ConnectomeManager>>,
267        parameter_queue: ParameterUpdateQueue,
268    ) -> Self {
269        Self {
270            connectome,
271            parameter_queue: Some(parameter_queue),
272            current_genome: Arc::new(RwLock::new(None)),
273            genome_load_counter: Arc::new(RwLock::new(0)),
274            genome_load_timestamp: Arc::new(RwLock::new(None)),
275            burst_runner: None,
276        }
277    }
278
279    /// Set the burst runner for cache refresh
280    pub fn set_burst_runner(&mut self, burst_runner: Arc<RwLock<BurstLoopRunner>>) {
281        self.burst_runner = Some(burst_runner);
282    }
283
284    /// Refresh cortical_id cache in burst runner
285    fn refresh_burst_runner_cache(&self) {
286        if let Some(ref burst_runner) = self.burst_runner {
287            let manager = self.connectome.read();
288            let mappings = manager.get_all_cortical_idx_to_id_mappings();
289            let chunk_sizes = manager.get_all_visualization_granularities();
290            let mapping_count = mappings.len();
291            let burst_runner_write = burst_runner.write();
292            burst_runner_write.refresh_cortical_id_mappings(mappings);
293            burst_runner_write.refresh_visualization_granularities(chunk_sizes);
294            info!(target: "feagi-services", "Refreshed burst runner cache with {} cortical areas", mapping_count);
295        }
296    }
297
298    /// Get a reference to the current genome Arc
299    /// This allows other services to share access to the genome for persistence
300    pub fn get_current_genome_arc(&self) -> Arc<RwLock<Option<feagi_evolutionary::RuntimeGenome>>> {
301        Arc::clone(&self.current_genome)
302    }
303}
304
305#[async_trait]
306impl GenomeService for GenomeServiceImpl {
307    async fn load_genome(&self, params: LoadGenomeParams) -> ServiceResult<GenomeInfo> {
308        info!(target: "feagi-services", "Loading genome from JSON");
309
310        // Parse genome using feagi-evo (this is CPU-bound, but relatively fast)
311        let mut genome = feagi_evolutionary::load_genome_from_json(&params.json_str)
312            .map_err(|e| ServiceError::InvalidInput(format!("Failed to parse genome: {}", e)))?;
313        let (_areas_added, morphs_added) = feagi_evolutionary::ensure_core_components(&mut genome);
314        if morphs_added > 0 {
315            info!(
316                target: "feagi-services",
317                "Added {} missing core morphologies during genome load",
318                morphs_added
319            );
320        }
321
322        // Extract simulation_timestep from genome physiology (will be returned in GenomeInfo)
323        let simulation_timestep = genome.physiology.simulation_timestep;
324        info!(target: "feagi-services", "Genome simulation_timestep: {} seconds", simulation_timestep);
325
326        // Store genome for future updates (source of truth for structural changes)
327        info!(target: "feagi-services", "Storing RuntimeGenome with {} cortical areas, {} morphologies",
328            genome.cortical_areas.len(), genome.morphologies.iter().count());
329        *self.current_genome.write() = Some(genome.clone());
330
331        // Increment genome load counter and set timestamp
332        let genome_num = {
333            let mut counter = self.genome_load_counter.write();
334            *counter += 1;
335            *counter
336        };
337
338        let genome_timestamp = std::time::SystemTime::now()
339            .duration_since(std::time::UNIX_EPOCH)
340            .ok()
341            .map(|d| d.as_secs() as i64);
342
343        *self.genome_load_timestamp.write() = genome_timestamp;
344
345        info!(target: "feagi-services", "Genome load #{}, timestamp: {:?}", genome_num, genome_timestamp);
346
347        // Load into connectome via ConnectomeManager
348        // This involves synaptogenesis which can be CPU-intensive, so run it on a blocking thread
349        // CRITICAL: Add timeout to prevent hanging during shutdown
350        // Note: spawn_blocking tasks cannot be cancelled, but timeout ensures we don't wait forever
351        // CRITICAL FIX: Don't hold write lock during entire operation - let neuroembryogenesis manage locks
352        // This prevents deadlock when neuroembryogenesis tries to acquire its own write locks
353        let connectome_clone = self.connectome.clone();
354        let blocking_handle = tokio::task::spawn_blocking(
355            move || -> Result<feagi_brain_development::neuroembryogenesis::DevelopmentProgress, ServiceError> {
356                // Acquire write lock only for prepare/resize operations
357                let mut genome_clone = genome;
358                let (prepare_result, resize_result) = {
359                    let mut manager = connectome_clone.write();
360                    let prepare_result = manager.prepare_for_new_genome();
361                    let resize_result = prepare_result
362                        .as_ref()
363                        .ok()
364                        .map(|_| manager.resize_for_genome(&genome_clone));
365                    (prepare_result, resize_result)
366                }; // Lock released here
367
368                prepare_result.map_err(|e| {
369                    tracing::error!(target: "feagi-services", "prepare_for_new_genome failed: {}", e);
370                    ServiceError::from(e)
371                })?;
372                if let Some(resize_result) = resize_result {
373                    resize_result.map_err(|e| {
374                        tracing::error!(target: "feagi-services", "resize_for_genome failed: {}", e);
375                        ServiceError::from(e)
376                    })?;
377                }
378
379                // Now call develop_from_genome without holding the lock
380                // It will acquire its own locks internally
381                let manager_arc = feagi_brain_development::ConnectomeManager::instance();
382                let mut neuro = Neuroembryogenesis::new(manager_arc.clone());
383
384                let develop_result = neuro.develop_from_genome(&genome_clone);
385                let develop_result = match develop_result {
386                    Ok(()) => Ok(()),
387                    Err(e) => {
388                        let err_str = e.to_string();
389                        let is_already_exists = err_str.contains("already exists");
390                        if is_already_exists {
391                            tracing::warn!(
392                                target: "feagi-services",
393                                "Neuroembryogenesis failed with 'already exists' - clearing connectome and retrying once: {}",
394                                err_str
395                            );
396                            // Re-prepare and retry: handle stale connectome state.
397                            let (retry_prepare, retry_resize) = {
398                                let mut manager = connectome_clone.write();
399                                let prep = manager.prepare_for_new_genome();
400                                let resize = prep.as_ref().ok().map(|_| manager.resize_for_genome(&genome_clone));
401                                (prep, resize)
402                            };
403                            retry_prepare.map_err(|e| {
404                                tracing::error!(target: "feagi-services", "Retry prepare_for_new_genome failed: {}", e);
405                                ServiceError::from(e)
406                            })?;
407                            if let Some(res) = retry_resize {
408                                res.map_err(|e| {
409                                    tracing::error!(target: "feagi-services", "Retry resize_for_genome failed: {}", e);
410                                    ServiceError::from(e)
411                                })?;
412                            }
413                            let mut neuro_retry = Neuroembryogenesis::new(manager_arc.clone());
414                            neuro_retry.develop_from_genome(&genome_clone).map_err(|e| {
415                                tracing::error!(target: "feagi-services", "Neuroembryogenesis retry failed: {}", e);
416                                ServiceError::Backend(format!(
417                                    "Neuroembryogenesis failed: {}. Retry after connectome reset also failed.",
418                                    err_str
419                                ))
420                            })?;
421                            neuro = neuro_retry;
422                            Ok(())
423                        } else {
424                            tracing::error!(target: "feagi-services", "Neuroembryogenesis failed: {}", e);
425                            Err(ServiceError::Backend(format!("Neuroembryogenesis failed: {}", e)))
426                        }
427                    }
428                };
429                develop_result?;
430
431                // Ensure core cortical areas exist after neuroembryogenesis
432                // (they may have been added during corticogenesis, but we ensure they exist)
433                {
434                    let mut manager = manager_arc.write();
435                    manager.ensure_core_cortical_areas().map_err(|e| {
436                        tracing::error!(target: "feagi-services", "ensure_core_cortical_areas failed: {}", e);
437                        ServiceError::Backend(format!("Failed to ensure core cortical areas: {}", e))
438                    })?;
439                }
440
441                // After neuroembryogenesis, update genome metadata with root_region_id
442                let root_region_id = manager_arc.read().get_root_region_id();
443                if let Some(root_id) = root_region_id {
444                    genome_clone.metadata.brain_regions_root = Some(root_id);
445                    info!(target: "feagi-services", "✅ Set genome brain_regions_root: {}", genome_clone.metadata.brain_regions_root.as_ref().unwrap());
446                } else {
447                    warn!(target: "feagi-services", "⚠️ No root region found after neuroembryogenesis");
448                }
449
450                Ok(neuro.get_progress())
451            },
452        );
453
454        // Wait with timeout - if timeout expires, abort the blocking task
455        let progress = match tokio::time::timeout(
456            tokio::time::Duration::from_secs(300), // 5 minute timeout
457            blocking_handle,
458        )
459        .await
460        {
461            Ok(Ok(result)) => result?,
462            Ok(Err(e)) => {
463                return Err(ServiceError::Backend(format!(
464                    "Blocking task panicked: {}",
465                    e
466                )))
467            }
468            Err(_) => {
469                // Timeout expired - abort the task (though it may continue running)
470                warn!(target: "feagi-services", "Genome loading timed out after 5 minutes - aborting");
471                return Err(ServiceError::Backend(
472                    "Genome loading timed out after 5 minutes".to_string(),
473                ));
474            }
475        };
476
477        info!(
478            target: "feagi-services",
479            "Genome loaded: {} cortical areas, {} neurons, {} synapses created",
480            progress.cortical_areas_created,
481            progress.neurons_created,
482            progress.synapses_created
483        );
484
485        // CRITICAL: Sync auto-generated brain regions back to RuntimeGenome
486        // BDU may auto-generate brain regions if the genome didn't have any.
487        // We need to sync these back to current_genome so they're included when saving.
488        let brain_regions_from_bdu = {
489            let manager = self.connectome.read();
490            let hierarchy = manager.get_brain_region_hierarchy();
491            hierarchy.get_all_regions()
492        };
493
494        if !brain_regions_from_bdu.is_empty() {
495            let mut current_genome_guard = self.current_genome.write();
496            if let Some(ref mut genome) = *current_genome_guard {
497                // Only update if BDU has more regions (handles auto-generation case)
498                if brain_regions_from_bdu.len() > genome.brain_regions.len() {
499                    info!(
500                        target: "feagi-services",
501                        "Syncing {} auto-generated brain regions from BDU to RuntimeGenome",
502                        brain_regions_from_bdu.len()
503                    );
504                    genome.brain_regions = brain_regions_from_bdu;
505                }
506            }
507        }
508
509        // Return genome info with simulation_timestep
510        let (cortical_area_count, brain_region_count) = {
511            let manager = self.connectome.read();
512            let cortical_area_count = manager.get_cortical_area_count();
513            let brain_region_ids = manager.get_brain_region_ids();
514            let brain_region_count = brain_region_ids.len();
515            (cortical_area_count, brain_region_count)
516        };
517
518        // Refresh burst runner cache after genome load
519        self.refresh_burst_runner_cache();
520
521        Ok(GenomeInfo {
522            genome_id: "current".to_string(),
523            genome_title: "Current Genome".to_string(),
524            version: "2.1".to_string(),
525            cortical_area_count,
526            brain_region_count,
527            simulation_timestep,          // From genome physiology
528            genome_num: Some(genome_num), // Actual load counter
529            genome_timestamp,             // Timestamp when genome was loaded
530        })
531    }
532
533    async fn save_genome(&self, params: SaveGenomeParams) -> ServiceResult<String> {
534        info!(target: "feagi-services", "Saving genome to JSON");
535
536        // Check if we have a RuntimeGenome stored (includes morphologies, physiology, etc.)
537        let genome_opt = self.current_genome.read().clone();
538
539        let mut genome = genome_opt.ok_or_else(|| {
540            ServiceError::Internal(
541                "No RuntimeGenome stored. Genome must be loaded via load_genome() before it can be saved.".to_string()
542            )
543        })?;
544
545        info!(target: "feagi-services", "✅ RuntimeGenome loaded, exporting in flat format v3.0");
546
547        // Update metadata if provided
548        if let Some(id) = params.genome_id {
549            genome.metadata.genome_id = id;
550        }
551        if let Some(title) = params.genome_title {
552            genome.metadata.genome_title = title;
553        }
554
555        // Use the full RuntimeGenome saver (produces flat format v3.0)
556        let json_str = feagi_evolutionary::save_genome_to_json(&genome)
557            .map_err(|e| ServiceError::Internal(format!("Failed to save genome: {}", e)))?;
558
559        info!(target: "feagi-services", "✅ Genome exported successfully (flat format v3.0)");
560        Ok(json_str)
561    }
562
563    async fn export_region_genome(&self, region_id: String) -> ServiceResult<String> {
564        let genome = self.current_genome.read().clone().ok_or_else(|| {
565            ServiceError::Internal(
566                "No RuntimeGenome stored. Genome must be loaded before exporting a region."
567                    .to_string(),
568            )
569        })?;
570        let subset =
571            feagi_evolutionary::subset_runtime_genome_for_region_branch(&genome, &region_id)
572                .map_err(|e| match e {
573                    feagi_evolutionary::EvoError::InvalidRegion(msg) => {
574                        ServiceError::InvalidInput(msg)
575                    }
576                    other => ServiceError::Internal(other.to_string()),
577                })?;
578        feagi_evolutionary::save_genome_to_json(&subset).map_err(|e| {
579            ServiceError::Internal(format!("Failed to serialize region genome: {}", e))
580        })
581    }
582
583    async fn get_genome_info(&self) -> ServiceResult<GenomeInfo> {
584        trace!(target: "feagi-services", "Getting genome info");
585
586        // CRITICAL: Minimize lock scope - drop lock immediately after reading values
587        let (cortical_area_count, brain_region_count) = {
588            let manager = self.connectome.read();
589            let cortical_area_count = manager.get_cortical_area_count();
590            let brain_region_ids = manager.get_brain_region_ids();
591            let brain_region_count = brain_region_ids.len();
592            trace!(
593                target: "feagi-services",
594                "Reading genome info: {} cortical areas, {} brain regions",
595                cortical_area_count,
596                brain_region_count
597            );
598            trace!(
599                target: "feagi-services",
600                "Brain region IDs: {:?}",
601                brain_region_ids.iter().take(10).collect::<Vec<_>>()
602            );
603            (cortical_area_count, brain_region_count)
604        }; // Lock dropped here
605
606        // Get simulation_timestep from stored genome if available
607        let simulation_timestep = {
608            let genome_opt = self.current_genome.read();
609            genome_opt
610                .as_ref()
611                .map(|g| g.physiology.simulation_timestep)
612                .unwrap_or(0.025) // Default if no genome loaded
613        };
614
615        // Get actual genome load counter and timestamp
616        let genome_num = {
617            let counter = self.genome_load_counter.read();
618            if *counter > 0 {
619                Some(*counter)
620            } else {
621                None // No genome loaded yet
622            }
623        };
624
625        let genome_timestamp = *self.genome_load_timestamp.read();
626
627        Ok(GenomeInfo {
628            genome_id: "current".to_string(),
629            genome_title: "Current Genome".to_string(),
630            version: "2.1".to_string(),
631            cortical_area_count,
632            brain_region_count,
633            simulation_timestep,
634            genome_num,
635            genome_timestamp,
636        })
637    }
638
639    async fn validate_genome(&self, json_str: String) -> ServiceResult<bool> {
640        trace!(target: "feagi-services", "Validating genome JSON");
641
642        // Parse genome
643        let genome = feagi_evolutionary::load_genome_from_json(&json_str)
644            .map_err(|e| ServiceError::InvalidInput(format!("Failed to parse genome: {}", e)))?;
645
646        // Validate genome structure
647        let validation = feagi_evolutionary::validate_genome(&genome);
648
649        if !validation.errors.is_empty() {
650            return Err(ServiceError::InvalidInput(format!(
651                "Genome validation failed: {} errors, {} warnings. First error: {}",
652                validation.errors.len(),
653                validation.warnings.len(),
654                validation
655                    .errors
656                    .first()
657                    .unwrap_or(&"Unknown error".to_string())
658            )));
659        }
660
661        Ok(true)
662    }
663
664    async fn reset_connectome(&self) -> ServiceResult<()> {
665        info!(target: "feagi-services", "Resetting connectome");
666
667        // Use ConnectomeManager's prepare_for_new_genome method
668        self.connectome
669            .write()
670            .prepare_for_new_genome()
671            .map_err(ServiceError::from)?;
672
673        info!(target: "feagi-services", "Connectome reset complete");
674        Ok(())
675    }
676
677    async fn create_cortical_areas(
678        &self,
679        params: Vec<CreateCorticalAreaParams>,
680    ) -> ServiceResult<Vec<CorticalAreaInfo>> {
681        info!(target: "feagi-services", "Creating {} new cortical areas via GenomeService", params.len());
682
683        // IO areas (IPU/OPU) must always be in root region; get root ID for default
684        let root_region_id = self.connectome.read().get_root_region_id();
685
686        // Step 1: Build CorticalArea structures
687        let mut areas_to_add = Vec::new();
688        for param in &params {
689            // Convert String to CorticalID
690            let cortical_id_typed = CorticalID::try_from_base_64(&param.cortical_id)
691                .map_err(|e| ServiceError::InvalidInput(format!("Invalid cortical ID: {}", e)))?;
692
693            // Get cortical area type from the cortical ID
694            let area_type = cortical_id_typed.as_cortical_type().map_err(|e| {
695                ServiceError::InvalidInput(format!("Failed to determine cortical area type: {}", e))
696            })?;
697
698            // Create CorticalArea
699            let mut area = CorticalArea::new(
700                cortical_id_typed,
701                0, // Auto-assigned by ConnectomeManager
702                param.name.clone(),
703                CorticalAreaDimensions::new(
704                    param.dimensions.0 as u32,
705                    param.dimensions.1 as u32,
706                    param.dimensions.2 as u32,
707                )?,
708                param.position.into(), // Convert (i32, i32, i32) to GenomeCoordinate3D
709                area_type,
710            )?;
711
712            // Apply all neural parameters
713            if let Some(visible) = param.visible {
714                area.add_property_mut("visible".to_string(), serde_json::json!(visible));
715            }
716            if let Some(sub_group) = &param.sub_group {
717                area.add_property_mut("sub_group".to_string(), serde_json::json!(sub_group));
718            }
719            if let Some(neurons_per_voxel) = param.neurons_per_voxel {
720                area.add_property_mut(
721                    "neurons_per_voxel".to_string(),
722                    serde_json::json!(neurons_per_voxel),
723                );
724            }
725            if let Some(postsynaptic_current) = param.postsynaptic_current {
726                area.add_property_mut(
727                    "postsynaptic_current".to_string(),
728                    serde_json::json!(postsynaptic_current),
729                );
730            }
731            if let Some(plasticity_constant) = param.plasticity_constant {
732                area.add_property_mut(
733                    "plasticity_constant".to_string(),
734                    serde_json::json!(plasticity_constant),
735                );
736            }
737            if let Some(degeneration) = param.degeneration {
738                area.add_property_mut("degeneration".to_string(), serde_json::json!(degeneration));
739            }
740            if let Some(psp_uniform_distribution) = param.psp_uniform_distribution {
741                area.add_property_mut(
742                    "psp_uniform_distribution".to_string(),
743                    serde_json::json!(psp_uniform_distribution),
744                );
745            }
746            if let Some(firing_threshold_increment) = param.firing_threshold_increment {
747                area.add_property_mut(
748                    "firing_threshold_increment".to_string(),
749                    serde_json::json!(firing_threshold_increment),
750                );
751            }
752            if let Some(firing_threshold_limit) = param.firing_threshold_limit {
753                area.add_property_mut(
754                    "firing_threshold_limit".to_string(),
755                    serde_json::json!(firing_threshold_limit),
756                );
757            }
758            if let Some(consecutive_fire_count) = param.consecutive_fire_count {
759                area.add_property_mut(
760                    "consecutive_fire_limit".to_string(),
761                    serde_json::json!(consecutive_fire_count),
762                );
763            }
764            if let Some(snooze_period) = param.snooze_period {
765                area.add_property_mut(
766                    "snooze_period".to_string(),
767                    serde_json::json!(snooze_period),
768                );
769            }
770            if let Some(refractory_period) = param.refractory_period {
771                area.add_property_mut(
772                    "refractory_period".to_string(),
773                    serde_json::json!(refractory_period),
774                );
775            }
776            if let Some(leak_coefficient) = param.leak_coefficient {
777                area.add_property_mut(
778                    "leak_coefficient".to_string(),
779                    serde_json::json!(leak_coefficient),
780                );
781            }
782            if let Some(leak_variability) = param.leak_variability {
783                area.add_property_mut(
784                    "leak_variability".to_string(),
785                    serde_json::json!(leak_variability),
786                );
787            }
788            if let Some(burst_engine_active) = param.burst_engine_active {
789                area.add_property_mut(
790                    "burst_engine_active".to_string(),
791                    serde_json::json!(burst_engine_active),
792                );
793            }
794            if matches!(area_type, CorticalAreaType::Memory(_)) {
795                let merged = merge_memory_area_properties(
796                    area.properties.clone(),
797                    param.properties.as_ref(),
798                );
799                area.properties = merged;
800            } else if let Some(properties) = &param.properties {
801                area.properties = properties.clone();
802            }
803
804            // IO areas (IPU/OPU) must always be in root region; default if missing
805            if matches!(
806                area_type,
807                CorticalAreaType::BrainInput(_) | CorticalAreaType::BrainOutput(_)
808            ) && !area.properties.contains_key("parent_region_id")
809            {
810                if let Some(ref root_id) = root_region_id {
811                    area.add_property_mut(
812                        "parent_region_id".to_string(),
813                        serde_json::Value::String(root_id.clone()),
814                    );
815                }
816            }
817
818            areas_to_add.push(area);
819        }
820
821        // Step 2: Add to runtime genome (source of truth)
822        {
823            let mut genome_lock = self.current_genome.write();
824            if let Some(ref mut genome) = *genome_lock {
825                for area in &areas_to_add {
826                    genome.cortical_areas.insert(area.cortical_id, area.clone());
827                    info!(target: "feagi-services", "Added {} to runtime genome", area.cortical_id.as_base_64());
828                    if let Some(parent) = area
829                        .properties
830                        .get("parent_region_id")
831                        .and_then(|v| v.as_str())
832                    {
833                        let region = genome.brain_regions.get_mut(parent).ok_or_else(|| {
834                            ServiceError::InvalidInput(format!(
835                                "Unknown parent_region_id '{}' for new cortical area {}",
836                                parent,
837                                area.cortical_id.as_base_64()
838                            ))
839                        })?;
840                        region.cortical_areas.insert(area.cortical_id);
841                    }
842                }
843            } else {
844                return Err(ServiceError::Backend("No genome loaded".to_string()));
845            }
846        }
847
848        // Step 3: Get genome for neuroembryogenesis context
849        let genome_clone = {
850            let genome_lock = self.current_genome.read();
851            genome_lock
852                .as_ref()
853                .ok_or_else(|| ServiceError::Backend("No genome loaded".to_string()))?
854                .clone()
855        };
856
857        // Step 4: Call neuroembryogenesis to create structures, neurons, and synapses
858        let (neurons_created, synapses_created) = {
859            let connectome_clone = self.connectome.clone();
860            tokio::task::spawn_blocking(move || {
861                let mut neuro = Neuroembryogenesis::new(connectome_clone);
862                neuro.add_cortical_areas(areas_to_add.clone(), &genome_clone)
863            })
864            .await
865            .map_err(|e| ServiceError::Backend(format!("Neuroembryogenesis task failed: {}", e)))?
866            .map_err(|e| ServiceError::Backend(format!("Neuroembryogenesis failed: {}", e)))?
867        };
868
869        info!(target: "feagi-services",
870              "✅ Created {} cortical areas: {} neurons, {} synapses",
871              params.len(), neurons_created, synapses_created);
872
873        // Refresh burst runner cache after creating areas
874        self.refresh_burst_runner_cache();
875
876        // Step 5: Fetch and return area information
877        let mut created_areas = Vec::new();
878        for param in &params {
879            match self.get_cortical_area_info(&param.cortical_id).await {
880                Ok(area_info) => created_areas.push(area_info),
881                Err(e) => {
882                    warn!(target: "feagi-services", "Created area {} but failed to fetch info: {}", param.cortical_id, e);
883                    return Err(ServiceError::Backend(format!(
884                        "Created areas but failed to fetch info for {}: {}",
885                        param.cortical_id, e
886                    )));
887                }
888            }
889        }
890
891        Ok(created_areas)
892    }
893
894    async fn update_cortical_area(
895        &self,
896        cortical_id: &str,
897        changes: HashMap<String, Value>,
898    ) -> ServiceResult<CorticalAreaInfo> {
899        info!(target: "feagi-services", "Updating cortical area: {} with {} changes", cortical_id, changes.len());
900
901        // Convert String to CorticalID (supports legacy core aliases)
902        let cortical_id_typed = feagi_evolutionary::string_to_cortical_id(cortical_id)
903            .map_err(|e| ServiceError::InvalidInput(format!("Invalid cortical ID: {}", e)))?;
904
905        let mut changes = changes;
906        let mut effective_cortical_id = cortical_id_typed;
907        let mut effective_cortical_id_str = cortical_id.to_string();
908
909        // Verify cortical area exists.
910        // If not found, best-effort resolve IO cortical IDs that were recently remapped
911        // by coding changes (Absolute/Incremental, Linear/Fractional), where bytes 4-5
912        // change but the unit identifier + group/subunit stay the same.
913        {
914            let manager = self.connectome.read();
915            if !manager.has_cortical_area(&effective_cortical_id) {
916                let target_bytes = effective_cortical_id.as_bytes();
917                let mut candidates: Vec<_> = manager
918                    .get_cortical_area_ids()
919                    .iter()
920                    .map(|c| **c)
921                    .filter(|candidate| {
922                        let c = candidate.as_bytes();
923                        c[0] == target_bytes[0] // i/o namespace
924                            && c[1] == target_bytes[1]
925                            && c[2] == target_bytes[2]
926                            && c[3] == target_bytes[3] // cortical unit identifier
927                            && c[6] == target_bytes[6] // sub-unit index
928                            && c[7] == target_bytes[7] // unit(group) index
929                    })
930                    .collect();
931                if candidates.len() == 1 {
932                    let resolved = candidates.remove(0);
933                    info!(
934                        target: "feagi-services",
935                        "[GENOME-UPDATE] Resolved remapped cortical ID '{}' -> '{}'",
936                        cortical_id,
937                        resolved.as_base_64()
938                    );
939                    effective_cortical_id = resolved;
940                    effective_cortical_id_str = resolved.as_base_64();
941                } else {
942                    return Err(ServiceError::NotFound {
943                        resource: "CorticalArea".to_string(),
944                        id: cortical_id.to_string(),
945                    });
946                }
947            }
948        }
949
950        if changes.contains_key("group_id") {
951            let new_id = self.apply_unit_index_update(
952                &effective_cortical_id,
953                &effective_cortical_id_str,
954                &changes,
955            )?;
956            effective_cortical_id = new_id;
957            effective_cortical_id_str = new_id.as_base_64();
958            self.regenerate_mappings_for_area(&effective_cortical_id)?;
959            changes.remove("group_id");
960            if changes.is_empty() {
961                return self
962                    .get_cortical_area_info(&effective_cortical_id_str)
963                    .await;
964            }
965        }
966
967        // Classify changes for intelligent routing
968        let change_type = CorticalChangeClassifier::classify_changes(&changes);
969        CorticalChangeClassifier::log_classification_result(&changes, change_type);
970
971        // Route based on change type
972        match change_type {
973            ChangeType::Parameter => {
974                // Fast path: Direct neuron updates (~2-5ms, NO synapse rebuild)
975                self.update_parameters_only(&effective_cortical_id_str, changes)
976                    .await
977            }
978            ChangeType::Metadata => {
979                // Fastest path: Metadata updates only (~1ms)
980                self.update_metadata_only(&effective_cortical_id_str, changes)
981                    .await
982            }
983            ChangeType::Structural => {
984                // Structural path: Requires synapse rebuild (~100-200ms)
985                self.update_with_localized_rebuild(&effective_cortical_id_str, changes)
986                    .await
987            }
988            ChangeType::Hybrid => {
989                // Hybrid path: Handle each type separately
990                let separated = CorticalChangeClassifier::separate_changes_by_type(&changes);
991
992                // Process in order: metadata first, then parameters, then structural
993                if let Some(metadata_changes) = separated.get(&ChangeType::Metadata) {
994                    if !metadata_changes.is_empty() {
995                        self.update_metadata_only(
996                            &effective_cortical_id_str,
997                            metadata_changes.clone(),
998                        )
999                        .await?;
1000                    }
1001                }
1002
1003                if let Some(param_changes) = separated.get(&ChangeType::Parameter) {
1004                    if !param_changes.is_empty() {
1005                        self.update_parameters_only(
1006                            &effective_cortical_id_str,
1007                            param_changes.clone(),
1008                        )
1009                        .await?;
1010                    }
1011                }
1012
1013                if let Some(struct_changes) = separated.get(&ChangeType::Structural) {
1014                    if !struct_changes.is_empty() {
1015                        self.update_with_localized_rebuild(
1016                            &effective_cortical_id_str,
1017                            struct_changes.clone(),
1018                        )
1019                        .await?;
1020                    }
1021                }
1022
1023                // Return updated info
1024                self.get_cortical_area_info(&effective_cortical_id_str)
1025                    .await
1026            }
1027        }
1028    }
1029}
1030
1031impl GenomeServiceImpl {
1032    /// Fast path: Update only neuron parameters without synapse rebuild
1033    ///
1034    /// Performance: ~1-2µs to queue (non-blocking), applied in next burst cycle
1035    async fn update_parameters_only(
1036        &self,
1037        cortical_id: &str,
1038        changes: HashMap<String, Value>,
1039    ) -> ServiceResult<CorticalAreaInfo> {
1040        info!(target: "feagi-services", "[FAST-UPDATE] Parameter-only update for {}", cortical_id);
1041
1042        // Convert String to CorticalID
1043        let cortical_id_typed = CorticalID::try_from_base_64(cortical_id)
1044            .map_err(|e| ServiceError::InvalidInput(format!("Invalid cortical ID: {}", e)))?;
1045        // Get cortical index for NPU updates
1046        let cortical_idx = {
1047            let manager = self.connectome.read();
1048            manager
1049                .get_cortical_idx(&cortical_id_typed)
1050                .ok_or_else(|| ServiceError::NotFound {
1051                    resource: "CorticalArea".to_string(),
1052                    id: cortical_id.to_string(),
1053                })?
1054        };
1055
1056        // Queue parameter updates for burst loop to consume (non-blocking!)
1057        if let Some(queue) = &self.parameter_queue {
1058            // Get base threshold for spatial gradient updates
1059            let base_threshold = {
1060                let manager = self.connectome.read();
1061                manager
1062                    .get_cortical_area(&cortical_id_typed)
1063                    .map(|area| area.firing_threshold())
1064            };
1065
1066            for (param_name, value) in &changes {
1067                // Only queue parameters that affect NPU neurons
1068                let classifier = CorticalChangeClassifier::parameter_changes();
1069                if classifier.contains(param_name.as_str()) {
1070                    // Include base threshold for spatial gradient updates
1071                    let bt = if param_name == "neuron_fire_threshold_increment"
1072                        || param_name == "firing_threshold_increment"
1073                    {
1074                        base_threshold
1075                    } else {
1076                        None
1077                    };
1078
1079                    queue.push(feagi_npu_burst_engine::ParameterUpdate {
1080                        cortical_idx,
1081                        cortical_id: cortical_id.to_string(),
1082                        parameter_name: param_name.clone(),
1083                        value: value.clone(),
1084                        dimensions: None, // Not needed anymore - neurons have stored positions
1085                        neurons_per_voxel: None,
1086                        base_threshold: bt,
1087                    });
1088                    trace!(
1089                        target: "feagi-services",
1090                        "[PARAM-QUEUE] Queued {}={} for area {}",
1091                        param_name,
1092                        value,
1093                        cortical_id
1094                    );
1095                }
1096            }
1097            info!(target: "feagi-services", "[FAST-UPDATE] Queued parameter updates (will apply in next burst)");
1098        } else {
1099            warn!(target: "feagi-services", "Parameter queue not available - updates will not affect neurons");
1100        }
1101
1102        // Persist parameter-only updates into the live ConnectomeManager so API reads (and BV UI)
1103        // reflect the same values that are applied to the NPU.
1104        //
1105        // IMPORTANT: The parameter queue updates runtime neuron state; ConnectomeManager is the
1106        // source-of-truth for cortical-area *reported* properties.
1107        {
1108            let mut manager = self.connectome.write();
1109            if let Some(area) = manager.get_cortical_area_mut(&cortical_id_typed) {
1110                for (key, value) in &changes {
1111                    match key.as_str() {
1112                        // Thresholds
1113                        "firing_threshold" | "neuron_fire_threshold" => {
1114                            if let Some(v) = value.as_f64() {
1115                                area.properties
1116                                    .insert("firing_threshold".to_string(), serde_json::json!(v));
1117                            }
1118                        }
1119                        "firing_threshold_limit" | "neuron_firing_threshold_limit" => {
1120                            if let Some(v) = value.as_f64() {
1121                                area.properties.insert(
1122                                    "firing_threshold_limit".to_string(),
1123                                    serde_json::json!(v),
1124                                );
1125                            }
1126                        }
1127                        // Spatial gradient increments
1128                        "firing_threshold_increment_x" => {
1129                            if let Some(v) = value.as_f64() {
1130                                area.properties.insert(
1131                                    "firing_threshold_increment_x".to_string(),
1132                                    serde_json::json!(v),
1133                                );
1134                            }
1135                        }
1136                        "firing_threshold_increment_y" => {
1137                            if let Some(v) = value.as_f64() {
1138                                area.properties.insert(
1139                                    "firing_threshold_increment_y".to_string(),
1140                                    serde_json::json!(v),
1141                                );
1142                            }
1143                        }
1144                        "firing_threshold_increment_z" => {
1145                            if let Some(v) = value.as_f64() {
1146                                area.properties.insert(
1147                                    "firing_threshold_increment_z".to_string(),
1148                                    serde_json::json!(v),
1149                                );
1150                            }
1151                        }
1152                        "firing_threshold_increment" | "neuron_fire_threshold_increment" => {
1153                            if let Some(arr) = value.as_array() {
1154                                if arr.len() == 3 {
1155                                    if let (Some(x), Some(y), Some(z)) =
1156                                        (arr[0].as_f64(), arr[1].as_f64(), arr[2].as_f64())
1157                                    {
1158                                        area.properties.insert(
1159                                            "firing_threshold_increment_x".to_string(),
1160                                            serde_json::json!(x),
1161                                        );
1162                                        area.properties.insert(
1163                                            "firing_threshold_increment_y".to_string(),
1164                                            serde_json::json!(y),
1165                                        );
1166                                        area.properties.insert(
1167                                            "firing_threshold_increment_z".to_string(),
1168                                            serde_json::json!(z),
1169                                        );
1170                                    }
1171                                }
1172                            } else if let Some(obj) = value.as_object() {
1173                                if let (Some(x), Some(y), Some(z)) = (
1174                                    obj.get("x").and_then(|v| v.as_f64()),
1175                                    obj.get("y").and_then(|v| v.as_f64()),
1176                                    obj.get("z").and_then(|v| v.as_f64()),
1177                                ) {
1178                                    area.properties.insert(
1179                                        "firing_threshold_increment_x".to_string(),
1180                                        serde_json::json!(x),
1181                                    );
1182                                    area.properties.insert(
1183                                        "firing_threshold_increment_y".to_string(),
1184                                        serde_json::json!(y),
1185                                    );
1186                                    area.properties.insert(
1187                                        "firing_threshold_increment_z".to_string(),
1188                                        serde_json::json!(z),
1189                                    );
1190                                }
1191                            }
1192                        }
1193
1194                        // Timing/decay
1195                        "refractory_period" | "neuron_refractory_period" | "refrac" => {
1196                            if let Some(v) = value.as_u64() {
1197                                area.properties.insert(
1198                                    "refractory_period".to_string(),
1199                                    serde_json::json!(v as u32),
1200                                );
1201                            }
1202                        }
1203                        "leak_coefficient" | "neuron_leak_coefficient" | "leak" => {
1204                            if let Some(v) = value.as_f64() {
1205                                area.properties
1206                                    .insert("leak_coefficient".to_string(), serde_json::json!(v));
1207                            }
1208                        }
1209
1210                        // Burst gating
1211                        "consecutive_fire_cnt_max"
1212                        | "neuron_consecutive_fire_count"
1213                        | "consecutive_fire_count" => {
1214                            if let Some(v) = value.as_u64() {
1215                                // ConnectomeManager getters expect `consecutive_fire_limit`.
1216                                area.properties.insert(
1217                                    "consecutive_fire_limit".to_string(),
1218                                    serde_json::json!(v as u32),
1219                                );
1220                            }
1221                        }
1222                        "snooze_length" | "neuron_snooze_period" | "snooze_period" => {
1223                            if let Some(v) = value.as_u64() {
1224                                // ConnectomeManager getters expect `snooze_period`.
1225                                area.properties.insert(
1226                                    "snooze_period".to_string(),
1227                                    serde_json::json!(v as u32),
1228                                );
1229                            }
1230                        }
1231
1232                        // Excitability (BV uses percent UI but sends 0..=1 to the API)
1233                        "neuron_excitability" | "excitability" => {
1234                            if let Some(v) = value.as_f64() {
1235                                if (0.0..=1.0).contains(&v) {
1236                                    area.properties.insert(
1237                                        "neuron_excitability".to_string(),
1238                                        serde_json::json!(v),
1239                                    );
1240                                } else {
1241                                    warn!(
1242                                        target: "feagi-services",
1243                                        "[FAST-UPDATE] Ignoring neuron_excitability={} for area {} (expected 0..=1)",
1244                                        v,
1245                                        cortical_id
1246                                    );
1247                                }
1248                            }
1249                        }
1250
1251                        // PSP + degeneration + plasticity
1252                        "postsynaptic_current" | "neuron_post_synaptic_potential" => {
1253                            if let Some(v) = value.as_f64() {
1254                                area.properties.insert(
1255                                    "postsynaptic_current".to_string(),
1256                                    serde_json::json!(v),
1257                                );
1258                            }
1259                        }
1260                        "postsynaptic_current_max" | "neuron_post_synaptic_potential_max" => {
1261                            if let Some(v) = value.as_f64() {
1262                                area.properties.insert(
1263                                    "postsynaptic_current_max".to_string(),
1264                                    serde_json::json!(v),
1265                                );
1266                            }
1267                        }
1268                        "degeneration" | "neuron_degeneracy_coefficient" => {
1269                            if let Some(v) = value.as_f64() {
1270                                area.properties
1271                                    .insert("degeneration".to_string(), serde_json::json!(v));
1272                            }
1273                        }
1274                        "plasticity_constant" | "neuron_plasticity_constant" => {
1275                            if let Some(v) = value.as_f64() {
1276                                area.properties.insert(
1277                                    "plasticity_constant".to_string(),
1278                                    serde_json::json!(v),
1279                                );
1280                            }
1281                        }
1282                        "psp_uniform_distribution" | "neuron_psp_uniform_distribution" => {
1283                            if let Some(v) = value.as_bool() {
1284                                area.properties.insert(
1285                                    "psp_uniform_distribution".to_string(),
1286                                    serde_json::json!(v),
1287                                );
1288                            }
1289                        }
1290
1291                        // Memory parameters (used by plasticity registration + API display)
1292                        "init_lifespan" | "neuron_init_lifespan" => {
1293                            if let Some(v) = value.as_u64() {
1294                                area.properties.insert(
1295                                    "init_lifespan".to_string(),
1296                                    serde_json::json!(v as u32),
1297                                );
1298                            }
1299                        }
1300                        "lifespan_growth_rate" | "neuron_lifespan_growth_rate" => {
1301                            // Accept integer and float representations.
1302                            if let Some(v) =
1303                                value.as_f64().or_else(|| value.as_u64().map(|u| u as f64))
1304                            {
1305                                area.properties.insert(
1306                                    "lifespan_growth_rate".to_string(),
1307                                    serde_json::json!(v as f32),
1308                                );
1309                            }
1310                        }
1311                        "longterm_mem_threshold" | "neuron_longterm_mem_threshold" => {
1312                            if let Some(v) = value.as_u64() {
1313                                area.properties.insert(
1314                                    "longterm_mem_threshold".to_string(),
1315                                    serde_json::json!(v as u32),
1316                                );
1317                            }
1318                        }
1319                        "temporal_depth" => {
1320                            if let Some(v) = value.as_u64() {
1321                                if v == 0 {
1322                                    warn!(
1323                                        target: "feagi-services",
1324                                        "[FAST-UPDATE] Ignoring temporal_depth=0 for area {} (temporal_depth must be >= 1)",
1325                                        cortical_id
1326                                    );
1327                                } else {
1328                                    area.properties.insert(
1329                                        "temporal_depth".to_string(),
1330                                        serde_json::json!(v as u32),
1331                                    );
1332                                }
1333                            }
1334                        }
1335
1336                        // Membrane potential / runtime flags
1337                        "mp_charge_accumulation" | "neuron_mp_charge_accumulation" => {
1338                            if let Some(v) = value.as_bool() {
1339                                area.properties.insert(
1340                                    "mp_charge_accumulation".to_string(),
1341                                    serde_json::json!(v),
1342                                );
1343                            } else {
1344                                warn!(
1345                                    target: "feagi-services",
1346                                    "[FAST-UPDATE] mp_charge_accumulation not stored: value must be JSON boolean, got {:?} (area {})",
1347                                    value,
1348                                    cortical_id
1349                                );
1350                            }
1351                        }
1352                        "mp_driven_psp" | "neuron_mp_driven_psp" => {
1353                            if let Some(v) = value.as_bool() {
1354                                area.properties
1355                                    .insert("mp_driven_psp".to_string(), serde_json::json!(v));
1356                            }
1357                        }
1358
1359                        // Burst engine
1360                        "burst_engine_active" => {
1361                            if let Some(v) = value.as_bool() {
1362                                area.properties.insert(
1363                                    "burst_engine_active".to_string(),
1364                                    serde_json::json!(v),
1365                                );
1366                            }
1367                        }
1368                        "visualization_voxel_granularity" => {
1369                            // Only store if != 1x1x1 (default), delete if set to 1x1x1
1370                            // Handle both integer and float JSON values
1371                            if let Some(arr) = value.as_array() {
1372                                if arr.len() == 3 {
1373                                    // Try to parse as integers (u64) or floats (f64), then convert to u32
1374                                    let x_opt = arr[0]
1375                                        .as_u64()
1376                                        .or_else(|| arr[0].as_f64().map(|f| f as u64));
1377                                    let y_opt = arr[1]
1378                                        .as_u64()
1379                                        .or_else(|| arr[1].as_f64().map(|f| f as u64));
1380                                    let z_opt = arr[2]
1381                                        .as_u64()
1382                                        .or_else(|| arr[2].as_f64().map(|f| f as u64));
1383
1384                                    if let (Some(x), Some(y), Some(z)) = (x_opt, y_opt, z_opt) {
1385                                        let x_u32 = x as u32;
1386                                        let y_u32 = y as u32;
1387                                        let z_u32 = z as u32;
1388
1389                                        // Default is 1x1x1 - only store if different
1390                                        if x_u32 == 1 && y_u32 == 1 && z_u32 == 1 {
1391                                            // Remove override (return to default)
1392                                            area.properties
1393                                                .remove("visualization_voxel_granularity");
1394                                        } else {
1395                                            // Store override (non-default value) as integer array
1396                                            area.properties.insert(
1397                                                "visualization_voxel_granularity".to_string(),
1398                                                serde_json::json!([x_u32, y_u32, z_u32]),
1399                                            );
1400                                        }
1401                                    }
1402                                }
1403                            }
1404                        }
1405
1406                        _ => {}
1407                    }
1408                }
1409            }
1410            manager.refresh_cortical_area_hashes(true, false);
1411        }
1412
1413        // Update RuntimeGenome if available (CRITICAL for save/load persistence!)
1414        if let Some(genome) = self.current_genome.write().as_mut() {
1415            if let Some(area) = genome.cortical_areas.get_mut(&cortical_id_typed) {
1416                trace!(
1417                    target: "feagi-services",
1418                    "[GENOME-UPDATE] Updating RuntimeGenome for area {}",
1419                    cortical_id
1420                );
1421                for (key, value) in &changes {
1422                    match key.as_str() {
1423                        "neuron_fire_threshold" | "firing_threshold" => {
1424                            if let Some(v) = value.as_f64() {
1425                                area.properties
1426                                    .insert("firing_threshold".to_string(), serde_json::json!(v));
1427                            }
1428                        }
1429                        "firing_threshold_limit" | "neuron_firing_threshold_limit" => {
1430                            if let Some(v) = value.as_f64() {
1431                                area.properties.insert(
1432                                    "firing_threshold_limit".to_string(),
1433                                    serde_json::json!(v),
1434                                );
1435                            }
1436                        }
1437                        "firing_threshold_increment_x" => {
1438                            if let Some(v) = value.as_f64() {
1439                                area.properties.insert(
1440                                    "firing_threshold_increment_x".to_string(),
1441                                    serde_json::json!(v),
1442                                );
1443                            }
1444                        }
1445                        "firing_threshold_increment_y" => {
1446                            if let Some(v) = value.as_f64() {
1447                                area.properties.insert(
1448                                    "firing_threshold_increment_y".to_string(),
1449                                    serde_json::json!(v),
1450                                );
1451                            }
1452                        }
1453                        "firing_threshold_increment_z" => {
1454                            if let Some(v) = value.as_f64() {
1455                                area.properties.insert(
1456                                    "firing_threshold_increment_z".to_string(),
1457                                    serde_json::json!(v),
1458                                );
1459                            }
1460                        }
1461                        "leak_coefficient" | "neuron_leak_coefficient" => {
1462                            if let Some(v) = value.as_f64() {
1463                                area.properties
1464                                    .insert("leak_coefficient".to_string(), serde_json::json!(v));
1465                            }
1466                        }
1467                        "leak_variability" | "neuron_leak_variability" => {
1468                            if let Some(v) = value.as_f64() {
1469                                area.properties
1470                                    .insert("leak_variability".to_string(), serde_json::json!(v));
1471                            }
1472                        }
1473                        "refractory_period" | "neuron_refractory_period" => {
1474                            if let Some(v) = value.as_u64() {
1475                                area.properties.insert(
1476                                    "refractory_period".to_string(),
1477                                    serde_json::json!(v as u32),
1478                                );
1479                            }
1480                        }
1481                        "snooze_period" | "neuron_snooze_period" => {
1482                            if let Some(v) = value.as_u64() {
1483                                // Converter expects "snooze_length" not "snooze_period"
1484                                area.properties.insert(
1485                                    "snooze_length".to_string(),
1486                                    serde_json::json!(v as u32),
1487                                );
1488                            }
1489                        }
1490                        "consecutive_fire_count" | "neuron_consecutive_fire_count" => {
1491                            if let Some(v) = value.as_u64() {
1492                                // Converter expects "consecutive_fire_cnt_max" not "consecutive_fire_count"
1493                                area.properties.insert(
1494                                    "consecutive_fire_cnt_max".to_string(),
1495                                    serde_json::json!(v as u32),
1496                                );
1497                            }
1498                        }
1499                        "postsynaptic_current" | "neuron_post_synaptic_potential" => {
1500                            if let Some(v) = value.as_f64() {
1501                                area.properties.insert(
1502                                    "postsynaptic_current".to_string(),
1503                                    serde_json::json!(v),
1504                                );
1505                            }
1506                        }
1507                        "postsynaptic_current_max" | "neuron_post_synaptic_potential_max" => {
1508                            if let Some(v) = value.as_f64() {
1509                                area.properties.insert(
1510                                    "postsynaptic_current_max".to_string(),
1511                                    serde_json::json!(v),
1512                                );
1513                            }
1514                        }
1515                        "plasticity_constant" | "neuron_plasticity_constant" => {
1516                            if let Some(v) = value.as_f64() {
1517                                area.properties.insert(
1518                                    "plasticity_constant".to_string(),
1519                                    serde_json::json!(v),
1520                                );
1521                            }
1522                        }
1523                        "degeneration" | "neuron_degeneracy_coefficient" => {
1524                            if let Some(v) = value.as_f64() {
1525                                area.properties
1526                                    .insert("degeneration".to_string(), serde_json::json!(v));
1527                            }
1528                        }
1529                        "psp_uniform_distribution" | "neuron_psp_uniform_distribution" => {
1530                            if let Some(v) = value.as_bool() {
1531                                area.properties.insert(
1532                                    "psp_uniform_distribution".to_string(),
1533                                    serde_json::json!(v),
1534                                );
1535                            }
1536                        }
1537                        "mp_driven_psp" | "neuron_mp_driven_psp" => {
1538                            if let Some(v) = value.as_bool() {
1539                                area.properties
1540                                    .insert("mp_driven_psp".to_string(), serde_json::json!(v));
1541                                info!(
1542                                    target: "feagi-services",
1543                                    "[GENOME-UPDATE] Updated mp_driven_psp={} in RuntimeGenome for area {}",
1544                                    v, cortical_id
1545                                );
1546                            } else {
1547                                warn!(
1548                                    target: "feagi-services",
1549                                    "[GENOME-UPDATE] Failed to update mp_driven_psp: value is not a bool (got {:?})",
1550                                    value
1551                                );
1552                            }
1553                        }
1554                        "mp_charge_accumulation" | "neuron_mp_charge_accumulation" => {
1555                            if let Some(v) = value.as_bool() {
1556                                area.properties.insert(
1557                                    "mp_charge_accumulation".to_string(),
1558                                    serde_json::json!(v),
1559                                );
1560                            } else {
1561                                warn!(
1562                                    target: "feagi-services",
1563                                    "[GENOME-UPDATE] mp_charge_accumulation not stored: value must be JSON boolean, got {:?} (area {})",
1564                                    value,
1565                                    cortical_id
1566                                );
1567                            }
1568                        }
1569                        "neuron_excitability" => {
1570                            if let Some(v) = value.as_f64() {
1571                                area.properties.insert(
1572                                    "neuron_excitability".to_string(),
1573                                    serde_json::json!(v),
1574                                );
1575                            }
1576                        }
1577                        "init_lifespan" | "neuron_init_lifespan" => {
1578                            if let Some(v) = value.as_u64() {
1579                                area.properties.insert(
1580                                    "init_lifespan".to_string(),
1581                                    serde_json::json!(v as u32),
1582                                );
1583                            }
1584                        }
1585                        "lifespan_growth_rate" | "neuron_lifespan_growth_rate" => {
1586                            if let Some(v) = value.as_u64() {
1587                                area.properties.insert(
1588                                    "lifespan_growth_rate".to_string(),
1589                                    serde_json::json!(v as u32),
1590                                );
1591                            }
1592                        }
1593                        "longterm_mem_threshold" | "neuron_longterm_mem_threshold" => {
1594                            if let Some(v) = value.as_u64() {
1595                                area.properties.insert(
1596                                    "longterm_mem_threshold".to_string(),
1597                                    serde_json::json!(v as u32),
1598                                );
1599                            }
1600                        }
1601                        "temporal_depth" => {
1602                            if let Some(v) = value.as_u64() {
1603                                if v == 0 {
1604                                    warn!(
1605                                        target: "feagi-services",
1606                                        "[GENOME-UPDATE] Ignoring temporal_depth=0 for area {} (temporal_depth must be >= 1)",
1607                                        cortical_id
1608                                    );
1609                                } else {
1610                                    area.properties.insert(
1611                                        "temporal_depth".to_string(),
1612                                        serde_json::json!(v as u32),
1613                                    );
1614                                }
1615                            }
1616                        }
1617                        "firing_threshold_increment" | "neuron_fire_threshold_increment" => {
1618                            // Converter expects separate x, y, z properties, not an array
1619                            if let Some(arr) = value.as_array() {
1620                                if arr.len() == 3 {
1621                                    if let (Some(x), Some(y), Some(z)) =
1622                                        (arr[0].as_f64(), arr[1].as_f64(), arr[2].as_f64())
1623                                    {
1624                                        area.properties.insert(
1625                                            "firing_threshold_increment_x".to_string(),
1626                                            serde_json::json!(x),
1627                                        );
1628                                        area.properties.insert(
1629                                            "firing_threshold_increment_y".to_string(),
1630                                            serde_json::json!(y),
1631                                        );
1632                                        area.properties.insert(
1633                                            "firing_threshold_increment_z".to_string(),
1634                                            serde_json::json!(z),
1635                                        );
1636                                    }
1637                                }
1638                            } else if let Some(obj) = value.as_object() {
1639                                // Convert {x, y, z} to separate properties
1640                                if let (Some(x), Some(y), Some(z)) = (
1641                                    obj.get("x").and_then(|v| v.as_f64()),
1642                                    obj.get("y").and_then(|v| v.as_f64()),
1643                                    obj.get("z").and_then(|v| v.as_f64()),
1644                                ) {
1645                                    area.properties.insert(
1646                                        "firing_threshold_increment_x".to_string(),
1647                                        serde_json::json!(x),
1648                                    );
1649                                    area.properties.insert(
1650                                        "firing_threshold_increment_y".to_string(),
1651                                        serde_json::json!(y),
1652                                    );
1653                                    area.properties.insert(
1654                                        "firing_threshold_increment_z".to_string(),
1655                                        serde_json::json!(z),
1656                                    );
1657                                }
1658                            }
1659                        }
1660                        "burst_engine_active" => {
1661                            if let Some(v) = value.as_bool() {
1662                                area.properties.insert(
1663                                    "burst_engine_active".to_string(),
1664                                    serde_json::json!(v),
1665                                );
1666                            }
1667                        }
1668                        "visualization_voxel_granularity" => {
1669                            // Only store if != 1x1x1 (default), delete if set to 1x1x1
1670                            // Handle both integer and float JSON values
1671                            if let Some(arr) = value.as_array() {
1672                                if arr.len() == 3 {
1673                                    // Try to parse as integers (u64) or floats (f64), then convert to u32
1674                                    let x_opt = arr[0]
1675                                        .as_u64()
1676                                        .or_else(|| arr[0].as_f64().map(|f| f as u64));
1677                                    let y_opt = arr[1]
1678                                        .as_u64()
1679                                        .or_else(|| arr[1].as_f64().map(|f| f as u64));
1680                                    let z_opt = arr[2]
1681                                        .as_u64()
1682                                        .or_else(|| arr[2].as_f64().map(|f| f as u64));
1683
1684                                    if let (Some(x), Some(y), Some(z)) = (x_opt, y_opt, z_opt) {
1685                                        let x_u32 = x as u32;
1686                                        let y_u32 = y as u32;
1687                                        let z_u32 = z as u32;
1688
1689                                        // Default is 1x1x1 - only store if different
1690                                        if x_u32 == 1 && y_u32 == 1 && z_u32 == 1 {
1691                                            // Remove override (return to default)
1692                                            area.properties
1693                                                .remove("visualization_voxel_granularity");
1694                                        } else {
1695                                            // Store override (non-default value) as integer array
1696                                            area.properties.insert(
1697                                                "visualization_voxel_granularity".to_string(),
1698                                                serde_json::json!([x_u32, y_u32, z_u32]),
1699                                            );
1700                                        }
1701                                    }
1702                                }
1703                            }
1704                        }
1705                        _ => {}
1706                    }
1707                }
1708            } else {
1709                warn!(
1710                    target: "feagi-services",
1711                    "[GENOME-UPDATE] WARNING: Cortical area {} not found in RuntimeGenome - property updates will not persist to saved genome!",
1712                    cortical_id
1713                );
1714            }
1715        } else {
1716            warn!(
1717                target: "feagi-services",
1718                "[GENOME-UPDATE] WARNING: No RuntimeGenome loaded - property updates will not persist to saved genome!"
1719            );
1720        }
1721
1722        // Update ConnectomeManager metadata for consistency
1723        {
1724            let mut manager = self.connectome.write();
1725            let area = manager
1726                .get_cortical_area_mut(&cortical_id_typed)
1727                .ok_or_else(|| ServiceError::NotFound {
1728                    resource: "CorticalArea".to_string(),
1729                    id: cortical_id.to_string(),
1730                })?;
1731
1732            // Update BDU metadata fields
1733            for (key, value) in &changes {
1734                match key.as_str() {
1735                    "neuron_fire_threshold" | "firing_threshold" => {
1736                        if let Some(v) = value.as_f64() {
1737                            area.add_property_mut(
1738                                "firing_threshold".to_string(),
1739                                serde_json::json!(v),
1740                            );
1741                        }
1742                    }
1743                    "firing_threshold_limit" | "neuron_firing_threshold_limit" => {
1744                        if let Some(v) = value.as_f64() {
1745                            area.add_property_mut(
1746                                "firing_threshold_limit".to_string(),
1747                                serde_json::json!(v),
1748                            );
1749                        }
1750                    }
1751                    "leak_coefficient" | "neuron_leak_coefficient" => {
1752                        if let Some(v) = value.as_f64() {
1753                            area.add_property_mut(
1754                                "leak_coefficient".to_string(),
1755                                serde_json::json!(v),
1756                            );
1757                        }
1758                    }
1759                    "leak_variability" | "neuron_leak_variability" => {
1760                        if let Some(v) = value.as_f64() {
1761                            area.add_property_mut(
1762                                "leak_variability".to_string(),
1763                                serde_json::json!(v),
1764                            );
1765                        }
1766                    }
1767                    "refractory_period" | "neuron_refractory_period" => {
1768                        if let Some(v) = value.as_u64() {
1769                            area.add_property_mut(
1770                                "refractory_period".to_string(),
1771                                serde_json::json!(v as u32),
1772                            );
1773                        }
1774                    }
1775                    "snooze_period" | "neuron_snooze_period" => {
1776                        if let Some(v) = value.as_u64() {
1777                            area.add_property_mut(
1778                                "snooze_period".to_string(),
1779                                serde_json::json!(v as u32),
1780                            );
1781                        }
1782                    }
1783                    "consecutive_fire_count" | "neuron_consecutive_fire_count" => {
1784                        if let Some(v) = value.as_u64() {
1785                            area.add_property_mut(
1786                                "consecutive_fire_limit".to_string(),
1787                                serde_json::json!(v as u32),
1788                            );
1789                        }
1790                    }
1791                    "plasticity_constant" | "neuron_plasticity_constant" => {
1792                        if let Some(v) = value.as_f64() {
1793                            area.add_property_mut(
1794                                "plasticity_constant".to_string(),
1795                                serde_json::json!(v),
1796                            );
1797                        }
1798                    }
1799                    "degeneration" | "neuron_degeneracy_coefficient" => {
1800                        if let Some(v) = value.as_f64() {
1801                            area.add_property_mut("degeneration".to_string(), serde_json::json!(v));
1802                        }
1803                    }
1804                    "postsynaptic_current" | "neuron_post_synaptic_potential" => {
1805                        if let Some(v) = value.as_f64() {
1806                            area.add_property_mut(
1807                                "postsynaptic_current".to_string(),
1808                                serde_json::json!(v),
1809                            );
1810                        }
1811                    }
1812                    "postsynaptic_current_max" | "neuron_post_synaptic_potential_max" => {
1813                        if let Some(v) = value.as_f64() {
1814                            area.add_property_mut(
1815                                "postsynaptic_current_max".to_string(),
1816                                serde_json::json!(v),
1817                            );
1818                        }
1819                    }
1820                    "psp_uniform_distribution" | "neuron_psp_uniform_distribution" => {
1821                        if let Some(v) = value.as_bool() {
1822                            area.add_property_mut(
1823                                "psp_uniform_distribution".to_string(),
1824                                serde_json::json!(v),
1825                            );
1826                        }
1827                    }
1828                    "mp_driven_psp" | "neuron_mp_driven_psp" => {
1829                        if let Some(v) = value.as_bool() {
1830                            area.add_property_mut(
1831                                "mp_driven_psp".to_string(),
1832                                serde_json::json!(v),
1833                            );
1834                            info!(
1835                                target: "feagi-services",
1836                                "[CONNECTOME-UPDATE] Updated mp_driven_psp={} in ConnectomeManager for area {}",
1837                                v, cortical_id
1838                            );
1839                        }
1840                    }
1841                    "mp_charge_accumulation" | "neuron_mp_charge_accumulation" => {
1842                        if let Some(v) = value.as_bool() {
1843                            area.add_property_mut(
1844                                "mp_charge_accumulation".to_string(),
1845                                serde_json::json!(v),
1846                            );
1847                        } else {
1848                            warn!(
1849                                target: "feagi-services",
1850                                "[CONNECTOME-UPDATE] mp_charge_accumulation not applied: value must be JSON boolean, got {:?} (area {})",
1851                                value,
1852                                cortical_id
1853                            );
1854                        }
1855                    }
1856                    "excitability" | "neuron_excitability" => {
1857                        if let Some(v) = value.as_f64() {
1858                            area.add_property_mut("excitability".to_string(), serde_json::json!(v));
1859                        }
1860                    }
1861                    "init_lifespan" | "neuron_init_lifespan" => {
1862                        if let Some(v) = value.as_u64() {
1863                            area.add_property_mut(
1864                                "init_lifespan".to_string(),
1865                                serde_json::json!(v as u32),
1866                            );
1867                        }
1868                    }
1869                    "lifespan_growth_rate" | "neuron_lifespan_growth_rate" => {
1870                        if let Some(v) = value.as_u64() {
1871                            area.add_property_mut(
1872                                "lifespan_growth_rate".to_string(),
1873                                serde_json::json!(v as u32),
1874                            );
1875                        }
1876                    }
1877                    "longterm_mem_threshold" | "neuron_longterm_mem_threshold" => {
1878                        if let Some(v) = value.as_u64() {
1879                            area.add_property_mut(
1880                                "longterm_mem_threshold".to_string(),
1881                                serde_json::json!(v as u32),
1882                            );
1883                        }
1884                    }
1885                    "firing_threshold_increment" | "neuron_fire_threshold_increment" => {
1886                        // Expect either array [x, y, z] or dict {x, y, z}
1887                        if let Some(arr) = value.as_array() {
1888                            if arr.len() == 3 {
1889                                let x = arr[0].as_f64().unwrap_or(0.0);
1890                                let y = arr[1].as_f64().unwrap_or(0.0);
1891                                let z = arr[2].as_f64().unwrap_or(0.0);
1892
1893                                // Store both array format and individual x/y/z properties
1894                                area.add_property_mut(
1895                                    "firing_threshold_increment".to_string(),
1896                                    serde_json::json!(arr),
1897                                );
1898                                area.add_property_mut(
1899                                    "firing_threshold_increment_x".to_string(),
1900                                    serde_json::json!(x),
1901                                );
1902                                area.add_property_mut(
1903                                    "firing_threshold_increment_y".to_string(),
1904                                    serde_json::json!(y),
1905                                );
1906                                area.add_property_mut(
1907                                    "firing_threshold_increment_z".to_string(),
1908                                    serde_json::json!(z),
1909                                );
1910                            }
1911                        } else if let Some(obj) = value.as_object() {
1912                            // Convert {x, y, z} to individual properties
1913                            let x = obj.get("x").and_then(|v| v.as_f64()).unwrap_or(0.0);
1914                            let y = obj.get("y").and_then(|v| v.as_f64()).unwrap_or(0.0);
1915                            let z = obj.get("z").and_then(|v| v.as_f64()).unwrap_or(0.0);
1916
1917                            area.add_property_mut(
1918                                "firing_threshold_increment".to_string(),
1919                                serde_json::json!([x, y, z]),
1920                            );
1921                            area.add_property_mut(
1922                                "firing_threshold_increment_x".to_string(),
1923                                serde_json::json!(x),
1924                            );
1925                            area.add_property_mut(
1926                                "firing_threshold_increment_y".to_string(),
1927                                serde_json::json!(y),
1928                            );
1929                            area.add_property_mut(
1930                                "firing_threshold_increment_z".to_string(),
1931                                serde_json::json!(z),
1932                            );
1933                        }
1934                    }
1935                    "burst_engine_active" => {
1936                        if let Some(v) = value.as_bool() {
1937                            area.add_property_mut(
1938                                "burst_engine_active".to_string(),
1939                                serde_json::json!(v),
1940                            );
1941                        }
1942                    }
1943                    _ => {}
1944                }
1945            }
1946            manager.refresh_cortical_area_hashes(true, false);
1947        }
1948
1949        // If memory-related parameters were updated, immediately apply them to the runtime
1950        // plasticity subsystem (and FireLedger tracking), otherwise changes only take effect after save+reload.
1951        //
1952        // BV behavior observed:
1953        // - Update via API updates RuntimeGenome + ConnectomeManager
1954        // - PlasticityService keeps running with the old temporal_depth/lifecycle config until genome reload
1955        //
1956        // This block re-registers the memory area configuration in the live executor.
1957        #[cfg(feature = "plasticity")]
1958        {
1959            let memory_param_changed = changes.keys().any(|k| {
1960                matches!(
1961                    k.as_str(),
1962                    "init_lifespan"
1963                        | "neuron_init_lifespan"
1964                        | "lifespan_growth_rate"
1965                        | "neuron_lifespan_growth_rate"
1966                        | "longterm_mem_threshold"
1967                        | "neuron_longterm_mem_threshold"
1968                        | "temporal_depth"
1969                )
1970            });
1971
1972            if memory_param_changed {
1973                use feagi_evolutionary::extract_memory_properties;
1974                use feagi_npu_plasticity::{MemoryNeuronLifecycleConfig, PlasticityExecutor};
1975
1976                let mut manager = self.connectome.write();
1977                if let Some(area) = manager.get_cortical_area(&cortical_id_typed) {
1978                    if let Some(mem_props) = extract_memory_properties(&area.properties) {
1979                        // Ensure upstream tracking is consistent with current mappings before re-registering.
1980                        let _ = manager
1981                            .refresh_upstream_cortical_areas_from_mappings(&cortical_id_typed);
1982
1983                        // Update FireLedger upstream tracking for this memory area (monotonic-increase).
1984                        // Note: FireLedger track_area is an *exact* setting; this uses max(existing, desired)
1985                        // to avoid shrinking windows that may be required elsewhere (e.g., other memory areas).
1986                        if let Some(npu_arc) = manager.get_npu().cloned() {
1987                            if let Ok(mut npu) = npu_arc.lock() {
1988                                let upstream_areas =
1989                                    manager.get_upstream_cortical_areas(&cortical_id_typed);
1990                                let existing_configs = npu.get_all_fire_ledger_configs();
1991                                let desired = mem_props.temporal_depth as usize;
1992
1993                                for upstream_idx in upstream_areas.iter().copied() {
1994                                    let existing = existing_configs
1995                                        .iter()
1996                                        .find(|(idx, _)| *idx == upstream_idx)
1997                                        .map(|(_, w)| *w)
1998                                        .unwrap_or(0);
1999                                    let resolved = existing.max(desired);
2000                                    if resolved != existing {
2001                                        if let Err(e) =
2002                                            npu.configure_fire_ledger_window(upstream_idx, resolved)
2003                                        {
2004                                            warn!(
2005                                                target: "feagi-services",
2006                                                "[GENOME-UPDATE] Failed to configure FireLedger window for upstream idx={} (requested={}): {}",
2007                                                upstream_idx,
2008                                                resolved,
2009                                                e
2010                                            );
2011                                        }
2012                                    }
2013                                }
2014                            } else {
2015                                warn!(target: "feagi-services", "[GENOME-UPDATE] Failed to lock NPU for FireLedger update");
2016                            }
2017                        }
2018
2019                        // Re-register memory area config in PlasticityExecutor so temporal_depth/lifecycle changes apply immediately.
2020                        if let Some(executor) = manager.get_plasticity_executor() {
2021                            if let Ok(exec) = executor.lock() {
2022                                let upstream_areas =
2023                                    manager.get_upstream_cortical_areas(&cortical_id_typed);
2024                                let upstream_non_memory =
2025                                    manager.filter_non_memory_upstream_areas(&upstream_areas);
2026                                let lifecycle_config = MemoryNeuronLifecycleConfig {
2027                                    initial_lifespan: mem_props.init_lifespan,
2028                                    lifespan_growth_rate: mem_props.lifespan_growth_rate,
2029                                    longterm_threshold: mem_props.longterm_threshold,
2030                                    max_reactivations: 1000,
2031                                };
2032
2033                                exec.register_memory_area(
2034                                    cortical_idx,
2035                                    cortical_id.to_string(),
2036                                    mem_props.temporal_depth,
2037                                    upstream_non_memory,
2038                                    Some(lifecycle_config),
2039                                );
2040                            } else {
2041                                warn!(target: "feagi-services", "[GENOME-UPDATE] Failed to lock PlasticityExecutor for memory-area update");
2042                            }
2043                        }
2044                    }
2045                }
2046            }
2047        }
2048
2049        info!(target: "feagi-services", "[FAST-UPDATE] Parameter update complete");
2050
2051        // Return updated info
2052        self.get_cortical_area_info(cortical_id).await
2053    }
2054
2055    /// Update IO coding parameters and remap the cortical ID.
2056    ///
2057    /// This updates both RuntimeGenome and ConnectomeManager, preserving cortical_idx.
2058    fn apply_io_coding_update(
2059        &self,
2060        cortical_id: &CorticalID,
2061        cortical_id_str: &str,
2062        changes: &HashMap<String, Value>,
2063    ) -> ServiceResult<CorticalID> {
2064        let new_cortical_id_str = changes
2065            .get("new_cortical_id")
2066            .and_then(|v| v.as_str())
2067            .ok_or_else(|| {
2068                ServiceError::InvalidInput(
2069                    "new_cortical_id is required when updating IO coding parameters".to_string(),
2070                )
2071            })?;
2072        let _new_cortical_id = CorticalID::try_from_base_64(new_cortical_id_str).map_err(|e| {
2073            ServiceError::InvalidInput(format!(
2074                "Invalid new_cortical_id '{}': {}",
2075                new_cortical_id_str, e
2076            ))
2077        })?;
2078
2079        let bytes = cortical_id.as_bytes();
2080        let is_input = bytes[0] == b'i';
2081        let is_output = bytes[0] == b'o';
2082        if !is_input && !is_output {
2083            return Err(ServiceError::InvalidInput(
2084                "IO coding updates only apply to IPU/OPU cortical areas".to_string(),
2085            ));
2086        }
2087
2088        let current_flag = cortical_id.extract_io_data_flag().map_err(|e| {
2089            ServiceError::InvalidInput(format!(
2090                "Unable to decode IO configuration from cortical ID '{}': {}",
2091                cortical_id_str, e
2092            ))
2093        })?;
2094
2095        let parse_frame = |raw: &str| -> Option<FrameChangeHandling> {
2096            let lower = raw.trim().to_ascii_lowercase();
2097            match lower.as_str() {
2098                "absolute" => Some(FrameChangeHandling::Absolute),
2099                "incremental" => Some(FrameChangeHandling::Incremental),
2100                _ => None,
2101            }
2102        };
2103        let parse_positioning = |raw: &str| -> Option<PercentageNeuronPositioning> {
2104            let lower = raw.trim().to_ascii_lowercase();
2105            match lower.as_str() {
2106                "linear" => Some(PercentageNeuronPositioning::Linear),
2107                "fractional" => Some(PercentageNeuronPositioning::Fractional),
2108                _ => None,
2109            }
2110        };
2111        let parse_signage = |raw: &str| -> Option<bool> {
2112            let lower = raw.trim().to_ascii_lowercase();
2113            if lower.contains("unsigned") {
2114                Some(false)
2115            } else if lower.contains("signed") {
2116                Some(true)
2117            } else {
2118                None
2119            }
2120        };
2121
2122        let requested_signage = changes.get("coding_signage").and_then(|v| v.as_str());
2123        let requested_behavior = changes.get("coding_behavior").and_then(|v| v.as_str());
2124        let requested_type = changes.get("coding_type").and_then(|v| v.as_str());
2125
2126        let new_flag = match current_flag {
2127            IOCorticalAreaConfigurationFlag::Percentage(frame, positioning) => {
2128                let signed = requested_signage.and_then(parse_signage).unwrap_or(false);
2129                let new_frame = requested_behavior.and_then(parse_frame).unwrap_or(frame);
2130                let new_positioning = requested_type
2131                    .and_then(parse_positioning)
2132                    .unwrap_or(positioning);
2133                if signed {
2134                    IOCorticalAreaConfigurationFlag::SignedPercentage(new_frame, new_positioning)
2135                } else {
2136                    IOCorticalAreaConfigurationFlag::Percentage(new_frame, new_positioning)
2137                }
2138            }
2139            IOCorticalAreaConfigurationFlag::Percentage2D(frame, positioning) => {
2140                let signed = requested_signage.and_then(parse_signage).unwrap_or(false);
2141                let new_frame = requested_behavior.and_then(parse_frame).unwrap_or(frame);
2142                let new_positioning = requested_type
2143                    .and_then(parse_positioning)
2144                    .unwrap_or(positioning);
2145                if signed {
2146                    IOCorticalAreaConfigurationFlag::SignedPercentage2D(new_frame, new_positioning)
2147                } else {
2148                    IOCorticalAreaConfigurationFlag::Percentage2D(new_frame, new_positioning)
2149                }
2150            }
2151            IOCorticalAreaConfigurationFlag::Percentage3D(frame, positioning) => {
2152                let signed = requested_signage.and_then(parse_signage).unwrap_or(false);
2153                let new_frame = requested_behavior.and_then(parse_frame).unwrap_or(frame);
2154                let new_positioning = requested_type
2155                    .and_then(parse_positioning)
2156                    .unwrap_or(positioning);
2157                if signed {
2158                    IOCorticalAreaConfigurationFlag::SignedPercentage3D(new_frame, new_positioning)
2159                } else {
2160                    IOCorticalAreaConfigurationFlag::Percentage3D(new_frame, new_positioning)
2161                }
2162            }
2163            IOCorticalAreaConfigurationFlag::Percentage4D(frame, positioning) => {
2164                let signed = requested_signage.and_then(parse_signage).unwrap_or(false);
2165                let new_frame = requested_behavior.and_then(parse_frame).unwrap_or(frame);
2166                let new_positioning = requested_type
2167                    .and_then(parse_positioning)
2168                    .unwrap_or(positioning);
2169                if signed {
2170                    IOCorticalAreaConfigurationFlag::SignedPercentage4D(new_frame, new_positioning)
2171                } else {
2172                    IOCorticalAreaConfigurationFlag::Percentage4D(new_frame, new_positioning)
2173                }
2174            }
2175            IOCorticalAreaConfigurationFlag::SignedPercentage(frame, positioning) => {
2176                let signed = requested_signage.and_then(parse_signage).unwrap_or(true);
2177                let new_frame = requested_behavior.and_then(parse_frame).unwrap_or(frame);
2178                let new_positioning = requested_type
2179                    .and_then(parse_positioning)
2180                    .unwrap_or(positioning);
2181                if signed {
2182                    IOCorticalAreaConfigurationFlag::SignedPercentage(new_frame, new_positioning)
2183                } else {
2184                    IOCorticalAreaConfigurationFlag::Percentage(new_frame, new_positioning)
2185                }
2186            }
2187            IOCorticalAreaConfigurationFlag::SignedPercentage2D(frame, positioning) => {
2188                let signed = requested_signage.and_then(parse_signage).unwrap_or(true);
2189                let new_frame = requested_behavior.and_then(parse_frame).unwrap_or(frame);
2190                let new_positioning = requested_type
2191                    .and_then(parse_positioning)
2192                    .unwrap_or(positioning);
2193                if signed {
2194                    IOCorticalAreaConfigurationFlag::SignedPercentage2D(new_frame, new_positioning)
2195                } else {
2196                    IOCorticalAreaConfigurationFlag::Percentage2D(new_frame, new_positioning)
2197                }
2198            }
2199            IOCorticalAreaConfigurationFlag::SignedPercentage3D(frame, positioning) => {
2200                let signed = requested_signage.and_then(parse_signage).unwrap_or(true);
2201                let new_frame = requested_behavior.and_then(parse_frame).unwrap_or(frame);
2202                let new_positioning = requested_type
2203                    .and_then(parse_positioning)
2204                    .unwrap_or(positioning);
2205                if signed {
2206                    IOCorticalAreaConfigurationFlag::SignedPercentage3D(new_frame, new_positioning)
2207                } else {
2208                    IOCorticalAreaConfigurationFlag::Percentage3D(new_frame, new_positioning)
2209                }
2210            }
2211            IOCorticalAreaConfigurationFlag::SignedPercentage4D(frame, positioning) => {
2212                let signed = requested_signage.and_then(parse_signage).unwrap_or(true);
2213                let new_frame = requested_behavior.and_then(parse_frame).unwrap_or(frame);
2214                let new_positioning = requested_type
2215                    .and_then(parse_positioning)
2216                    .unwrap_or(positioning);
2217                if signed {
2218                    IOCorticalAreaConfigurationFlag::SignedPercentage4D(new_frame, new_positioning)
2219                } else {
2220                    IOCorticalAreaConfigurationFlag::Percentage4D(new_frame, new_positioning)
2221                }
2222            }
2223            IOCorticalAreaConfigurationFlag::CartesianPlane(frame) => {
2224                if let Some(signage) = requested_signage {
2225                    if !signage.trim().eq_ignore_ascii_case("not applicable") {
2226                        return Err(ServiceError::InvalidInput(
2227                            "coding_signage not supported for CartesianPlane".to_string(),
2228                        ));
2229                    }
2230                }
2231                if let Some(coding_type) = requested_type {
2232                    if !coding_type.trim().eq_ignore_ascii_case("not applicable") {
2233                        return Err(ServiceError::InvalidInput(
2234                            "coding_type not supported for CartesianPlane".to_string(),
2235                        ));
2236                    }
2237                }
2238                let new_frame = requested_behavior.and_then(parse_frame).unwrap_or(frame);
2239                IOCorticalAreaConfigurationFlag::CartesianPlane(new_frame)
2240            }
2241            IOCorticalAreaConfigurationFlag::Misc(frame) => {
2242                if let Some(signage) = requested_signage {
2243                    if !signage.trim().eq_ignore_ascii_case("not applicable") {
2244                        return Err(ServiceError::InvalidInput(
2245                            "coding_signage not supported for Misc".to_string(),
2246                        ));
2247                    }
2248                }
2249                if let Some(coding_type) = requested_type {
2250                    if !coding_type.trim().eq_ignore_ascii_case("not applicable") {
2251                        return Err(ServiceError::InvalidInput(
2252                            "coding_type not supported for Misc".to_string(),
2253                        ));
2254                    }
2255                }
2256                let new_frame = requested_behavior.and_then(parse_frame).unwrap_or(frame);
2257                IOCorticalAreaConfigurationFlag::Misc(new_frame)
2258            }
2259            IOCorticalAreaConfigurationFlag::Boolean => {
2260                if let Some(signage) = requested_signage {
2261                    if !signage.trim().eq_ignore_ascii_case("boolean")
2262                        && !signage.trim().eq_ignore_ascii_case("not applicable")
2263                    {
2264                        return Err(ServiceError::InvalidInput(
2265                            "coding_signage not supported for Boolean".to_string(),
2266                        ));
2267                    }
2268                }
2269                if let Some(coding_type) = requested_type {
2270                    if !coding_type.trim().eq_ignore_ascii_case("not applicable") {
2271                        return Err(ServiceError::InvalidInput(
2272                            "coding_type not supported for Boolean".to_string(),
2273                        ));
2274                    }
2275                }
2276                if let Some(behavior) = requested_behavior {
2277                    if !behavior.trim().eq_ignore_ascii_case("not applicable") {
2278                        return Err(ServiceError::InvalidInput(
2279                            "coding_behavior not supported for Boolean".to_string(),
2280                        ));
2281                    }
2282                }
2283                IOCorticalAreaConfigurationFlag::Boolean
2284            }
2285        };
2286
2287        let unit_identifier = [bytes[1], bytes[2], bytes[3]];
2288        let cortical_subunit_index = CorticalSubUnitIndex::from(bytes[6]);
2289        let cortical_unit_index = CorticalUnitIndex::from(bytes[7]);
2290        let computed_id = new_flag.as_io_cortical_id(
2291            is_input,
2292            unit_identifier,
2293            cortical_unit_index,
2294            cortical_subunit_index,
2295        );
2296
2297        if computed_id.as_base_64() != new_cortical_id_str {
2298            return Err(ServiceError::InvalidInput(format!(
2299                "new_cortical_id '{}' does not match computed ID '{}'",
2300                new_cortical_id_str,
2301                computed_id.as_base_64()
2302            )));
2303        }
2304        info!(
2305            target: "feagi-services",
2306            "[IO-CODING] Remapping cortical ID {} -> {}",
2307            cortical_id.as_base_64(),
2308            computed_id.as_base_64()
2309        );
2310
2311        let new_cortical_type = if is_input {
2312            CorticalAreaType::BrainInput(new_flag)
2313        } else {
2314            CorticalAreaType::BrainOutput(new_flag)
2315        };
2316
2317        // Update RuntimeGenome if available
2318        if let Some(genome) = self.current_genome.write().as_mut() {
2319            let mut area = genome.cortical_areas.remove(cortical_id).ok_or_else(|| {
2320                ServiceError::NotFound {
2321                    resource: "CorticalArea".to_string(),
2322                    id: cortical_id.as_base_64(),
2323                }
2324            })?;
2325            area.cortical_id = computed_id;
2326            area.cortical_type = new_cortical_type;
2327            genome.cortical_areas.insert(computed_id, area);
2328
2329            for region in genome.brain_regions.values_mut() {
2330                if region.cortical_areas.remove(cortical_id) {
2331                    region.cortical_areas.insert(computed_id);
2332                }
2333            }
2334
2335            let old_id_str = cortical_id.as_base_64();
2336            let new_id_str = computed_id.as_base_64();
2337            for area in genome.cortical_areas.values_mut() {
2338                if let Some(mapping) = area
2339                    .properties
2340                    .get_mut("cortical_mapping_dst")
2341                    .and_then(|v| v.as_object_mut())
2342                {
2343                    if let Some(value) = mapping.remove(&old_id_str) {
2344                        mapping.insert(new_id_str.clone(), value);
2345                    }
2346                }
2347            }
2348        }
2349
2350        // Update ConnectomeManager (runtime only; keep NPU registry unchanged)
2351        {
2352            let mut manager = self.connectome.write();
2353            manager.rename_cortical_area_id_with_options(
2354                cortical_id,
2355                computed_id,
2356                new_cortical_type,
2357                false,
2358            )?;
2359        }
2360
2361        // Refresh burst runner cache to propagate updated cortical_id mapping.
2362        self.refresh_burst_runner_cache();
2363
2364        Ok(computed_id)
2365    }
2366
2367    /// Update the unit index (group_id) for an IO cortical area and remap the cortical ID.
2368    ///
2369    /// This updates both RuntimeGenome and ConnectomeManager, preserving cortical_idx.
2370    fn apply_unit_index_update(
2371        &self,
2372        cortical_id: &CorticalID,
2373        cortical_id_str: &str,
2374        changes: &HashMap<String, Value>,
2375    ) -> ServiceResult<CorticalID> {
2376        let new_group_id_value = changes.get("group_id").ok_or_else(|| {
2377            ServiceError::InvalidInput("group_id is required for unit index update".to_string())
2378        })?;
2379        let new_group_id = if let Some(value) = new_group_id_value.as_u64() {
2380            value
2381                .try_into()
2382                .map_err(|_| ServiceError::InvalidInput("group_id out of range".to_string()))?
2383        } else if let Some(value) = new_group_id_value.as_f64() {
2384            if value.fract() != 0.0 {
2385                return Err(ServiceError::InvalidInput(
2386                    "group_id must be an integer".to_string(),
2387                ));
2388            }
2389            let as_u64 = value as u64;
2390            as_u64
2391                .try_into()
2392                .map_err(|_| ServiceError::InvalidInput("group_id out of range".to_string()))?
2393        } else if let Some(raw) = new_group_id_value.as_str() {
2394            raw.parse::<u8>().map_err(|_| {
2395                ServiceError::InvalidInput("group_id must be an integer".to_string())
2396            })?
2397        } else {
2398            return Err(ServiceError::InvalidInput(
2399                "group_id must be an integer".to_string(),
2400            ));
2401        };
2402
2403        let bytes = cortical_id.as_bytes();
2404        let is_input = bytes[0] == b'i';
2405        let is_output = bytes[0] == b'o';
2406        if !is_input && !is_output {
2407            return Err(ServiceError::InvalidInput(
2408                "Unit index updates only apply to IPU/OPU cortical areas".to_string(),
2409            ));
2410        }
2411
2412        let current_flag = cortical_id.extract_io_data_flag().map_err(|e| {
2413            ServiceError::InvalidInput(format!(
2414                "Unable to decode IO configuration from cortical ID '{}': {}",
2415                cortical_id_str, e
2416            ))
2417        })?;
2418        let unit_identifier = [bytes[1], bytes[2], bytes[3]];
2419        let cortical_subunit_index = CorticalSubUnitIndex::from(bytes[6]);
2420        let cortical_unit_index = CorticalUnitIndex::from(new_group_id);
2421        let computed_id = current_flag.as_io_cortical_id(
2422            is_input,
2423            unit_identifier,
2424            cortical_unit_index,
2425            cortical_subunit_index,
2426        );
2427
2428        let new_cortical_type = if is_input {
2429            CorticalAreaType::BrainInput(current_flag)
2430        } else {
2431            CorticalAreaType::BrainOutput(current_flag)
2432        };
2433
2434        if &computed_id == cortical_id {
2435            if let Some(genome) = self.current_genome.write().as_mut() {
2436                if let Some(area) = genome.cortical_areas.get_mut(cortical_id) {
2437                    area.properties
2438                        .insert("group_id".to_string(), serde_json::json!(new_group_id));
2439                }
2440            }
2441            let mut manager = self.connectome.write();
2442            if let Some(area) = manager.get_cortical_area_mut(cortical_id) {
2443                area.properties
2444                    .insert("group_id".to_string(), serde_json::json!(new_group_id));
2445            }
2446            return Ok(*cortical_id);
2447        }
2448
2449        info!(
2450            target: "feagi-services",
2451            "[UNIT-INDEX] Remapping cortical ID {} -> {}",
2452            cortical_id.as_base_64(),
2453            computed_id.as_base_64()
2454        );
2455
2456        // Update RuntimeGenome if available
2457        if let Some(genome) = self.current_genome.write().as_mut() {
2458            let mut area = genome.cortical_areas.remove(cortical_id).ok_or_else(|| {
2459                ServiceError::NotFound {
2460                    resource: "CorticalArea".to_string(),
2461                    id: cortical_id.as_base_64(),
2462                }
2463            })?;
2464            area.cortical_id = computed_id;
2465            area.cortical_type = new_cortical_type;
2466            area.properties
2467                .insert("group_id".to_string(), serde_json::json!(new_group_id));
2468            genome.cortical_areas.insert(computed_id, area);
2469
2470            for region in genome.brain_regions.values_mut() {
2471                if region.cortical_areas.remove(cortical_id) {
2472                    region.cortical_areas.insert(computed_id);
2473                }
2474            }
2475
2476            let old_id_str = cortical_id.as_base_64();
2477            let new_id_str = computed_id.as_base_64();
2478            for area in genome.cortical_areas.values_mut() {
2479                if let Some(mapping) = area
2480                    .properties
2481                    .get_mut("cortical_mapping_dst")
2482                    .and_then(|v| v.as_object_mut())
2483                {
2484                    if let Some(value) = mapping.remove(&old_id_str) {
2485                        mapping.insert(new_id_str.clone(), value);
2486                    }
2487                }
2488            }
2489        }
2490
2491        // Update ConnectomeManager (runtime only; keep NPU registry unchanged)
2492        {
2493            let mut manager = self.connectome.write();
2494            manager.rename_cortical_area_id_with_options(
2495                cortical_id,
2496                computed_id,
2497                new_cortical_type,
2498                false,
2499            )?;
2500            if let Some(area) = manager.get_cortical_area_mut(&computed_id) {
2501                area.properties
2502                    .insert("group_id".to_string(), serde_json::json!(new_group_id));
2503            }
2504        }
2505
2506        // Refresh burst runner cache to propagate updated cortical_id mapping.
2507        self.refresh_burst_runner_cache();
2508
2509        Ok(computed_id)
2510    }
2511
2512    /// Regenerate incoming/outgoing synapses for a cortical area without rebuilding neurons.
2513    fn regenerate_mappings_for_area(&self, cortical_id: &CorticalID) -> ServiceResult<()> {
2514        let mut manager = self.connectome.write();
2515        let cortical_id_str = cortical_id.as_base_64();
2516
2517        let outgoing_targets = {
2518            let Some(area) = manager.get_cortical_area(cortical_id) else {
2519                return Err(ServiceError::NotFound {
2520                    resource: "CorticalArea".to_string(),
2521                    id: cortical_id_str.clone(),
2522                });
2523            };
2524            let mut targets = Vec::new();
2525            if let Some(mapping) = area
2526                .properties
2527                .get("cortical_mapping_dst")
2528                .and_then(|v| v.as_object())
2529            {
2530                for key in mapping.keys() {
2531                    match CorticalID::try_from_base_64(key) {
2532                        Ok(dst_id) => targets.push(dst_id),
2533                        Err(err) => warn!(
2534                            target: "feagi-services",
2535                            "Invalid cortical_mapping_dst ID '{}': {}",
2536                            key,
2537                            err
2538                        ),
2539                    }
2540                }
2541            }
2542            targets
2543        };
2544
2545        let incoming_sources = {
2546            let mut sources = Vec::new();
2547            for src_id in manager.get_all_cortical_ids() {
2548                if src_id == *cortical_id {
2549                    continue;
2550                }
2551                let Some(src_area) = manager.get_cortical_area(&src_id) else {
2552                    continue;
2553                };
2554                if let Some(mapping) = src_area
2555                    .properties
2556                    .get("cortical_mapping_dst")
2557                    .and_then(|v| v.as_object())
2558                {
2559                    if mapping.contains_key(&cortical_id_str) {
2560                        sources.push(src_id);
2561                    }
2562                }
2563            }
2564            sources
2565        };
2566
2567        for dst_id in outgoing_targets {
2568            if dst_id == *cortical_id {
2569                continue;
2570            }
2571            manager
2572                .regenerate_synapses_for_mapping(cortical_id, &dst_id)
2573                .map_err(|e| {
2574                    ServiceError::Backend(format!(
2575                        "Failed to regenerate outgoing synapses {} -> {}: {}",
2576                        cortical_id.as_base_64(),
2577                        dst_id.as_base_64(),
2578                        e
2579                    ))
2580                })?;
2581        }
2582
2583        for src_id in incoming_sources {
2584            manager
2585                .regenerate_synapses_for_mapping(&src_id, cortical_id)
2586                .map_err(|e| {
2587                    ServiceError::Backend(format!(
2588                        "Failed to regenerate incoming synapses {} -> {}: {}",
2589                        src_id.as_base_64(),
2590                        cortical_id.as_base_64(),
2591                        e
2592                    ))
2593                })?;
2594        }
2595
2596        Ok(())
2597    }
2598
2599    /// Fastest path: Update only metadata without affecting neurons/synapses
2600    ///
2601    /// Performance: ~1ms (metadata changes only)
2602    async fn update_metadata_only(
2603        &self,
2604        cortical_id: &str,
2605        changes: HashMap<String, Value>,
2606    ) -> ServiceResult<CorticalAreaInfo> {
2607        info!(target: "feagi-services", "[METADATA-UPDATE] Metadata-only update for {}", cortical_id);
2608        let needs_burst_cache_refresh = changes.contains_key("visualization_voxel_granularity");
2609        let mut properties_changed = false;
2610        let mut geometry_changed = false;
2611        for key in changes.keys() {
2612            match key.as_str() {
2613                "coordinates_3d" | "coordinate_3d" | "coordinates" | "position"
2614                | "coordinate_2d" | "coordinates_2d" => {
2615                    geometry_changed = true;
2616                }
2617                "cortical_name" | "name" | "visible" | "visualization_voxel_granularity" => {
2618                    properties_changed = true;
2619                }
2620                _ => {
2621                    properties_changed = true;
2622                }
2623            }
2624        }
2625
2626        // Convert cortical_id to CorticalID
2627        let cortical_id_typed =
2628            feagi_evolutionary::string_to_cortical_id(cortical_id).map_err(|e| {
2629                ServiceError::InvalidInput(format!("Invalid cortical ID '{}': {}", cortical_id, e))
2630            })?;
2631        let mut effective_cortical_id = cortical_id_typed;
2632        let mut effective_cortical_id_str = cortical_id.to_string();
2633
2634        let has_coding_update = changes.contains_key("coding_signage")
2635            || changes.contains_key("coding_behavior")
2636            || changes.contains_key("coding_type")
2637            || changes.contains_key("new_cortical_id");
2638        if has_coding_update {
2639            let new_id = self.apply_io_coding_update(&cortical_id_typed, cortical_id, &changes)?;
2640            effective_cortical_id = new_id;
2641            effective_cortical_id_str = new_id.as_base_64();
2642        }
2643
2644        // Update RuntimeGenome if available
2645        if let Some(genome) = self.current_genome.write().as_mut() {
2646            if let Some(area) = genome.cortical_areas.get_mut(&effective_cortical_id) {
2647                for (key, value) in &changes {
2648                    match key.as_str() {
2649                        "cortical_name" | "name" => {
2650                            if let Some(name) = value.as_str() {
2651                                area.name = name.to_string();
2652                            }
2653                        }
2654                        "coordinates_3d" | "coordinate_3d" | "coordinates" | "position" => {
2655                            // Parse coordinates - support both array [x, y, z] and object {"x": x, "y": y, "z": z}
2656                            if let Some(arr) = value.as_array() {
2657                                // Array format: [x, y, z]
2658                                if arr.len() >= 3 {
2659                                    let x = arr[0].as_i64().unwrap_or(0) as i32;
2660                                    let y = arr[1].as_i64().unwrap_or(0) as i32;
2661                                    let z = arr[2].as_i64().unwrap_or(0) as i32;
2662                                    area.position = (x, y, z).into();
2663                                    info!(target: "feagi-services", "[GENOME-UPDATE] Updated position (array format): ({}, {}, {})", x, y, z);
2664                                }
2665                            } else if let Some(obj) = value.as_object() {
2666                                // Object format: {"x": x, "y": y, "z": z}
2667                                let x = obj.get("x").and_then(|v| v.as_i64()).unwrap_or(0) as i32;
2668                                let y = obj.get("y").and_then(|v| v.as_i64()).unwrap_or(0) as i32;
2669                                let z = obj.get("z").and_then(|v| v.as_i64()).unwrap_or(0) as i32;
2670                                area.position = (x, y, z).into();
2671                                info!(target: "feagi-services", "[GENOME-UPDATE] Updated position (object format): ({}, {}, {})", x, y, z);
2672                            }
2673                        }
2674                        "coordinate_2d" | "coordinates_2d" => {
2675                            if let Some(arr) = value.as_array() {
2676                                if arr.len() >= 2 {
2677                                    let x = arr[0].as_i64().unwrap_or(0) as i32;
2678                                    let y = arr[1].as_i64().unwrap_or(0) as i32;
2679                                    area.properties.insert(
2680                                        "coordinate_2d".to_string(),
2681                                        serde_json::json!([x, y]),
2682                                    );
2683                                    info!(target: "feagi-services", "[GENOME-UPDATE] Updated coordinate_2d: ({}, {})", x, y);
2684                                } else {
2685                                    warn!(target: "feagi-services", "[GENOME-UPDATE] coordinate_2d array must have 2 elements, got {}", arr.len());
2686                                }
2687                            } else if let Some(obj) = value.as_object() {
2688                                let x = obj.get("x").and_then(|v| v.as_i64()).unwrap_or(0) as i32;
2689                                let y = obj.get("y").and_then(|v| v.as_i64()).unwrap_or(0) as i32;
2690                                area.properties
2691                                    .insert("coordinate_2d".to_string(), serde_json::json!([x, y]));
2692                                info!(target: "feagi-services", "[GENOME-UPDATE] Updated coordinate_2d (object format): ({}, {})", x, y);
2693                            } else {
2694                                warn!(target: "feagi-services", "[GENOME-UPDATE] coordinate_2d must be array or object, got: {:?}", value);
2695                            }
2696                        }
2697                        "visualization_voxel_granularity" => {
2698                            // Only store if != 1x1x1 (default), delete if set to 1x1x1
2699                            info!(target: "feagi-services", "[GENOME-UPDATE] Received visualization_voxel_granularity update: {:?}", value);
2700                            if let Some(arr) = value.as_array() {
2701                                if arr.len() == 3 {
2702                                    // Try to parse as integers (u64) or floats (f64), then convert to u32
2703                                    let x_opt = arr[0]
2704                                        .as_u64()
2705                                        .or_else(|| arr[0].as_f64().map(|f| f as u64));
2706                                    let y_opt = arr[1]
2707                                        .as_u64()
2708                                        .or_else(|| arr[1].as_f64().map(|f| f as u64));
2709                                    let z_opt = arr[2]
2710                                        .as_u64()
2711                                        .or_else(|| arr[2].as_f64().map(|f| f as u64));
2712
2713                                    if let (Some(x), Some(y), Some(z)) = (x_opt, y_opt, z_opt) {
2714                                        let x_u32 = x as u32;
2715                                        let y_u32 = y as u32;
2716                                        let z_u32 = z as u32;
2717
2718                                        // Default is 1x1x1 - only store if different
2719                                        if x_u32 == 1 && y_u32 == 1 && z_u32 == 1 {
2720                                            // Remove override (return to default)
2721                                            area.properties
2722                                                .remove("visualization_voxel_granularity");
2723                                            info!(target: "feagi-services", "[GENOME-UPDATE] Removed visualization_voxel_granularity override (returned to default 1x1x1)");
2724                                        } else {
2725                                            // Store override (non-default value) as integer array
2726                                            area.properties.insert(
2727                                                "visualization_voxel_granularity".to_string(),
2728                                                serde_json::json!([x_u32, y_u32, z_u32]),
2729                                            );
2730                                            info!(target: "feagi-services", "[GENOME-UPDATE] Updated visualization_voxel_granularity: ({}, {}, {})", x_u32, y_u32, z_u32);
2731                                        }
2732                                    } else {
2733                                        warn!(target: "feagi-services", "[GENOME-UPDATE] Failed to parse visualization_voxel_granularity array values as integers");
2734                                    }
2735                                } else {
2736                                    warn!(target: "feagi-services", "[GENOME-UPDATE] visualization_voxel_granularity array must have 3 elements, got {}", arr.len());
2737                                }
2738                            } else {
2739                                warn!(target: "feagi-services", "[GENOME-UPDATE] visualization_voxel_granularity must be an array, got: {:?}", value);
2740                            }
2741                        }
2742                        _ => {}
2743                    }
2744                }
2745            }
2746        }
2747
2748        // Update ConnectomeManager metadata
2749        {
2750            let mut manager = self.connectome.write();
2751            let area = manager
2752                .get_cortical_area_mut(&effective_cortical_id)
2753                .ok_or_else(|| ServiceError::NotFound {
2754                    resource: "CorticalArea".to_string(),
2755                    id: effective_cortical_id_str.clone(),
2756                })?;
2757
2758            // Update metadata fields
2759            for (key, value) in &changes {
2760                match key.as_str() {
2761                    "cortical_name" | "name" => {
2762                        if let Some(name) = value.as_str() {
2763                            area.name = name.to_string();
2764                        }
2765                    }
2766                    "visible" => {
2767                        if let Some(v) = value.as_bool() {
2768                            area.add_property_mut("visible".to_string(), serde_json::json!(v));
2769                        }
2770                    }
2771                    "coordinates_3d" | "coordinate_3d" | "coordinates" | "position" => {
2772                        // Parse coordinates - support both array [x, y, z] and object {"x": x, "y": y, "z": z}
2773                        if let Some(arr) = value.as_array() {
2774                            // Array format: [x, y, z]
2775                            if arr.len() >= 3 {
2776                                let x = arr[0].as_i64().unwrap_or(0) as i32;
2777                                let y = arr[1].as_i64().unwrap_or(0) as i32;
2778                                let z = arr[2].as_i64().unwrap_or(0) as i32;
2779                                area.position = (x, y, z).into();
2780                                info!(target: "feagi-services", "[CONNECTOME-UPDATE] Updated position (array format): ({}, {}, {})", x, y, z);
2781                            }
2782                        } else if let Some(obj) = value.as_object() {
2783                            // Object format: {"x": x, "y": y, "z": z}
2784                            let x = obj.get("x").and_then(|v| v.as_i64()).unwrap_or(0) as i32;
2785                            let y = obj.get("y").and_then(|v| v.as_i64()).unwrap_or(0) as i32;
2786                            let z = obj.get("z").and_then(|v| v.as_i64()).unwrap_or(0) as i32;
2787                            area.position = (x, y, z).into();
2788                            info!(target: "feagi-services", "[CONNECTOME-UPDATE] Updated position (object format): ({}, {}, {})", x, y, z);
2789                        }
2790                    }
2791                    "coordinate_2d" | "coordinates_2d" => {
2792                        if let Some(arr) = value.as_array() {
2793                            if arr.len() >= 2 {
2794                                let x = arr[0].as_i64().unwrap_or(0) as i32;
2795                                let y = arr[1].as_i64().unwrap_or(0) as i32;
2796                                area.properties
2797                                    .insert("coordinate_2d".to_string(), serde_json::json!([x, y]));
2798                                info!(target: "feagi-services", "[CONNECTOME-UPDATE] Updated coordinate_2d: ({}, {})", x, y);
2799                            } else {
2800                                warn!(target: "feagi-services", "[CONNECTOME-UPDATE] coordinate_2d array must have 2 elements, got {}", arr.len());
2801                            }
2802                        } else if let Some(obj) = value.as_object() {
2803                            let x = obj.get("x").and_then(|v| v.as_i64()).unwrap_or(0) as i32;
2804                            let y = obj.get("y").and_then(|v| v.as_i64()).unwrap_or(0) as i32;
2805                            area.properties
2806                                .insert("coordinate_2d".to_string(), serde_json::json!([x, y]));
2807                            info!(target: "feagi-services", "[CONNECTOME-UPDATE] Updated coordinate_2d (object format): ({}, {})", x, y);
2808                        } else {
2809                            warn!(target: "feagi-services", "[CONNECTOME-UPDATE] coordinate_2d must be array or object, got: {:?}", value);
2810                        }
2811                    }
2812                    "visualization_voxel_granularity" => {
2813                        // Only store if != 1x1x1 (default), delete if set to 1x1x1
2814                        info!(target: "feagi-services", "[CONNECTOME-UPDATE] Received visualization_voxel_granularity update: {:?}", value);
2815                        if let Some(arr) = value.as_array() {
2816                            if arr.len() == 3 {
2817                                // Try to parse as integers (u64) or floats (f64), then convert to u32
2818                                let x_opt = arr[0]
2819                                    .as_u64()
2820                                    .or_else(|| arr[0].as_f64().map(|f| f as u64));
2821                                let y_opt = arr[1]
2822                                    .as_u64()
2823                                    .or_else(|| arr[1].as_f64().map(|f| f as u64));
2824                                let z_opt = arr[2]
2825                                    .as_u64()
2826                                    .or_else(|| arr[2].as_f64().map(|f| f as u64));
2827
2828                                if let (Some(x), Some(y), Some(z)) = (x_opt, y_opt, z_opt) {
2829                                    let x_u32 = x as u32;
2830                                    let y_u32 = y as u32;
2831                                    let z_u32 = z as u32;
2832
2833                                    // Default is 1x1x1 - only store if different
2834                                    if x_u32 == 1 && y_u32 == 1 && z_u32 == 1 {
2835                                        // Remove override (return to default)
2836                                        area.properties.remove("visualization_voxel_granularity");
2837                                        info!(target: "feagi-services", "[CONNECTOME-UPDATE] Removed visualization_voxel_granularity override (returned to default 1x1x1)");
2838                                    } else {
2839                                        // Store override (non-default value) as integer array
2840                                        area.properties.insert(
2841                                            "visualization_voxel_granularity".to_string(),
2842                                            serde_json::json!([x_u32, y_u32, z_u32]),
2843                                        );
2844                                        info!(target: "feagi-services", "[CONNECTOME-UPDATE] Updated visualization_voxel_granularity: ({}, {}, {})", x_u32, y_u32, z_u32);
2845                                    }
2846                                } else {
2847                                    warn!(target: "feagi-services", "[CONNECTOME-UPDATE] Failed to parse visualization_voxel_granularity array values as integers");
2848                                }
2849                            } else {
2850                                warn!(target: "feagi-services", "[CONNECTOME-UPDATE] visualization_voxel_granularity array must have 3 elements, got {}", arr.len());
2851                            }
2852                        } else {
2853                            warn!(target: "feagi-services", "[CONNECTOME-UPDATE] visualization_voxel_granularity must be an array, got: {:?}", value);
2854                        }
2855                    }
2856                    _ => {}
2857                }
2858            }
2859            manager.refresh_cortical_area_hashes(properties_changed, geometry_changed);
2860        }
2861
2862        // Refresh burst runner cache so the NPU aggregation path immediately uses new granularity.
2863        if needs_burst_cache_refresh {
2864            self.refresh_burst_runner_cache();
2865        }
2866
2867        info!(target: "feagi-services", "[METADATA-UPDATE] Metadata update complete");
2868
2869        // Return updated info
2870        self.get_cortical_area_info(&effective_cortical_id_str)
2871            .await
2872    }
2873
2874    /// Structural rebuild: For dimension/density changes requiring synapse rebuild
2875    ///
2876    /// Performance: ~100-200ms (localized to one area, not full brain)
2877    ///
2878    /// CRITICAL: This requires:
2879    /// 1. Deleting all neurons in the area
2880    /// 2. Deleting all incoming/outgoing synapses (automatic via neuron deletion)
2881    /// 3. Recreating neurons with new dimensions/density
2882    /// 4. Rebuilding synapses via cortical mapping
2883    async fn update_with_localized_rebuild(
2884        &self,
2885        cortical_id: &str,
2886        changes: HashMap<String, Value>,
2887    ) -> ServiceResult<CorticalAreaInfo> {
2888        info!(target: "feagi-services", "[STRUCTURAL-REBUILD] Localized rebuild for {}", cortical_id);
2889
2890        // Validate cortical ID format
2891        let _cortical_id_typed = CorticalID::try_from_base_64(cortical_id)
2892            .map_err(|e| ServiceError::InvalidInput(format!("Invalid cortical ID: {}", e)))?;
2893
2894        if changes.contains_key("group_id") && changes.len() == 1 {
2895            let new_id =
2896                self.apply_unit_index_update(&_cortical_id_typed, cortical_id, &changes)?;
2897            self.regenerate_mappings_for_area(&new_id)?;
2898            return self.get_cortical_area_info(&new_id.as_base_64()).await;
2899        }
2900
2901        // Must run on blocking thread due to heavy ConnectomeManager operations
2902        let connectome = Arc::clone(&self.connectome);
2903        let genome_store = Arc::clone(&self.current_genome);
2904        let cortical_id_owned = cortical_id.to_string();
2905        let burst_runner_clone = self.burst_runner.clone();
2906
2907        tokio::task::spawn_blocking(move || {
2908            Self::do_localized_rebuild(
2909                &cortical_id_owned,
2910                changes,
2911                connectome,
2912                genome_store,
2913                burst_runner_clone,
2914            )
2915        })
2916        .await
2917        .map_err(|e| ServiceError::Backend(format!("Rebuild task panicked: {}", e)))?
2918    }
2919
2920    /// Perform localized rebuild (blocking operation)
2921    fn do_localized_rebuild(
2922        cortical_id: &str,
2923        changes: HashMap<String, Value>,
2924        connectome: Arc<RwLock<ConnectomeManager>>,
2925        genome_store: Arc<RwLock<Option<feagi_evolutionary::RuntimeGenome>>>,
2926        burst_runner: Option<Arc<RwLock<BurstLoopRunner>>>,
2927    ) -> ServiceResult<CorticalAreaInfo> {
2928        info!(
2929            "[STRUCTURAL-REBUILD] Starting localized rebuild for {}",
2930            cortical_id
2931        );
2932
2933        // Convert cortical_id to CorticalID
2934        let cortical_id_typed =
2935            feagi_evolutionary::string_to_cortical_id(cortical_id).map_err(|e| {
2936                ServiceError::InvalidInput(format!("Invalid cortical ID '{}': {}", cortical_id, e))
2937            })?;
2938
2939        // Step 1: Update RuntimeGenome dimensions/density
2940        let (old_dimensions, old_density, new_dimensions, new_density) = {
2941            let mut genome_guard = genome_store.write();
2942            let genome = genome_guard
2943                .as_mut()
2944                .ok_or_else(|| ServiceError::Backend("No genome loaded".to_string()))?;
2945
2946            let area = genome
2947                .cortical_areas
2948                .get_mut(&cortical_id_typed)
2949                .ok_or_else(|| ServiceError::NotFound {
2950                    resource: "CorticalArea".to_string(),
2951                    id: cortical_id.to_string(),
2952                })?;
2953
2954            let old_dims = area.dimensions;
2955            let old_dens = area.neurons_per_voxel();
2956
2957            // Apply dimensional changes
2958            // CRITICAL: For IPU/OPU areas, cortical_dimensions_per_device must be multiplied by dev_count
2959            let is_per_device = changes.contains_key("cortical_dimensions_per_device");
2960
2961            if let Some(dims) = changes
2962                .get("dimensions")
2963                .or_else(|| changes.get("cortical_dimensions"))
2964                .or_else(|| changes.get("cortical_dimensions_per_device"))
2965            {
2966                let (width, height, depth) = if let Some(arr) = dims.as_array() {
2967                    // Handle array format: [width, height, depth]
2968                    if arr.len() >= 3 {
2969                        (
2970                            arr[0].as_u64().unwrap_or(1) as usize,
2971                            arr[1].as_u64().unwrap_or(1) as usize,
2972                            arr[2].as_u64().unwrap_or(1) as usize,
2973                        )
2974                    } else {
2975                        (1, 1, 1)
2976                    }
2977                } else if let Some(obj) = dims.as_object() {
2978                    // Handle object format: {"x": width, "y": height, "z": depth}
2979                    (
2980                        obj.get("x").and_then(|v| v.as_u64()).unwrap_or(1) as usize,
2981                        obj.get("y").and_then(|v| v.as_u64()).unwrap_or(1) as usize,
2982                        obj.get("z").and_then(|v| v.as_u64()).unwrap_or(1) as usize,
2983                    )
2984                } else {
2985                    (1, 1, 1)
2986                };
2987
2988                // If this is per-device dimensions, multiply depth by dev_count to get total dimensions
2989                let final_depth = if is_per_device {
2990                    // Get dev_count from changes or from existing area properties
2991                    let dev_count = changes
2992                        .get("dev_count")
2993                        .and_then(|v| v.as_u64())
2994                        .or_else(|| area.properties.get("dev_count").and_then(|v| v.as_u64()))
2995                        .unwrap_or(1) as usize;
2996
2997                    info!("[STRUCTURAL-REBUILD] Per-device dimensions: [{}, {}, {}] × dev_count={} → total depth={}",
2998                          width, height, depth, dev_count, depth * dev_count);
2999
3000                    depth * dev_count
3001                } else {
3002                    depth
3003                };
3004
3005                area.dimensions =
3006                    CorticalAreaDimensions::new(width as u32, height as u32, final_depth as u32)?;
3007            }
3008
3009            // Apply density changes
3010            // Update neurons_per_voxel from any of the legacy parameter names
3011            for density_param in [
3012                "neurons_per_voxel",
3013                "per_voxel_neuron_cnt",
3014                "neuron_density",
3015            ] {
3016                if let Some(density) = changes.get(density_param).and_then(|v| v.as_u64()) {
3017                    area.add_property_mut(
3018                        "neurons_per_voxel".to_string(),
3019                        serde_json::json!(density as u32),
3020                    );
3021                    break;
3022                }
3023            }
3024
3025            // Store dev_count and cortical_dimensions_per_device in properties for IPU/OPU areas
3026            if let Some(dev_count) = changes.get("dev_count") {
3027                area.properties
3028                    .insert("dev_count".to_string(), dev_count.clone());
3029            }
3030            if let Some(per_device_dims) = changes.get("cortical_dimensions_per_device") {
3031                area.properties.insert(
3032                    "cortical_dimensions_per_device".to_string(),
3033                    per_device_dims.clone(),
3034                );
3035            }
3036
3037            // Update spatial gradient increment properties if changed
3038            // These require rebuild because thresholds must be recalculated based on position
3039            // Handle neuron_fire_threshold_increment as ARRAY [x, y, z] from BV
3040            if let Some(value) = changes.get("neuron_fire_threshold_increment") {
3041                if let Some(arr) = value.as_array() {
3042                    // Parse array [x, y, z] into separate properties
3043                    if arr.len() >= 3 {
3044                        let x = arr[0].as_f64().unwrap_or(0.0) as f32;
3045                        let y = arr[1].as_f64().unwrap_or(0.0) as f32;
3046                        let z = arr[2].as_f64().unwrap_or(0.0) as f32;
3047
3048                        area.properties.insert(
3049                            "firing_threshold_increment_x".to_string(),
3050                            serde_json::json!(x),
3051                        );
3052                        area.properties.insert(
3053                            "firing_threshold_increment_y".to_string(),
3054                            serde_json::json!(y),
3055                        );
3056                        area.properties.insert(
3057                            "firing_threshold_increment_z".to_string(),
3058                            serde_json::json!(z),
3059                        );
3060
3061                        info!(
3062                            "[STRUCTURAL-REBUILD] Updated firing_threshold_increment to [{}, {}, {}] for area {}",
3063                            x, y, z, cortical_id
3064                        );
3065                    }
3066                }
3067            }
3068
3069            // Handle individual x/y/z properties if sent separately
3070            for increment_param in [
3071                "firing_threshold_increment_x",
3072                "firing_threshold_increment_y",
3073                "firing_threshold_increment_z",
3074            ] {
3075                if let Some(value) = changes.get(increment_param) {
3076                    area.properties
3077                        .insert(increment_param.to_string(), value.clone());
3078                    info!(
3079                        "[STRUCTURAL-REBUILD] Updated {} to {} for area {}",
3080                        increment_param, value, cortical_id
3081                    );
3082                }
3083            }
3084
3085            // Update leak_variability if changed (also requires rebuild)
3086            for param in ["leak_variability", "neuron_leak_variability"] {
3087                if let Some(value) = changes.get(param) {
3088                    area.properties
3089                        .insert("leak_variability".to_string(), value.clone());
3090                    info!(
3091                        "[STRUCTURAL-REBUILD] Updated leak_variability to {} for area {}",
3092                        value, cortical_id
3093                    );
3094                    break;
3095                }
3096            }
3097
3098            (
3099                old_dims,
3100                old_dens,
3101                area.dimensions,
3102                area.neurons_per_voxel(),
3103            )
3104        };
3105
3106        let total_voxels = new_dimensions.width as usize
3107            * new_dimensions.height as usize
3108            * new_dimensions.depth as usize;
3109        let estimated_neurons = total_voxels * new_density as usize;
3110
3111        info!(
3112            "[STRUCTURAL-REBUILD] Dimension: {:?} -> {:?}",
3113            old_dimensions, new_dimensions
3114        );
3115        info!(
3116            "[STRUCTURAL-REBUILD] Density: {} -> {} neurons/voxel",
3117            old_density, new_density
3118        );
3119
3120        if estimated_neurons > 1_000_000 {
3121            warn!(
3122                "[STRUCTURAL-REBUILD] ⚠️ Large area resize: {} neurons estimated. This may take significant time and memory.",
3123                estimated_neurons
3124            );
3125        }
3126
3127        // Step 2: Delete all neurons in the cortical area
3128        let neurons_to_delete = {
3129            let manager = connectome.read();
3130            manager.get_neurons_in_area(&cortical_id_typed)
3131        };
3132
3133        let deleted_count = if !neurons_to_delete.is_empty() {
3134            info!(
3135                "[STRUCTURAL-REBUILD] Deleting {} existing neurons",
3136                neurons_to_delete.len()
3137            );
3138            let mut manager = connectome.write();
3139            manager
3140                .delete_neurons_batch(neurons_to_delete)
3141                .map_err(|e| ServiceError::Backend(format!("Failed to delete neurons: {}", e)))?
3142        } else {
3143            0
3144        };
3145
3146        info!("[STRUCTURAL-REBUILD] Deleted {} neurons", deleted_count);
3147
3148        // Step 3: Update cortical area dimensions and properties in ConnectomeManager
3149        {
3150            let mut manager = connectome.write();
3151            manager
3152                .resize_cortical_area(&cortical_id_typed, new_dimensions)
3153                .map_err(|e| ServiceError::Backend(format!("Failed to resize area: {}", e)))?;
3154
3155            // Update neurons_per_voxel and properties
3156            if let Some(area) = manager.get_cortical_area_mut(&cortical_id_typed) {
3157                area.add_property_mut(
3158                    "neurons_per_voxel".to_string(),
3159                    serde_json::json!(new_density),
3160                );
3161
3162                // Store IPU/OPU properties
3163                if let Some(dev_count) = changes.get("dev_count") {
3164                    area.properties
3165                        .insert("dev_count".to_string(), dev_count.clone());
3166                }
3167                if let Some(per_device_dims) = changes.get("cortical_dimensions_per_device") {
3168                    area.properties.insert(
3169                        "cortical_dimensions_per_device".to_string(),
3170                        per_device_dims.clone(),
3171                    );
3172                }
3173
3174                // ✅ Sync spatial gradient increment properties to ConnectomeManager
3175                // This ensures BV reads back the updated values
3176                // Handle neuron_fire_threshold_increment as ARRAY [x, y, z] from BV
3177                if let Some(value) = changes.get("neuron_fire_threshold_increment") {
3178                    if let Some(arr) = value.as_array() {
3179                        if arr.len() >= 3 {
3180                            let x = arr[0].as_f64().unwrap_or(0.0) as f32;
3181                            let y = arr[1].as_f64().unwrap_or(0.0) as f32;
3182                            let z = arr[2].as_f64().unwrap_or(0.0) as f32;
3183
3184                            area.properties.insert(
3185                                "firing_threshold_increment_x".to_string(),
3186                                serde_json::json!(x),
3187                            );
3188                            area.properties.insert(
3189                                "firing_threshold_increment_y".to_string(),
3190                                serde_json::json!(y),
3191                            );
3192                            area.properties.insert(
3193                                "firing_threshold_increment_z".to_string(),
3194                                serde_json::json!(z),
3195                            );
3196                        }
3197                    }
3198                }
3199
3200                // Handle individual x/y/z properties if sent separately
3201                for increment_param in [
3202                    "firing_threshold_increment_x",
3203                    "firing_threshold_increment_y",
3204                    "firing_threshold_increment_z",
3205                ] {
3206                    if let Some(value) = changes.get(increment_param) {
3207                        area.properties
3208                            .insert(increment_param.to_string(), value.clone());
3209                    }
3210                }
3211
3212                // ✅ Sync leak_variability to ConnectomeManager
3213                for param in ["leak_variability", "neuron_leak_variability"] {
3214                    if let Some(value) = changes.get(param) {
3215                        area.properties
3216                            .insert("leak_variability".to_string(), value.clone());
3217                        break;
3218                    }
3219                }
3220            }
3221            manager.refresh_cortical_area_hashes(true, true);
3222        }
3223
3224        // Step 4: Recreate neurons with new dimensions/density
3225        // CRITICAL PERFORMANCE FIX: Extract data from connectome, release lock, then create neurons
3226        // This prevents blocking API requests during the multi-second neuron creation process
3227        let (cortical_idx, area_data) = {
3228            let manager = connectome.read();
3229            let area = manager
3230                .get_cortical_area(&cortical_id_typed)
3231                .ok_or_else(|| ServiceError::NotFound {
3232                    resource: "CorticalArea".to_string(),
3233                    id: cortical_id.to_string(),
3234                })?;
3235            let cortical_idx = manager
3236                .get_cortical_idx(&cortical_id_typed)
3237                .ok_or_else(|| ServiceError::Backend("Cortical index not found".to_string()))?;
3238
3239            // Extract all data needed for neuron creation
3240            use feagi_brain_development::models::CorticalAreaExt;
3241            (
3242                cortical_idx,
3243                (
3244                    area.dimensions,
3245                    area.neurons_per_voxel(),
3246                    area.firing_threshold(),
3247                    area.firing_threshold_increment_x(),
3248                    area.firing_threshold_increment_y(),
3249                    area.firing_threshold_increment_z(),
3250                    area.firing_threshold_limit(),
3251                    area.leak_coefficient(),
3252                    area.neuron_excitability(),
3253                    area.refractory_period(),
3254                    area.consecutive_fire_count() as u16,
3255                    area.snooze_period(),
3256                    area.mp_charge_accumulation(),
3257                ),
3258            )
3259        };
3260
3261        // Release connectome lock before creating neurons (NPU lock will be held, but connectome is free for API)
3262        let npu_arc_for_creation = {
3263            let manager = connectome.read();
3264            manager
3265                .get_npu()
3266                .ok_or_else(|| ServiceError::Backend("NPU not connected".to_string()))?
3267                .clone()
3268        };
3269
3270        // CRITICAL PERFORMANCE: Event-based lock management
3271        // Lock is held until create_cortical_area_neurons() completes (100% done)
3272        // Timing varies by hardware/topology - we measure actual time, not estimate
3273        // NOTE: Connectome lock is already released, so API can query connectome data
3274        let total_neurons =
3275            area_data.0.width * area_data.0.height * area_data.0.depth * area_data.1;
3276
3277        if total_neurons > 1_000_000 {
3278            info!(
3279                "[STRUCTURAL-REBUILD] Creating large area ({} neurons) - NPU lock held until creation completes",
3280                total_neurons
3281            );
3282        }
3283
3284        let creation_start = std::time::Instant::now();
3285
3286        // CRITICAL PERFORMANCE: For very large areas (>1M neurons), batch by depth layers AND within layers
3287        // to release NPU lock frequently and allow burst loop to run
3288        let neurons_created = if total_neurons > 1_000_000 {
3289            // Batch by depth layers (z-dimension) - each layer is processed separately
3290            // Additionally, batch within each layer if layer is large (>100k neurons)
3291            let mut total_created = 0u32;
3292            let depth = area_data.0.depth;
3293            let width = area_data.0.width;
3294            let height = area_data.0.height;
3295            let neurons_per_voxel = area_data.1;
3296            let neurons_per_layer = (width * height * neurons_per_voxel) as usize;
3297            const BATCH_SIZE: usize = 10_000; // Process neurons in batches of 10k within each layer (reduces lock hold time)
3298
3299            // Determine if we need to batch within layers
3300            let needs_inner_batching = neurons_per_layer > 50_000; // Lower threshold for inner batching
3301
3302            info!(
3303                "[STRUCTURAL-REBUILD] Batching neuron creation: {} layers × {} neurons/layer = {} total{}",
3304                depth, neurons_per_layer, total_neurons,
3305                if needs_inner_batching { " (with inner-layer batching)" } else { "" }
3306            );
3307
3308            for z_layer in 0..depth {
3309                if needs_inner_batching {
3310                    // Large layer: batch within the layer by processing rows in chunks
3311                    let mut layer_created = 0u32;
3312                    let neurons_per_row = (width * neurons_per_voxel) as usize;
3313                    let rows_per_batch = (BATCH_SIZE / neurons_per_row).max(1);
3314                    let total_row_batches = (height as usize).div_ceil(rows_per_batch);
3315
3316                    if z_layer == 0 {
3317                        info!(
3318                            "[STRUCTURAL-REBUILD] Inner-layer batching: {} rows/layer, {} rows/batch, ~{} batches/layer",
3319                            height, rows_per_batch, total_row_batches
3320                        );
3321                    }
3322
3323                    for (batch_idx, row_start) in (0..height).step_by(rows_per_batch).enumerate() {
3324                        let row_end = (row_start + rows_per_batch as u32).min(height);
3325                        let rows_in_batch = row_end - row_start;
3326                        let neurons_in_batch = (rows_in_batch * width * neurons_per_voxel) as usize;
3327
3328                        let batch_start = std::time::Instant::now();
3329
3330                        // Acquire lock, create batch of rows, release lock
3331                        let batch_created = {
3332                            let mut npu_lock = npu_arc_for_creation.lock().map_err(|e| {
3333                                ServiceError::Backend(format!("Failed to lock NPU: {}", e))
3334                            })?;
3335
3336                            // Create neurons for this batch of rows (height=rows_in_batch, y_offset=row_start, z_offset=z_layer)
3337                            npu_lock
3338                                .create_cortical_area_neurons_with_offsets(
3339                                    cortical_idx,
3340                                    width,
3341                                    rows_in_batch,
3342                                    1, // depth=1 (single z-layer)
3343                                    neurons_per_voxel,
3344                                    area_data.2,  // default_threshold
3345                                    area_data.3,  // threshold_increment_x
3346                                    area_data.4,  // threshold_increment_y
3347                                    area_data.5,  // threshold_increment_z
3348                                    area_data.6,  // default_threshold_limit
3349                                    area_data.7,  // default_leak_coefficient
3350                                    0.0,          // default_resting_potential
3351                                    0,            // default_neuron_type
3352                                    area_data.9,  // default_refractory_period
3353                                    area_data.8,  // default_excitability
3354                                    area_data.10, // default_consecutive_fire_limit
3355                                    area_data.11, // default_snooze_period
3356                                    area_data.12, // default_mp_charge_accumulation
3357                                    row_start, // y_offset: create neurons starting at this y-coordinate
3358                                    z_layer,   // z_offset: create neurons at this z-coordinate
3359                                )
3360                                .map_err(|e| {
3361                                    ServiceError::Backend(format!(
3362                                        "NPU neuron creation failed for layer {} rows {}-{}: {}",
3363                                        z_layer, row_start, row_end, e
3364                                    ))
3365                                })?
3366                        };
3367
3368                        let batch_duration = batch_start.elapsed();
3369                        layer_created += batch_created;
3370
3371                        // Log if batch took too long (helps identify if batching is working)
3372                        if batch_duration.as_millis() > 100
3373                            && (batch_idx == 0 || batch_idx == total_row_batches - 1)
3374                        {
3375                            tracing::debug!(
3376                                "[STRUCTURAL-REBUILD] Layer {} batch {}/{}: {} neurons in {:.1}ms (rows {}-{})",
3377                                z_layer, batch_idx + 1, total_row_batches, neurons_in_batch,
3378                                batch_duration.as_millis(), row_start, row_end
3379                            );
3380                        }
3381                    }
3382
3383                    total_created += layer_created;
3384                } else {
3385                    // Small layer: create entire layer in one batch
3386                    let layer_created = {
3387                        let mut npu_lock = npu_arc_for_creation.lock().map_err(|e| {
3388                            ServiceError::Backend(format!("Failed to lock NPU: {}", e))
3389                        })?;
3390
3391                        // Create neurons for this z-layer only (depth=1, z_offset=z_layer)
3392                        npu_lock
3393                            .create_cortical_area_neurons_with_z_offset(
3394                                cortical_idx,
3395                                width,
3396                                height,
3397                                1, // depth=1 (single layer)
3398                                neurons_per_voxel,
3399                                area_data.2,  // default_threshold
3400                                area_data.3,  // threshold_increment_x
3401                                area_data.4,  // threshold_increment_y
3402                                area_data.5,  // threshold_increment_z
3403                                area_data.6,  // default_threshold_limit
3404                                area_data.7,  // default_leak_coefficient
3405                                0.0,          // default_resting_potential
3406                                0,            // default_neuron_type
3407                                area_data.9,  // default_refractory_period
3408                                area_data.8,  // default_excitability
3409                                area_data.10, // default_consecutive_fire_limit
3410                                area_data.11, // default_snooze_period
3411                                area_data.12, // default_mp_charge_accumulation
3412                                z_layer,      // z_offset: create neurons at this z-coordinate
3413                            )
3414                            .map_err(|e| {
3415                                ServiceError::Backend(format!(
3416                                    "NPU neuron creation failed for layer {}: {}",
3417                                    z_layer, e
3418                                ))
3419                            })?
3420                    };
3421
3422                    total_created += layer_created;
3423                }
3424
3425                // Log progress every 5 layers or on last layer
3426                if (z_layer + 1) % 5 == 0 || z_layer == depth - 1 {
3427                    let progress = ((z_layer + 1) as f32 / depth as f32) * 100.0;
3428                    info!(
3429                        "[STRUCTURAL-REBUILD] Progress: {}/{} layers ({}%) - {} neurons created",
3430                        z_layer + 1,
3431                        depth,
3432                        progress as u32,
3433                        total_created
3434                    );
3435                }
3436            }
3437
3438            total_created
3439        } else {
3440            // Small area: create all neurons in one batch (single lock acquisition)
3441            let mut npu_lock = npu_arc_for_creation
3442                .lock()
3443                .map_err(|e| ServiceError::Backend(format!("Failed to lock NPU: {}", e)))?;
3444
3445            npu_lock
3446                .create_cortical_area_neurons(
3447                    cortical_idx,
3448                    area_data.0.width,
3449                    area_data.0.height,
3450                    area_data.0.depth,
3451                    area_data.1,
3452                    area_data.2,
3453                    area_data.3,
3454                    area_data.4,
3455                    area_data.5,
3456                    area_data.6,
3457                    area_data.7,
3458                    0.0,
3459                    0,
3460                    area_data.9,
3461                    area_data.8,
3462                    area_data.10,
3463                    area_data.11,
3464                    area_data.12,
3465                )
3466                .map_err(|e| ServiceError::Backend(format!("NPU neuron creation failed: {}", e)))?
3467        };
3468
3469        let creation_duration = creation_start.elapsed();
3470        info!(
3471            "[STRUCTURAL-REBUILD] Created {} neurons in {:.2}s (NPU lock held until completion)",
3472            neurons_created,
3473            creation_duration.as_secs_f64()
3474        );
3475
3476        if creation_duration.as_secs() > 1 {
3477            warn!(
3478                "[STRUCTURAL-REBUILD] ⚠️ Long creation time: {:.2}s - burst loop was blocked during this period",
3479                creation_duration.as_secs_f64()
3480            );
3481        }
3482
3483        // Step 5: Rebuild outgoing synapses (this area -> others)
3484        let outgoing_targets = {
3485            let genome_guard = genome_store.read();
3486            let genome = genome_guard
3487                .as_ref()
3488                .ok_or_else(|| ServiceError::Backend("No genome loaded".to_string()))?;
3489            let area = genome
3490                .cortical_areas
3491                .get(&cortical_id_typed)
3492                .ok_or_else(|| ServiceError::NotFound {
3493                    resource: "CorticalArea".to_string(),
3494                    id: cortical_id.to_string(),
3495                })?;
3496            let mut targets = Vec::new();
3497            if let Some(mapping) = area
3498                .properties
3499                .get("cortical_mapping_dst")
3500                .and_then(|v| v.as_object())
3501            {
3502                for key in mapping.keys() {
3503                    match CorticalID::try_from_base_64(key) {
3504                        Ok(dst_id) => targets.push(dst_id),
3505                        Err(err) => warn!(
3506                            target: "feagi-services",
3507                            "Invalid cortical_mapping_dst ID '{}': {}",
3508                            key,
3509                            err
3510                        ),
3511                    }
3512                }
3513            }
3514            targets
3515        };
3516        let outgoing_synapses = {
3517            let mut manager = connectome.write();
3518            let mut total = 0u32;
3519            for dst_id in outgoing_targets {
3520                if dst_id == cortical_id_typed {
3521                    continue;
3522                }
3523                let count = manager
3524                    .regenerate_synapses_for_mapping(&cortical_id_typed, &dst_id)
3525                    .map_err(|e| {
3526                        ServiceError::Backend(format!(
3527                            "Failed to rebuild outgoing synapses {} -> {}: {}",
3528                            cortical_id,
3529                            dst_id.as_base_64(),
3530                            e
3531                        ))
3532                    })?;
3533                total = total.saturating_add(count as u32);
3534            }
3535            total
3536        };
3537
3538        info!(
3539            "[STRUCTURAL-REBUILD] Rebuilt {} outgoing synapses",
3540            outgoing_synapses
3541        );
3542
3543        // Step 6: Rebuild incoming synapses (others -> this area)
3544        let incoming_synapses = {
3545            let genome_guard = genome_store.read();
3546            let genome = genome_guard.as_ref().unwrap();
3547
3548            let mut total = 0u32;
3549            for (src_id, src_area) in &genome.cortical_areas {
3550                if src_id == &cortical_id_typed {
3551                    continue; // Skip self (already handled in outgoing)
3552                }
3553
3554                // Check if this area maps to our target area
3555                if let Some(dstmap) = src_area.properties.get("cortical_mapping_dst") {
3556                    if let Some(obj) = dstmap.as_object() {
3557                        if obj.contains_key(cortical_id) {
3558                            // This area has mappings to our target - rebuild them
3559                            let mut manager = connectome.write();
3560                            let count = manager
3561                                .regenerate_synapses_for_mapping(src_id, &cortical_id_typed)
3562                                .map_err(|e| {
3563                                    ServiceError::Backend(format!(
3564                                        "Failed to rebuild incoming synapses from {}: {}",
3565                                        src_id, e
3566                                    ))
3567                                })?;
3568                            total = total.saturating_add(count as u32);
3569                            info!(
3570                                "[STRUCTURAL-REBUILD] Rebuilt {} incoming synapses from {}",
3571                                count, src_id
3572                            );
3573                        }
3574                    }
3575                }
3576            }
3577
3578            total
3579        };
3580
3581        info!(
3582            "[STRUCTURAL-REBUILD] Rebuilt {} total incoming synapses",
3583            incoming_synapses
3584        );
3585
3586        // Step 7: Rebuild synapse index to ensure all new synapses are visible to propagation engine
3587        // This is critical for large areas (e.g., 2M+ neurons) to prevent system hangs
3588        // IMPORTANT: Release connectome lock before acquiring NPU lock to avoid blocking other operations
3589        let npu_arc = {
3590            let manager = connectome.read();
3591            manager
3592                .get_npu()
3593                .ok_or_else(|| ServiceError::Backend("NPU not connected".to_string()))?
3594                .clone()
3595        };
3596
3597        // CRITICAL PERFORMANCE: Release lock between operations to allow burst loop to run
3598        // Event-based: Lock held only until each operation completes, then released
3599
3600        // CRITICAL PERFORMANCE: Event-based lock management - release lock after each operation completes
3601        // This allows burst loop to run between operations, keeping system responsive
3602
3603        info!(
3604            "[STRUCTURAL-REBUILD] Rebuilding synapse index for {} neurons...",
3605            neurons_created
3606        );
3607        let index_rebuild_start = std::time::Instant::now();
3608        {
3609            let mut npu_lock = npu_arc
3610                .lock()
3611                .map_err(|e| ServiceError::Backend(format!("Failed to lock NPU: {}", e)))?;
3612
3613            npu_lock.rebuild_synapse_index();
3614            // Lock released here when scope ends (event-based: operation 100% complete)
3615        }
3616        let index_rebuild_duration = index_rebuild_start.elapsed();
3617        info!(
3618            "[STRUCTURAL-REBUILD] Synapse index rebuild complete in {:.2}s (NPU lock released)",
3619            index_rebuild_duration.as_secs_f64()
3620        );
3621        if index_rebuild_duration.as_millis() > 100 {
3622            warn!(
3623                "[STRUCTURAL-REBUILD] ⚠️ Slow synapse index rebuild: {:.2}s",
3624                index_rebuild_duration.as_secs_f64()
3625            );
3626        }
3627
3628        // No power neuron cache rebuild needed - power neuron is always neuron ID 1 (deterministic)
3629        // Direct O(1) access in phase1_injection_with_synapses, no cache required!
3630
3631        info!(
3632            "[STRUCTURAL-REBUILD] ✅ Complete: {} neurons, {} outgoing, {} incoming synapses",
3633            neurons_created, outgoing_synapses, incoming_synapses
3634        );
3635
3636        // Refresh burst runner cache after structural rebuild (areas may have been resized)
3637        if let Some(ref burst_runner) = burst_runner {
3638            let manager = connectome.read();
3639            let mappings = manager.get_all_cortical_idx_to_id_mappings();
3640            let mapping_count = mappings.len();
3641            burst_runner.write().refresh_cortical_id_mappings(mappings);
3642            info!(target: "feagi-services", "Refreshed burst runner cache with {} cortical areas", mapping_count);
3643        }
3644
3645        // CRITICAL PERFORMANCE: For large areas, skip expensive get_synapse_count_in_area
3646        // which iterates through all neurons (5.7M!) holding the NPU lock for hundreds of ms
3647        // Use known synapse counts from rebuild instead (we just calculated them)
3648        if neurons_created > 100_000 {
3649            info!(
3650                "[STRUCTURAL-REBUILD] Using known counts for large area ({} neurons) to avoid expensive NPU lock",
3651                neurons_created
3652            );
3653            // Get area info but use known counts (avoid expensive NPU iteration)
3654            let manager = connectome.read();
3655            let area = manager
3656                .get_cortical_area(&cortical_id_typed)
3657                .ok_or_else(|| ServiceError::NotFound {
3658                    resource: "CorticalArea".to_string(),
3659                    id: cortical_id.to_string(),
3660                })?;
3661            let cortical_idx = manager
3662                .get_cortical_idx(&cortical_id_typed)
3663                .ok_or_else(|| ServiceError::NotFound {
3664                    resource: "CorticalArea".to_string(),
3665                    id: cortical_id.to_string(),
3666                })?;
3667
3668            // Use known counts from rebuild (no expensive NPU lock needed)
3669            let neuron_count = neurons_created as usize;
3670            let synapse_count = (outgoing_synapses + incoming_synapses) as usize;
3671            let incoming_synapse_count = incoming_synapses as usize;
3672            let outgoing_synapse_count = outgoing_synapses as usize;
3673            let cortical_group = area.get_cortical_group();
3674            let cortical_bytes = cortical_id_typed.as_bytes();
3675            let is_io_area = cortical_bytes[0] == b'i' || cortical_bytes[0] == b'o';
3676            let io_flag = if is_io_area {
3677                cortical_id_typed.extract_io_data_flag().ok()
3678            } else {
3679                None
3680            };
3681            let cortical_subtype = if is_io_area {
3682                String::from_utf8(cortical_bytes[0..4].to_vec()).ok()
3683            } else {
3684                None
3685            };
3686            let unit_id = if is_io_area {
3687                Some(cortical_bytes[6])
3688            } else {
3689                None
3690            };
3691            let group_id = if is_io_area {
3692                Some(cortical_bytes[7])
3693            } else {
3694                None
3695            };
3696            let coding_signage = io_flag
3697                .as_ref()
3698                .map(|flag| signage_label_from_flag(flag).to_string());
3699            let coding_behavior = io_flag
3700                .as_ref()
3701                .map(|flag| behavior_label_from_flag(flag).to_string());
3702            let coding_type = io_flag
3703                .as_ref()
3704                .map(|flag| coding_type_label_from_flag(flag).to_string());
3705            let coding_options = if is_io_area {
3706                io_coding_options_for_unit(&cortical_id_typed)
3707            } else {
3708                None
3709            };
3710            let encoding_type = coding_behavior.clone();
3711            let encoding_format = coding_type.clone();
3712
3713            // Build full response using area properties (same as get_cortical_area_info_blocking)
3714            Ok(CorticalAreaInfo {
3715                cortical_id: area.cortical_id.as_base_64(),
3716                cortical_id_s: area.cortical_id.to_string(),
3717                cortical_idx,
3718                name: area.name.clone(),
3719                dimensions: (
3720                    area.dimensions.width as usize,
3721                    area.dimensions.height as usize,
3722                    area.dimensions.depth as usize,
3723                ),
3724                position: area.position.into(),
3725                area_type: cortical_group
3726                    .clone()
3727                    .unwrap_or_else(|| "CUSTOM".to_string()),
3728                cortical_group: cortical_group
3729                    .clone()
3730                    .unwrap_or_else(|| "CUSTOM".to_string()),
3731                cortical_type: {
3732                    use feagi_evolutionary::extract_memory_properties;
3733                    if extract_memory_properties(&area.properties).is_some() {
3734                        "memory".to_string()
3735                    } else if let Some(group) = &cortical_group {
3736                        match group.as_str() {
3737                            "IPU" => "sensory".to_string(),
3738                            "OPU" => "motor".to_string(),
3739                            "CORE" => "core".to_string(),
3740                            _ => "custom".to_string(),
3741                        }
3742                    } else {
3743                        "custom".to_string()
3744                    }
3745                },
3746                neuron_count,
3747                synapse_count,
3748                incoming_synapse_count,
3749                outgoing_synapse_count,
3750                visible: area.visible(),
3751                sub_group: area.sub_group(),
3752                neurons_per_voxel: area.neurons_per_voxel(),
3753                postsynaptic_current: area.postsynaptic_current() as f64,
3754                postsynaptic_current_max: area.postsynaptic_current_max() as f64,
3755                plasticity_constant: area.plasticity_constant() as f64,
3756                degeneration: area.degeneration() as f64,
3757                psp_uniform_distribution: area.psp_uniform_distribution(),
3758                mp_driven_psp: area.mp_driven_psp(),
3759                firing_threshold: area.firing_threshold() as f64,
3760                firing_threshold_increment: [
3761                    area.firing_threshold_increment_x() as f64,
3762                    area.firing_threshold_increment_y() as f64,
3763                    area.firing_threshold_increment_z() as f64,
3764                ],
3765                firing_threshold_limit: area.firing_threshold_limit() as f64,
3766                consecutive_fire_count: area.consecutive_fire_count(),
3767                snooze_period: area.snooze_period() as u32,
3768                refractory_period: area.refractory_period() as u32,
3769                leak_coefficient: area.leak_coefficient() as f64,
3770                leak_variability: area.leak_variability() as f64,
3771                mp_charge_accumulation: area.mp_charge_accumulation(),
3772                neuron_excitability: area.neuron_excitability() as f64,
3773                burst_engine_active: area.burst_engine_active(),
3774                init_lifespan: area.init_lifespan(),
3775                lifespan_growth_rate: area.lifespan_growth_rate() as f64,
3776                longterm_mem_threshold: area.longterm_mem_threshold(),
3777                temporal_depth: {
3778                    use feagi_evolutionary::extract_memory_properties;
3779                    extract_memory_properties(&area.properties).map(|p| p.temporal_depth.max(1))
3780                },
3781                properties: HashMap::new(),
3782                cortical_subtype,
3783                encoding_type,
3784                encoding_format,
3785                unit_id,
3786                group_id,
3787                coding_signage,
3788                coding_behavior,
3789                coding_type,
3790                coding_options,
3791                parent_region_id: manager.get_parent_region_id_for_area(&cortical_id_typed),
3792                dev_count: area
3793                    .properties
3794                    .get("dev_count")
3795                    .and_then(|v| v.as_u64().map(|n| n as usize)),
3796                cortical_dimensions_per_device: area
3797                    .properties
3798                    .get("cortical_dimensions_per_device")
3799                    .and_then(|v| v.as_array())
3800                    .and_then(|arr| {
3801                        if arr.len() == 3 {
3802                            Some((
3803                                arr[0].as_u64()? as usize,
3804                                arr[1].as_u64()? as usize,
3805                                arr[2].as_u64()? as usize,
3806                            ))
3807                        } else {
3808                            None
3809                        }
3810                    }),
3811                visualization_voxel_granularity: area
3812                    .properties
3813                    .get("visualization_voxel_granularity")
3814                    .and_then(|v| v.as_array())
3815                    .and_then(|arr| {
3816                        if arr.len() == 3 {
3817                            Some((
3818                                arr[0].as_u64()? as u32,
3819                                arr[1].as_u64()? as u32,
3820                                arr[2].as_u64()? as u32,
3821                            ))
3822                        } else {
3823                            None
3824                        }
3825                    }),
3826            })
3827        } else {
3828            // For smaller areas, use the full info retrieval
3829            Self::get_cortical_area_info_blocking(cortical_id, &connectome)
3830        }
3831    }
3832
3833    /// Helper to get cortical area info (blocking version for spawn_blocking contexts)
3834    fn get_cortical_area_info_blocking(
3835        cortical_id: &str,
3836        connectome: &Arc<RwLock<ConnectomeManager>>,
3837    ) -> ServiceResult<CorticalAreaInfo> {
3838        // Convert String to CorticalID
3839        let cortical_id_typed = CorticalID::try_from_base_64(cortical_id)
3840            .map_err(|e| ServiceError::InvalidInput(format!("Invalid cortical ID: {}", e)))?;
3841
3842        let manager = connectome.read();
3843
3844        let area = manager
3845            .get_cortical_area(&cortical_id_typed)
3846            .ok_or_else(|| ServiceError::NotFound {
3847                resource: "CorticalArea".to_string(),
3848                id: cortical_id.to_string(),
3849            })?;
3850
3851        let cortical_idx = manager
3852            .get_cortical_idx(&cortical_id_typed)
3853            .ok_or_else(|| ServiceError::NotFound {
3854                resource: "CorticalArea".to_string(),
3855                id: cortical_id.to_string(),
3856            })?;
3857
3858        let neuron_count = manager.get_neuron_count_in_area(&cortical_id_typed);
3859        let outgoing_synapse_count = manager.get_outgoing_synapse_count_in_area(&cortical_id_typed);
3860        let incoming_synapse_count = manager.get_incoming_synapse_count_in_area(&cortical_id_typed);
3861        let synapse_count = outgoing_synapse_count;
3862
3863        let cortical_group = area.get_cortical_group();
3864        let cortical_bytes = cortical_id_typed.as_bytes();
3865        let is_io_area = cortical_bytes[0] == b'i' || cortical_bytes[0] == b'o';
3866        let io_flag = if is_io_area {
3867            cortical_id_typed.extract_io_data_flag().ok()
3868        } else {
3869            None
3870        };
3871        let cortical_subtype = if is_io_area {
3872            String::from_utf8(cortical_bytes[0..4].to_vec()).ok()
3873        } else {
3874            None
3875        };
3876        let unit_id = if is_io_area {
3877            Some(cortical_bytes[6])
3878        } else {
3879            None
3880        };
3881        let group_id = if is_io_area {
3882            Some(cortical_bytes[7])
3883        } else {
3884            None
3885        };
3886        let coding_signage = io_flag
3887            .as_ref()
3888            .map(|flag| signage_label_from_flag(flag).to_string());
3889        let coding_behavior = io_flag
3890            .as_ref()
3891            .map(|flag| behavior_label_from_flag(flag).to_string());
3892        let coding_type = io_flag
3893            .as_ref()
3894            .map(|flag| coding_type_label_from_flag(flag).to_string());
3895        let coding_options = if is_io_area {
3896            io_coding_options_for_unit(&cortical_id_typed)
3897        } else {
3898            None
3899        };
3900        let encoding_type = coding_behavior.clone();
3901        let encoding_format = coding_type.clone();
3902
3903        Ok(CorticalAreaInfo {
3904            cortical_id: area.cortical_id.as_base_64(),
3905            cortical_id_s: area.cortical_id.to_string(), // Human-readable ASCII string
3906            cortical_idx,
3907            name: area.name.clone(),
3908            dimensions: (
3909                area.dimensions.width as usize,
3910                area.dimensions.height as usize,
3911                area.dimensions.depth as usize,
3912            ),
3913            position: area.position.into(),
3914            area_type: cortical_group
3915                .clone()
3916                .unwrap_or_else(|| "CUSTOM".to_string()),
3917            cortical_group: cortical_group
3918                .clone()
3919                .unwrap_or_else(|| "CUSTOM".to_string()),
3920            // Determine cortical_type based on properties
3921            cortical_type: {
3922                use feagi_evolutionary::extract_memory_properties;
3923                if extract_memory_properties(&area.properties).is_some() {
3924                    "memory".to_string()
3925                } else if let Some(group) = &cortical_group {
3926                    match group.as_str() {
3927                        "IPU" => "sensory".to_string(),
3928                        "OPU" => "motor".to_string(),
3929                        "CORE" => "core".to_string(),
3930                        _ => "custom".to_string(),
3931                    }
3932                } else {
3933                    "custom".to_string()
3934                }
3935            },
3936            neuron_count,
3937            synapse_count,
3938            incoming_synapse_count,
3939            outgoing_synapse_count,
3940            visible: area.visible(),
3941            sub_group: area.sub_group(),
3942            neurons_per_voxel: area.neurons_per_voxel(),
3943            postsynaptic_current: area.postsynaptic_current() as f64,
3944            postsynaptic_current_max: area.postsynaptic_current_max() as f64,
3945            plasticity_constant: area.plasticity_constant() as f64,
3946            degeneration: area.degeneration() as f64,
3947            psp_uniform_distribution: area.psp_uniform_distribution(),
3948            mp_driven_psp: area.mp_driven_psp(),
3949            firing_threshold: area.firing_threshold() as f64,
3950            firing_threshold_increment: [
3951                area.firing_threshold_increment_x() as f64,
3952                area.firing_threshold_increment_y() as f64,
3953                area.firing_threshold_increment_z() as f64,
3954            ],
3955            firing_threshold_limit: area.firing_threshold_limit() as f64,
3956            consecutive_fire_count: area.consecutive_fire_count(),
3957            snooze_period: area.snooze_period() as u32,
3958            refractory_period: area.refractory_period() as u32,
3959            leak_coefficient: area.leak_coefficient() as f64,
3960            leak_variability: area.leak_variability() as f64,
3961            mp_charge_accumulation: area.mp_charge_accumulation(),
3962            neuron_excitability: area.neuron_excitability() as f64,
3963            burst_engine_active: area.burst_engine_active(),
3964            init_lifespan: area.init_lifespan(),
3965            lifespan_growth_rate: area.lifespan_growth_rate() as f64,
3966            longterm_mem_threshold: area.longterm_mem_threshold(),
3967            temporal_depth: {
3968                use feagi_evolutionary::extract_memory_properties;
3969                extract_memory_properties(&area.properties).map(|p| p.temporal_depth.max(1))
3970            },
3971            properties: HashMap::new(),
3972            cortical_subtype,
3973            encoding_type,
3974            encoding_format,
3975            unit_id,
3976            group_id,
3977            coding_signage,
3978            coding_behavior,
3979            coding_type,
3980            coding_options,
3981            parent_region_id: manager.get_parent_region_id_for_area(&cortical_id_typed),
3982            // Extract dev_count and cortical_dimensions_per_device from properties for IPU/OPU
3983            dev_count: area
3984                .properties
3985                .get("dev_count")
3986                .and_then(|v| v.as_u64().map(|n| n as usize)),
3987            cortical_dimensions_per_device: area
3988                .properties
3989                .get("cortical_dimensions_per_device")
3990                .and_then(|v| v.as_array())
3991                .and_then(|arr| {
3992                    if arr.len() == 3 {
3993                        Some((
3994                            arr[0].as_u64()? as usize,
3995                            arr[1].as_u64()? as usize,
3996                            arr[2].as_u64()? as usize,
3997                        ))
3998                    } else {
3999                        None
4000                    }
4001                }),
4002            visualization_voxel_granularity: area
4003                .properties
4004                .get("visualization_voxel_granularity")
4005                .and_then(|v| v.as_array())
4006                .and_then(|arr| {
4007                    if arr.len() == 3 {
4008                        Some((
4009                            arr[0].as_u64()? as u32,
4010                            arr[1].as_u64()? as u32,
4011                            arr[2].as_u64()? as u32,
4012                        ))
4013                    } else {
4014                        None
4015                    }
4016                }),
4017        })
4018    }
4019
4020    /// Helper to get cortical area info
4021    async fn get_cortical_area_info(&self, cortical_id: &str) -> ServiceResult<CorticalAreaInfo> {
4022        // Convert String to CorticalID
4023        let cortical_id_typed = CorticalID::try_from_base_64(cortical_id)
4024            .map_err(|e| ServiceError::InvalidInput(format!("Invalid cortical ID: {}", e)))?;
4025
4026        let manager = self.connectome.read();
4027
4028        let area = manager
4029            .get_cortical_area(&cortical_id_typed)
4030            .ok_or_else(|| ServiceError::NotFound {
4031                resource: "CorticalArea".to_string(),
4032                id: cortical_id.to_string(),
4033            })?;
4034
4035        // DIAGNOSTIC: Log the position, dimensions, and visibility of the area being queried
4036        tracing::info!(target: "feagi-services",
4037            "get_cortical_area_info: querying {} - position {:?}, dimensions {:?}, visible: {}",
4038            cortical_id, area.position, area.dimensions, area.visible()
4039        );
4040
4041        let cortical_idx = manager
4042            .get_cortical_idx(&cortical_id_typed)
4043            .ok_or_else(|| ServiceError::NotFound {
4044                resource: "CorticalArea".to_string(),
4045                id: cortical_id.to_string(),
4046            })?;
4047
4048        let neuron_count = manager.get_neuron_count_in_area(&cortical_id_typed);
4049        let outgoing_synapse_count = manager.get_outgoing_synapse_count_in_area(&cortical_id_typed);
4050        let incoming_synapse_count = manager.get_incoming_synapse_count_in_area(&cortical_id_typed);
4051        let synapse_count = outgoing_synapse_count;
4052
4053        // Get cortical_group from the area (uses cortical_type_new if available)
4054        let cortical_group = area.get_cortical_group();
4055        let cortical_bytes = cortical_id_typed.as_bytes();
4056        let is_io_area = cortical_bytes[0] == b'i' || cortical_bytes[0] == b'o';
4057        let io_flag = if is_io_area {
4058            cortical_id_typed.extract_io_data_flag().ok()
4059        } else {
4060            None
4061        };
4062        let cortical_subtype = if is_io_area {
4063            String::from_utf8(cortical_bytes[0..4].to_vec()).ok()
4064        } else {
4065            None
4066        };
4067        let unit_id = if is_io_area {
4068            Some(cortical_bytes[6])
4069        } else {
4070            None
4071        };
4072        let group_id = if is_io_area {
4073            Some(cortical_bytes[7])
4074        } else {
4075            None
4076        };
4077        let coding_signage = io_flag
4078            .as_ref()
4079            .map(|flag| signage_label_from_flag(flag).to_string());
4080        let coding_behavior = io_flag
4081            .as_ref()
4082            .map(|flag| behavior_label_from_flag(flag).to_string());
4083        let coding_type = io_flag
4084            .as_ref()
4085            .map(|flag| coding_type_label_from_flag(flag).to_string());
4086        let coding_options = if is_io_area {
4087            io_coding_options_for_unit(&cortical_id_typed)
4088        } else {
4089            None
4090        };
4091        let encoding_type = coding_behavior.clone();
4092        let encoding_format = coding_type.clone();
4093
4094        Ok(CorticalAreaInfo {
4095            cortical_id: area.cortical_id.as_base_64(),
4096            cortical_id_s: area.cortical_id.to_string(), // Human-readable ASCII string
4097            cortical_idx,
4098            name: area.name.clone(),
4099            dimensions: (
4100                area.dimensions.width as usize,
4101                area.dimensions.height as usize,
4102                area.dimensions.depth as usize,
4103            ),
4104            position: area.position.into(),
4105            area_type: cortical_group
4106                .clone()
4107                .unwrap_or_else(|| "CUSTOM".to_string()),
4108            cortical_group: cortical_group
4109                .clone()
4110                .unwrap_or_else(|| "CUSTOM".to_string()),
4111            // Determine cortical_type based on properties
4112            cortical_type: {
4113                use feagi_evolutionary::extract_memory_properties;
4114                if extract_memory_properties(&area.properties).is_some() {
4115                    "memory".to_string()
4116                } else if let Some(group) = &cortical_group {
4117                    match group.as_str() {
4118                        "IPU" => "sensory".to_string(),
4119                        "OPU" => "motor".to_string(),
4120                        "CORE" => "core".to_string(),
4121                        _ => "custom".to_string(),
4122                    }
4123                } else {
4124                    "custom".to_string()
4125                }
4126            },
4127            neuron_count,
4128            synapse_count,
4129            incoming_synapse_count,
4130            outgoing_synapse_count,
4131            visible: area.visible(),
4132            sub_group: area.sub_group(),
4133            neurons_per_voxel: area.neurons_per_voxel(),
4134            postsynaptic_current: area.postsynaptic_current() as f64,
4135            postsynaptic_current_max: area.postsynaptic_current_max() as f64,
4136            plasticity_constant: area.plasticity_constant() as f64,
4137            degeneration: area.degeneration() as f64,
4138            psp_uniform_distribution: area.psp_uniform_distribution(),
4139            mp_driven_psp: area.mp_driven_psp(),
4140            firing_threshold: area.firing_threshold() as f64,
4141            firing_threshold_increment: [
4142                area.firing_threshold_increment_x() as f64,
4143                area.firing_threshold_increment_y() as f64,
4144                area.firing_threshold_increment_z() as f64,
4145            ],
4146            firing_threshold_limit: area.firing_threshold_limit() as f64,
4147            consecutive_fire_count: area.consecutive_fire_count(),
4148            snooze_period: area.snooze_period() as u32,
4149            refractory_period: area.refractory_period() as u32,
4150            leak_coefficient: area.leak_coefficient() as f64,
4151            leak_variability: area.leak_variability() as f64,
4152            mp_charge_accumulation: area.mp_charge_accumulation(),
4153            neuron_excitability: area.neuron_excitability() as f64,
4154            burst_engine_active: area.burst_engine_active(),
4155            init_lifespan: area.init_lifespan(),
4156            lifespan_growth_rate: area.lifespan_growth_rate() as f64,
4157            longterm_mem_threshold: area.longterm_mem_threshold(),
4158            temporal_depth: {
4159                use feagi_evolutionary::extract_memory_properties;
4160                extract_memory_properties(&area.properties).map(|p| p.temporal_depth.max(1))
4161            },
4162            properties: HashMap::new(),
4163            cortical_subtype,
4164            encoding_type,
4165            encoding_format,
4166            unit_id,
4167            group_id,
4168            coding_signage,
4169            coding_behavior,
4170            coding_type,
4171            coding_options,
4172            parent_region_id: manager.get_parent_region_id_for_area(&cortical_id_typed),
4173            // Extract dev_count and cortical_dimensions_per_device from properties for IPU/OPU
4174            dev_count: area
4175                .properties
4176                .get("dev_count")
4177                .and_then(|v| v.as_u64().map(|n| n as usize)),
4178            cortical_dimensions_per_device: area
4179                .properties
4180                .get("cortical_dimensions_per_device")
4181                .and_then(|v| v.as_array())
4182                .and_then(|arr| {
4183                    if arr.len() == 3 {
4184                        Some((
4185                            arr[0].as_u64()? as usize,
4186                            arr[1].as_u64()? as usize,
4187                            arr[2].as_u64()? as usize,
4188                        ))
4189                    } else {
4190                        None
4191                    }
4192                }),
4193            visualization_voxel_granularity: area
4194                .properties
4195                .get("visualization_voxel_granularity")
4196                .and_then(|v| v.as_array())
4197                .and_then(|arr| {
4198                    if arr.len() == 3 {
4199                        Some((
4200                            arr[0].as_u64()? as u32,
4201                            arr[1].as_u64()? as u32,
4202                            arr[2].as_u64()? as u32,
4203                        ))
4204                    } else {
4205                        None
4206                    }
4207                }),
4208        })
4209    }
4210}