trusty-mpm 1.8.2

trusty-mpm: unified multi-agent orchestration platform (core, daemon, CLI, TUI, Telegram)
//! `SessionManager::numbered_snapshot` — the stable `tm ls` slot view (#3034).
//!
//! Why: `manager.rs` sits at the 500-SLOC production cap; this single method
//! is extracted here so it has somewhere to land without needing a broader
//! `manager.rs` refactor, mirroring the `reactivate.rs`/`reconcile.rs` split
//! precedent already used for `mark_reactivated`/`reconcile_on_boot`.
//! What: an inherent `impl SessionManager` block adding
//! [`SessionManager::numbered_snapshot`], which observes every record into
//! the manager's [`super::slots::SlotRegistry`] and returns the resulting
//! per-slot view.
//! Test: `numbered_snapshot_*` in `super::slots_tests`.

use std::collections::HashMap;

use super::manager::SessionManager;
use super::record::{ManagedSessionId, SessionRecord};
use super::slots::NumberedSlot;

impl SessionManager {
    /// Build the stable-numbered session view for `tm ls` (#3034).
    ///
    /// Why: two concurrent `tm ls` invocations must see the SAME number for
    /// the same session, and a deleted session's number must stay reserved
    /// (tombstoned, not reused) — neither guarantee is possible from a
    /// client-side per-fetch enumeration, so assignment lives here in the one
    /// daemon-owned [`super::slots::SlotRegistry`] every listing call reads
    /// and writes.
    /// What: observes every record in `records` (pass the FULL, unfiltered
    /// [`SessionManager::list`] result — a `--source-id`-filtered subset
    /// would leave excluded sessions unobserved and renumbered on their next
    /// global listing) into the registry, then returns one [`NumberedSlot`]
    /// per slot the registry still HOLDS: `record: Some(_)` when still present
    /// in `records`, `record: None` once it disappears from the store via ANY
    /// removal path (hard-delete, decommission-reap, prune).
    ///
    /// Walks the held slots rather than `1..=max` because two paths release a
    /// slot and leave gaps: the retention sweep ([`super::retention`]) when a
    /// record ages out, and [`super::prune`] when it compacts a tombstone out of
    /// the store (#5897). A `1..=max` walk would emit a phantom tombstone row at
    /// every gap — a number nobody holds and no operator ever saw.
    /// Test: `numbered_snapshot_keeps_slot_stable_across_delete`,
    /// `numbered_snapshot_tombstones_deleted_slot`,
    /// `numbered_snapshot_never_reuses_a_deleted_slot`,
    /// `numbered_snapshot_concurrent_calls_agree_on_new_session_slot` in
    /// `super::slots_tests`.
    pub async fn numbered_snapshot(&self, records: &[SessionRecord]) -> Vec<NumberedSlot> {
        let mut reg = self.slots.write().await;
        for r in records {
            reg.observe(r.id, r.source_id.as_deref());
        }
        let live: HashMap<ManagedSessionId, SessionRecord> =
            records.iter().map(|r| (r.id, r.clone())).collect();
        reg.assigned_slots()
            .into_iter()
            .map(|slot| NumberedSlot {
                slot,
                record: reg.id_at(slot).and_then(|id| live.get(&id).cloned()),
                source_id: reg.source_id_at(slot).map(str::to_owned),
            })
            .collect()
    }
}