1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
//! `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()
}
}