LIFECYCLE / HOT-SWAP REVIEW — ACTIVE SOURCE ONLY
Scope: Module/ConfigManager.h, Modules.h, WorkloadMission.h, SampleTestIntegrationCode_v35.cpp, ThreadManager/SharedQueue, plus concrete lifecycle implementations. Static source inspection; no hardware execution and no source edits. Line numbers refer to source/ paths. Commented historical copies were excluded.

1. ACTUAL INSTANTIATION AND OWNERSHIP

v35 dynamic runner creates one Context and one shared ThreadManager (1421–1424), then one unique ConfigManager (1518–1519). Context contains shutdown_flag, pipelineReady, shared_ptr<ThreadManager> (Modules.h:47–52). ConfigManager owns vector<unique_ptr<IModule>>, shared aggregator, three queues and RuntimeControls; named module pointers are nonowning views (ConfigManager.h:821–838). ThreadManager starts two generic pool threads in its constructor (ThreadManager.h:46–49); concrete camera/algorithm/display/Lynsyn work is registered separately, not automatically dispatched through this pool.

buildPipeline (ConfigManager.h:227–371) allocates queues (cam2Alg=200, cam2Disp=200, alg2Disp=300 at586–589), RuntimeControls, aggregator, and wrappers in this exact order:
 DisplayModule -> AlgorithmModule -> CameraModule -> SoCModule -> optional LynsynModule -> EvolutionarySelector OR SchedulerModule.

All wrappers share aggregator ownership; AlgorithmModule and controller share RuntimeControls. Camera/Algorithm/Display share relevant queues. Wrapper concrete ownership is unique_ptr: DataConcrete (Modules:431), AlgorithmConcrete (763), SdlDisplayConcrete (853), SoCConcrete (1017), LynsynMonitorConcrete (1191). EvolutionarySelector owns Scheduler by value and GeneticAlgorithm by unique_ptr (EvolutionarySelector_02.h:600–606). GA owns SurrogateModel and optional AWM state; these persist for the mission.

Factory branching: schedulerEnabled || evoEnabled permits a controller; useERL selects EvolutionarySelector (ConfigManager:295–298,323–368). Selector config begins with Scheduler then overrides with EvolutionarySelector keys (327–330); ConfigManager injects actual active algorithm identity and GPU split applicability (337–344). Standard SchedulerModule's step currently applies a default-initialized MetricsSnapshot with commented-out real fetch (Modules:205–219); do not describe it as a fully wired measured scheduler.

Actual background work begins during aggregator construction, before startAll(): constructor launches flushThread (SystemMetricsAggregatorConcrete_v3_2.h:318–347); start() is empty (2115–2118). startAll orders aggregator.start -> Display -> Algorithm -> Camera -> SoC -> Lynsyn -> controller, with50ms delays, finally pipelineReady=true (ConfigManager:375–403). Thread sleep is not a successful-device/first-frame readiness acknowledgment. CameraModule checks open/configure but ignores bool startCapture (Modules:406–411). Lynsyn configure/initialize failure logs, disables expectsPower and returns; error callback only logs (1155–1175). A ready pipeline can therefore lack Lynsyn samples.

2. WARMUP AND MISSION ORDER

Dynamic profile validation occurs before hardware creation (v35:1433–1437). ConfigManager constructed once, GA warmup hold set BEFORE startAll (1518–1524), bootstrap controls applied once (1526–1529). PipelineStarted=true is set before startAll to enable cleanup of partial startup (1585–1586). Camera, telemetry and workers run during warmup; GA hold prevents evolve. Warmup waits honor global/context shutdown; no-frame warmup fails (1592–1609).

After warmup, mission clock starts at1614–1615. Optional collector construction/configuration then occurs1622–1655; optional background reader imposes a6s sleep1645. Only afterward is GA hold released1657–1658. Thus measured mission time can include >6s with GA paused; comments claiming collector begin at mission t=0 do not match exact ordering. WorkloadMission phases are absolute offsets from steady-clock start (WorkloadMission:166–178), not equal durations following completion of transitions. Transition overhead reduces each subsequent phase's active time. dueTransition does not advance the phase; caller must confirmAdvanced only after success (184–195).

3. EXACT HOT-SWAP / BARRIER ORDER

Caller (v35:1694–1858):
 a. Build next profile, snapshot current generation/epoch/regime/frame; static arms publish requested epoch+1 (1695–1711).
 b. Collector workload_exit is emitted BEFORE ConfigManager begins its GA barrier (1722–1727).
 c. ConfigManager::switchAlgorithmWorkload checks target and reads previous algorithm (480–492).
 d. Selector beginWorkloadTransition sets transitionInProgress=true, acquires gaMutex to wait for any in-flight evolve, records max(latest aggregator frame,last evolved frame), requests current epoch+1 and returns boundary generation (EvolutionarySelector:378–414). The mutex is released when begin returns. The atomic pause flag, plus recheck under gaMutex in normal evolve (739–745), keeps new evolution out during the swap.
 e. AlgorithmModule takes workloadMutex; equal algorithm names return success without restarting or changing epoch (Modules:529–540). Otherwise require both queues live (543–552), construct/configure replacement BEFORE stopping old worker (557–568).
 f. Read previous/requested epoch, stop old AlgorithmConcrete in HotSwap mode and join its registered thread (Modules:570–586; AlgorithmConcrete:287–309). This only changes running_; it DOES NOT stop input/output queues.
 g. Only AFTER join, check queues still live, drain input and output queues (Modules:588–602). Camera/display/SoC/Lynsyn/aggregator continue running. Draining input is an instantaneous boundary, not a camera pause; fresh frames can arrive immediately afterward.
 h. Clear processed_frame_id, leave processed epoch old, activate target epoch, then start replacement (611–621). Worker publishes processed frame then active epoch after real processing (AlgorithmConcrete:1011–1025).
 i. Replace internal unique_ptr and update algorithm config (Modules:655–657). Worker start does not mean a new frame has already completed.
 j. Selector commit locks gaMutex, validates active algorithm epoch, calls GA setWorkloadContext, installs post-transition frame floor, samples generation and releases pause (EvolutionarySelector:416–472). ConfigManager updates its audit config afterward (546–548).
 k. v35 samples results, waits up to5s for matching processed epoch + processed frame newer than pre-swap snapshot + aggregator snapshot at/after that frame (1745–1797). GA has already resumed by this point. It fails if swap false, generation differs, epoch does not increase once, no proof frame, or return-to-initial surrogate bank is empty (1799–1842).
 l. Only then confirmAdvanced, update active label/id, re-pin static controls and emit collector workload_enter (1844–1858).

This is a real algorithm-only swap, NOT reconstruction of the controller or telemetry pipeline. Preservation is supported by actual object ownership and setWorkloadContext implementation, not only comments. Retained: GA generation/population/RNG/mutation state/AWM history/Scheduler/RuntimeControls, surrogate object plus per-workload banks, per-workload max measured FPS (GeneticAlgorithm_05.h:435–501). Population genes may be canonicalized for new hardware capability (483–485). The previous action's temporal credit is deliberately cleared via hasLastApplied_=false (490), and workloadEpoch increments (491). Workload ids are stable only within process and depend on first-seen label order (WorkloadMission:61–73).

Rollback: replacement configure failure leaves old running (Modules:559–567); start failure restores epoch, attempts old restart but only logs if rollback itself fails (622–652). Queues-stopped check AFTER old join returns false without restarting old (591–596). ConfigManager cancels pause on false; commit exception attempts old workload and ignores rollback result/exception (532–543). The v35 mission detects returned false and aborts, rather than continuing a supposed successful phase. General callers should not treat a false result as proof old worker remains healthy.

Barrier limit: invalid-snapshot watchdog branch acquires gaMutex without rechecking transitionInProgress (EvolutionarySelector:713–727), and recovery at726 is outside mutex. An already-past-outer-check control iteration may still change RuntimeControls during a swap. The barrier demonstrably excludes normal evolve, not every external actuator mutation. Evolution reviewer owns this wider recovery/credit analysis.

4. SHUTDOWN ORDER AND REVISED PRIOR RISKS

Normal v35 dynamic ending exports surrogate/AWM while controller remains alive (1959–1968), seals collector while controller is still alive (1973–1978), then ctx.shutdown=true -> manager.stopAll -> manager.flushMetrics -> ctx.tm.reset (1981–1985). stopAll explicitly stops Display first, then reverse module order excluding Display: controller -> Lynsyn -> SoC -> Camera -> Algorithm (ConfigManager:413–445). Display joins Component::Custom then destroys renderer (Modules:830–844); controller joins direct thread and resets Scheduler (EvolutionarySelector:323–330); camera joins capture and deletes DataConcrete (Modules:418–423); algorithm is stopped last. stopAll never stops or drains queues. SharedQueue's documented lifecycle owner is ConfigManager but actual stop occurs only at queue destruction, unless other code explicitly calls it (SharedQueue:295–311; ConfigManager:413–445).

L1 — HIGH — PRIOR RAW CAMERA OWNER RISK NOW CONFIRMED FOR LEGACY YUYV SHUTDOWN.
BufferDeleter contains DataConcrete* and calls owner->queueBuffer (DataConcrete:131–139); legacy YUYV frames retain this guard (1511–1527). Camera is destroyed BEFORE algorithm, while camera input/display queues still exist and can retain guarded frames (ConfigManager:431–440; Modules:418–423,717–722). DataConcrete destructor closes/unmaps buffers (304–318,1729–1736). Queued frame destruction later calls a freed owner; an algorithm still processing can also read unmapped memory. Display-first destruction releases its retained last frames safely while camera exists, but does NOT drain its original queue and does not prevent new producer enqueues between Display stop and Camera stop. CSI_RG10_TO_YUYV path has independent vector ownership and immediately requeues raw buffers (1483–1509), so that backend is not affected by this specific guard hazard. No hardware reproduction claimed.

L2 — HIGH — PRIOR OUTPUT-PUSH/JOIN DEADLOCK REMAINS REACHABLE.
Algorithm output uses blocking SharedQueue::push (AlgorithmConcrete:1051 and1882); full bounded push waits only for queue space or queue stop (SharedQueue:123–164), never AlgorithmConcrete::running_. stopAlgorithm sets running=false then joins without waking output push (287–303). Hot-swap drains output only AFTER join (Modules:585,602), too late to break the wait. Normal stop explicitly terminates the only consumer first and never closes queues. Display quit/ESC also stops only the display's local running flag (SdlDisplay:918–923; Modules:868), with no context shutdown propagation: continued production can fill the300-frame output queue and make the next swap/shutdown hang. Input timed pop50ms mitigates empty-input waiting ONLY (AlgorithmConcrete:371–401); it is not a bounded total quiesce guarantee. CSI vector frames permit output queue growth; USB limited buffers can instead stall camera before300 output frames.

L3 — HIGH — PRIOR LYNSYN JOIN/MUTEX DEADLOCK REMAINS.
stop() sets threadShouldExit, then holds lifecycleMutex while joining registered Lynsyn thread (LynsynMonitorConcrete:139–148). On failed sampling, worker calls fullResetThreadSafe (376–378; mutex at184–186) and later separately locks lifecycleMutex at381 before re-init. If stop wins that mutex while worker has entered recovery, each waits on the other; setting exit flag does not interrupt a mutex wait. ConfigManager's stop invokes this path before camera/algorithm and no orchestration barrier makes it safe.

L4 — MEDIUM — PRIOR REPEATED DISPLAY MERGE CONFIRMED, UNCONDITIONAL TWO-SECOND CLAIM REVISED.
Texture conversion/upload dedup was added (SdlDisplay:944–955), but mergeDisplay still runs every render tick using retained lastProcessed (980–990); finalized old frame is missing and waits in aggregator cv.wait_for (Aggregator:412–428). However ConfigManager explicitly forwards merge_wait_ms derived from camera FPS with default10..250ms (239–247); aggregator clamps50..5000 (286). Typical default60fps ->50ms wait, not2s. Explicit Aggregator.merge_wait_ms=2000 still gives2s. No supplied JSON effective config was found under source; do not assert observed runtime wait. This can stall rendering, distort displayed arrival/FPS sampling, and contribute backpressure; it is distinct from input-pop cancellation.

L5 — HIGH FOR EVIDENCE — PRIOR COLLECTOR FAILURE CAN RETURN SUCCESS CONFIRMED.
begin_run false only logs continuing without collector (v35:1653–1655); collector abort check breaks without failed=true (1690–1692); end_run bool only logged (1973–1978). Return status depends solely on unrelated failed variable (1987). Ctrl-C/context termination can likewise end before mission duration and return success if failed remained false. Collector specialist additionally reproduced failed writes sealing success using /dev/full; see their report for evidence.

5. ADDITIONAL LIFECYCLE FINDINGS

L6 — HIGH — BENCHMARK EXCEPTION CLEANUP DOES NOT JOIN BEFORE OWNER DESTRUCTION.
Dynamic runner correctly keeps manager outside try and uses pipelineStarted to stop partial startup (v35:1426–1427,1585,1996–2002). run_benchmark_case instead creates Context and ConfigManager inside try/scoped block (1062–1096); manager.startAll at1158 can throw; ConfigManager catch only flushes aggregator and rethrows (398–402). ConfigManager has no destructor stop, IModule destructor is default (IModule:21), DisplayModule has no destructor joining its ThreadManager-owned worker (Modules:782–903). Automatic destruction can free DisplayModule/SdlDisplayConcrete while renderLoopThread still dereferences them. If failure occurs after controller startup, EvolutionarySelector's direct std::thread member (605–606) is still joinable, causing std::terminate on its default destructor. Benchmark outer catch1311–1313 is therefore not sufficient safe cleanup. Avoid stating the dynamic path has this same cleanup omission; it has explicit cleanup, but the underlying deadlocks remain.

L7 — HIGH — COLLECTOR LIFETIME IS NOT SYNCHRONIZED WITH SELECTOR.
End_run precedes selector stop and global pointer clearing is unsynchronized (v35:1973–1982). On exception inside try, local pilotCollector is destroyed BEFORE catch, while manager outside try remains active until1999. Collector destructor only stops/joins writer (PilotCollector.h:339), and global() is a plain pointer reference (PilotCollectorHooks.h:32), not cleared by destructor. A selector that already obtained pointer can call into a destroyed collector. Collector specialist verified end_run also iterates stage_ without ctrl_mutex and allows concurrent emitter paths. See collector report for full ownership/race analysis.

L8 — MEDIUM — OPTIONAL BACKGROUND STARTUP CONSUMES MEASURED FIRST PHASE.
Mission clock starts1614; bg reader sleep6s at1645 precedes collector begin1647 and GA resume1658. First phase loses>=6s of adaptive evolution and recording when --collector-bg-jsonl is used. With very short phase intervals multiple boundaries may already be due at first mission-loop iteration. Move/preflight collector/background readiness into excluded warmup or explicitly report offset; do not describe every phase as full requested seconds of adaptive operation.

L9 — MEDIUM — ADJACENT SAME-WORKLOAD PHASES ARE ACCEPTED BUT FAIL AT RUNTIME.
WorkloadMission parse only requires times increase and labels nonempty (109–151), so 0:HistogramEqualization,10:HistogramEqualization is valid. AlgorithmModule equal-name branch534–540 succeeds without epoch activation. ERL begin requested epoch+1, then commit detects mismatch430–434 and returns false via ConfigManager rollback; static arms instead reach v35 epoch invariant1816–1821. Reject duplicate adjacent phases during preflight, or define an epoch-only phase boundary path. Default alternating mission unaffected.

6. ERROR PROPAGATION SUMMARY / LIMITS

Exceptions from camera configuration/display/SoC initialization propagate (Modules:406–407,811–812,1004–1005); Lynsyn initialization is intentionally nonfatal (1155–1167). validateAll returns first false (ConfigManager:406–410). Module stop exceptions are swallowed/logged and stopAll still reports all stopped (426–445). Dynamic run fails on warmup no frames, swap/provenance invariant and algorithm input starvation (v35:1606–1609,1799–1842,1877–1885), but not all output failures, collector failures or premature loop exits. Neither CameraModule nor AlgorithmModule installs a context-failing error callback; display quit does not propagate global termination. runLoop itself always returns true (ConfigManager:448–458), although v35 has its own loops.

All findings above are source-level causal traces. No claim of successful compilation, sanitizer result or Jetson hardware reproduction is made. Earlier conditional findings were refined using newly available orchestration, with camera-backend and merge-wait qualifications explicitly retained.
