Skip to main content

magi_code/sessions/
metadata.rs

1use super::event::{SessionEvent, SessionEventKind};
2use super::manager::{Session, SessionInternalDiagnostic, SessionListReport, SessionManager};
3use super::read::{MAX_METADATA_VISIT_BYTES, MAX_METADATA_VISIT_LINES, validate_session_id};
4use super::store::{open_existing_named, validate_existing_file, validate_path_file};
5use super::write::{
6    COMPACTION_SCHEMA_VERSION, sanitize_compaction_summary, sanitize_session_title,
7};
8use crate::{output::redact_sensitive_text, persistence::atomic_write_with_permissions};
9use chrono::{DateTime, Utc};
10use serde::{Deserialize, Serialize};
11use serde_json::Value;
12use std::{
13    fs,
14    io::Read,
15    path::{Path, PathBuf},
16    time::{SystemTime, UNIX_EPOCH},
17};
18
19const MAX_SESSION_METADATA_BYTES: u64 = 64 * 1024;
20const MAX_SESSION_INTERNAL_DIAGNOSTICS: usize = 64;
21pub(crate) fn push_session_diagnostic(
22    diagnostics: &mut Vec<SessionInternalDiagnostic>,
23    session_id: Option<String>,
24    message: String,
25) {
26    if diagnostics.len() < MAX_SESSION_INTERNAL_DIAGNOSTICS {
27        let redacted = redact_sensitive_text(&message);
28        let mut chars = redacted.chars();
29        let mut bounded = chars.by_ref().take(500).collect::<String>();
30        if chars.next().is_some() {
31            bounded.push('…');
32        }
33        diagnostics.push(SessionInternalDiagnostic {
34            session_id,
35            message: bounded,
36        });
37    }
38}
39
40#[derive(Debug, Clone, Copy)]
41pub(crate) enum SessionDiagnosticOperation {
42    Replay,
43    Metadata,
44    Listing,
45    Title,
46    RecentContext,
47}
48
49impl SessionDiagnosticOperation {
50    fn as_str(self) -> &'static str {
51        match self {
52            Self::Replay => "replay",
53            Self::Metadata => "metadata",
54            Self::Listing => "listing",
55            Self::Title => "title",
56            Self::RecentContext => "recent_context",
57        }
58    }
59}
60
61pub(crate) fn report_session_diagnostic(
62    operation: SessionDiagnosticOperation,
63    _path: &Path,
64    _error: impl std::fmt::Display,
65) {
66    eprintln!(
67        "warning: session operation={} category=session_jsonl failed",
68        operation.as_str()
69    );
70}
71const SESSION_METADATA_SCHEMA_VERSION: u64 = 3;
72const SESSION_METADATA_EXTENSION: &str = "metadata.json";
73
74#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
75pub(in crate::sessions) struct SessionMetadataRecord {
76    schema_version: u64,
77    session_id: String,
78    latest_activity_timestamp: Option<DateTime<Utc>>,
79    latest_title: Option<String>,
80    session_path: Option<PathBuf>,
81    #[serde(default)]
82    first_user_input_text: Option<String>,
83    user_input_count: usize,
84    jsonl_len: u64,
85    jsonl_modified_ns: Option<u128>,
86    complete: bool,
87}
88
89#[derive(Debug, Clone, PartialEq, Eq)]
90pub(crate) struct SessionMetadataSummary {
91    pub(crate) session: Session,
92    pub(crate) latest_activity_time: SystemTime,
93    pub(crate) latest_title: Option<String>,
94    pub(crate) session_path: Option<PathBuf>,
95}
96
97impl SessionMetadataRecord {
98    fn empty(session: &Session, marker: JsonlMarker, complete: bool) -> Self {
99        Self {
100            schema_version: SESSION_METADATA_SCHEMA_VERSION,
101            session_id: session.id.clone(),
102            latest_activity_timestamp: None,
103            latest_title: None,
104            session_path: None,
105            first_user_input_text: None,
106            user_input_count: 0,
107            jsonl_len: marker.len,
108            jsonl_modified_ns: marker.modified_ns,
109            complete,
110        }
111    }
112
113    fn apply_event(&mut self, event: &SessionEvent) {
114        if self.session_path.is_none() {
115            self.session_path = event.session_path.clone();
116        }
117        self.latest_activity_timestamp = Some(
118            self.latest_activity_timestamp
119                .map_or(event.timestamp, |current| current.max(event.timestamp)),
120        );
121        match event.kind() {
122            Some(SessionEventKind::SessionTitle) => {
123                if let Some(title) = event.payload.get("title").and_then(Value::as_str)
124                    && let Some(title) = sanitize_session_title(title)
125                {
126                    self.latest_title = Some(title);
127                }
128                self.first_user_input_text = None;
129            }
130            Some(SessionEventKind::UserInput) => {
131                self.user_input_count = self.user_input_count.saturating_add(1);
132                if self.first_user_input_text.is_none()
133                    && let Some(text) = event.payload.get("text").and_then(Value::as_str)
134                    && !text.trim().is_empty()
135                {
136                    self.first_user_input_text = Some(text.to_string());
137                }
138            }
139            Some(SessionEventKind::Compaction) => {
140                if let Some(aggregate) = event.payload.get("aggregate") {
141                    if let Some(title) = aggregate.get("latest_title").and_then(Value::as_str) {
142                        self.latest_title = sanitize_session_title(title);
143                    }
144                    if let Some(count) = aggregate.get("user_input_count").and_then(Value::as_u64)
145                        && let Ok(count) = usize::try_from(count)
146                    {
147                        self.user_input_count = count;
148                    }
149                    self.first_user_input_text = aggregate
150                        .get("first_user_input_text")
151                        .and_then(Value::as_str)
152                        .map(|text| redact_sensitive_text(text).chars().take(4_000).collect());
153                    if let Some(path) = aggregate.get("session_path").and_then(Value::as_str) {
154                        self.session_path = Some(PathBuf::from(path));
155                    }
156                    if let Some(timestamp) = aggregate.get("latest_activity_timestamp")
157                        && let Ok(timestamp) =
158                            serde_json::from_value::<chrono::DateTime<Utc>>(timestamp.clone())
159                    {
160                        self.latest_activity_timestamp = Some(
161                            self.latest_activity_timestamp
162                                .map_or(timestamp, |current| current.max(timestamp)),
163                        );
164                    }
165                }
166            }
167            _ => {}
168        }
169    }
170
171    pub(crate) fn checkpoint_aggregate(&self) -> Value {
172        serde_json::json!({
173            "latest_title": self.latest_title,
174            "user_input_count": self.user_input_count,
175            "first_user_input_text": self.first_user_input_text.as_deref().map(|text| redact_sensitive_text(text).chars().take(4_000).collect::<String>()),
176            "session_path": self.session_path.as_ref().map(|path| path.to_string_lossy().to_string()),
177            "latest_activity_timestamp": self.latest_activity_timestamp,
178        })
179    }
180
181    pub(crate) fn activity_time(&self, session: &Session) -> SystemTime {
182        self.latest_activity_timestamp
183            .map(SystemTime::from)
184            .unwrap_or_else(|| jsonl_modified_time(session).unwrap_or(SystemTime::UNIX_EPOCH))
185    }
186}
187
188#[derive(Debug, Clone, Copy, PartialEq, Eq)]
189struct JsonlMarker {
190    len: u64,
191    modified_ns: Option<u128>,
192}
193
194impl SessionManager {
195    pub(crate) fn list_metadata_summaries(&self) -> anyhow::Result<Vec<SessionMetadataSummary>> {
196        Ok(self.list_metadata_report()?.summaries)
197    }
198
199    pub(crate) fn list_metadata_report(&self) -> anyhow::Result<SessionListReport> {
200        if !self.root.exists() {
201            return Ok(SessionListReport {
202                summaries: Vec::new(),
203                diagnostics: Vec::new(),
204            });
205        }
206        super::store::validate_session_root(&self.root)?;
207        let mut summaries = Vec::new();
208        let mut diagnostics = Vec::new();
209        for entry in fs::read_dir(&self.root)? {
210            let entry = match entry {
211                Ok(entry) => entry,
212                Err(error) => {
213                    push_session_diagnostic(
214                        &mut diagnostics,
215                        None,
216                        format!("failed to read session directory entry: {error}"),
217                    );
218                    continue;
219                }
220            };
221            let path = entry.path();
222            let Some((id, path)) = session_from_entry(path) else {
223                continue;
224            };
225            let id = match validate_session_id(id) {
226                Ok(id) => id,
227                Err(error) => {
228                    push_session_diagnostic(
229                        &mut diagnostics,
230                        None,
231                        format!("ignored invalid session file {}: {error}", path.display()),
232                    );
233                    continue;
234                }
235            };
236            if validate_path_file(&self.root, &id, &path).is_err() {
237                push_session_diagnostic(
238                    &mut diagnostics,
239                    Some(id),
240                    "ignored unsafe session file".to_string(),
241                );
242                continue;
243            }
244            let session = Session { id, path };
245            match session_metadata_summary(session) {
246                Ok(summary) => summaries.push(summary),
247                Err(error) => push_session_diagnostic(
248                    &mut diagnostics,
249                    None,
250                    format!("failed to load session metadata: {error}"),
251                ),
252            }
253        }
254        summaries.sort_by(|left, right| {
255            left.latest_activity_time
256                .cmp(&right.latest_activity_time)
257                .then_with(|| left.session.id.cmp(&right.session.id))
258        });
259        Ok(SessionListReport {
260            summaries,
261            diagnostics,
262        })
263    }
264}
265
266pub(crate) fn session_from_entry(path: PathBuf) -> Option<(String, PathBuf)> {
267    if path
268        .extension()
269        .is_none_or(|extension| extension != "jsonl")
270    {
271        return None;
272    }
273    let id = path.file_stem()?.to_string_lossy().to_string();
274    Some((id, path))
275}
276
277pub(crate) fn metadata_path_for_session(session: &Session) -> PathBuf {
278    session.path.with_extension(SESSION_METADATA_EXTENSION)
279}
280
281fn jsonl_marker(session: &Session) -> anyhow::Result<JsonlMarker> {
282    let root = session
283        .path
284        .parent()
285        .ok_or_else(|| anyhow::anyhow!("session file has no parent"))?;
286    let Some(file) = super::store::open_existing_primary(root, &session.id)? else {
287        anyhow::bail!("session JSONL is missing");
288    };
289    let metadata = file.metadata()?;
290    Ok(JsonlMarker {
291        len: metadata.len(),
292        modified_ns: metadata.modified().ok().map(system_time_ns),
293    })
294}
295
296fn jsonl_modified_time(session: &Session) -> Option<SystemTime> {
297    let root = session.path.parent()?;
298    super::store::open_existing_primary(root, &session.id)
299        .ok()
300        .flatten()
301        .and_then(|file| file.metadata().ok())
302        .and_then(|metadata| metadata.modified().ok())
303}
304
305fn system_time_ns(time: SystemTime) -> u128 {
306    time.duration_since(UNIX_EPOCH)
307        .unwrap_or_default()
308        .as_nanos()
309}
310
311fn read_session_metadata_with_policy(
312    session: &Session,
313    require_complete: bool,
314) -> anyhow::Result<Option<SessionMetadataRecord>> {
315    let path = metadata_path_for_session(session);
316    let root = session
317        .path
318        .parent()
319        .ok_or_else(|| anyhow::anyhow!("session file has no parent"))?;
320    let name = path
321        .file_name()
322        .ok_or_else(|| anyhow::anyhow!("metadata path has no filename"))?
323        .to_string_lossy();
324    if !session.path.exists() {
325        return Ok(None);
326    }
327    let Some(file) = open_existing_named(root, &name)? else {
328        return Ok(None);
329    };
330    let mut bytes = Vec::new();
331    file.take(MAX_SESSION_METADATA_BYTES.saturating_add(1))
332        .read_to_end(&mut bytes)?;
333    if bytes.len() as u64 > MAX_SESSION_METADATA_BYTES {
334        return Ok(None);
335    }
336    let Ok(record) = serde_json::from_slice::<SessionMetadataRecord>(&bytes) else {
337        return Ok(None);
338    };
339    if record.schema_version != SESSION_METADATA_SCHEMA_VERSION
340        || record.session_id != session.id
341        || (require_complete && !record.complete)
342    {
343        return Ok(None);
344    }
345    let marker = jsonl_marker(session)?;
346    if record.jsonl_len != marker.len || record.jsonl_modified_ns != marker.modified_ns {
347        return Ok(None);
348    }
349    Ok(Some(record))
350}
351
352pub(crate) fn read_complete_session_metadata(
353    session: &Session,
354) -> anyhow::Result<Option<SessionMetadataRecord>> {
355    read_session_metadata(session)
356}
357
358fn read_session_metadata(session: &Session) -> anyhow::Result<Option<SessionMetadataRecord>> {
359    read_session_metadata_with_policy(session, true)
360}
361
362pub(crate) fn read_session_metadata_for_append(
363    session: &Session,
364) -> anyhow::Result<Option<SessionMetadataRecord>> {
365    read_session_metadata_with_policy(session, false)
366}
367fn write_session_metadata(session: &Session, record: &SessionMetadataRecord) -> anyhow::Result<()> {
368    let path = metadata_path_for_session(session);
369    validate_existing_file(&path)?;
370    let bytes = serde_json::to_vec_pretty(record)?;
371    atomic_write_with_permissions(&path, &bytes, Some(super::store::SESSION_FILE_MODE))
372}
373
374pub(crate) fn rebuild_session_metadata_from_jsonl(
375    session: &Session,
376) -> anyhow::Result<SessionMetadataRecord> {
377    validate_session_id(session.id.clone())?;
378    let initial_marker = jsonl_marker(session)?;
379    let mut record = SessionMetadataRecord::empty(session, initial_marker, true);
380    let (_, _) = session.visit_events_tolerant_bounded(
381        MAX_METADATA_VISIT_LINES,
382        MAX_METADATA_VISIT_BYTES,
383        |event| {
384            if event.session_id == session.id {
385                record.apply_event(&event);
386            }
387        },
388    )?;
389    let final_marker = jsonl_marker(session)?;
390    record.jsonl_len = final_marker.len;
391    record.jsonl_modified_ns = final_marker.modified_ns;
392    record.complete = initial_marker == final_marker;
393    if let Err(error) = write_session_metadata(session, &record) {
394        report_session_diagnostic(
395            SessionDiagnosticOperation::Metadata,
396            &metadata_path_for_session(session),
397            &error,
398        );
399    }
400    Ok(record)
401}
402
403pub(crate) fn write_rotated_session_metadata(
404    session: &Session,
405    mut record: SessionMetadataRecord,
406    checkpoint: &SessionEvent,
407) -> anyhow::Result<()> {
408    record.apply_event(checkpoint);
409    let final_marker = jsonl_marker(session)?;
410    record.jsonl_len = final_marker.len;
411    record.jsonl_modified_ns = final_marker.modified_ns;
412    record.complete = true;
413    write_session_metadata(session, &record)
414}
415
416pub(crate) fn session_metadata_for_listing(
417    session: &Session,
418) -> anyhow::Result<SessionMetadataRecord> {
419    if let Some(record) = read_session_metadata(session)? {
420        return Ok(record);
421    }
422    rebuild_session_metadata_from_jsonl(session)
423}
424
425fn session_metadata_summary(session: Session) -> anyhow::Result<SessionMetadataSummary> {
426    let record = session_metadata_for_listing(&session)?;
427    let latest_activity_time = record.activity_time(&session);
428    Ok(SessionMetadataSummary {
429        session,
430        latest_activity_time,
431        latest_title: record.latest_title,
432        session_path: record.session_path,
433    })
434}
435#[derive(Debug, Clone, PartialEq, Eq)]
436pub(crate) struct CompactionCheckpoint {
437    pub(crate) summary: String,
438    pub(crate) provider: String,
439    pub(crate) model: String,
440    pub(crate) cutoff_event_count: usize,
441    pub(crate) event_index: usize,
442}
443pub(crate) fn update_session_metadata_after_append_batch(
444    session: &Session,
445    previous: Option<SessionMetadataRecord>,
446    events: &[SessionEvent],
447) -> anyhow::Result<()> {
448    let mut record = match previous {
449        Some(record) => record,
450        None => SessionMetadataRecord::empty(session, jsonl_marker(session)?, false),
451    };
452    for event in events {
453        record.apply_event(event);
454    }
455    let marker = jsonl_marker(session)?;
456    record.jsonl_len = marker.len;
457    record.jsonl_modified_ns = marker.modified_ns;
458    write_session_metadata(session, &record)
459}
460
461pub(crate) fn latest_valid_compaction_checkpoint(
462    session_id: &str,
463    events: &[SessionEvent],
464) -> (Option<CompactionCheckpoint>, Vec<String>) {
465    let mut diagnostics = Vec::new();
466    for (event_index, event) in events.iter().enumerate().rev() {
467        if event.kind() != Some(SessionEventKind::Compaction) {
468            continue;
469        }
470        match validate_compaction_checkpoint(session_id, event, event_index, events.len()) {
471            Ok(checkpoint) => return (Some(checkpoint), diagnostics),
472            Err(message) => diagnostics.push(message),
473        }
474    }
475    (None, diagnostics)
476}
477
478fn validate_compaction_checkpoint(
479    session_id: &str,
480    event: &SessionEvent,
481    event_index: usize,
482    total_event_count: usize,
483) -> Result<CompactionCheckpoint, String> {
484    if event.session_id != session_id {
485        return Err(format!(
486            "ignored malformed compaction checkpoint at event_index={event_index}: session_id_mismatch"
487        ));
488    }
489    let schema_version = event
490        .payload
491        .get("schema_version")
492        .and_then(Value::as_u64)
493        .ok_or_else(|| {
494            format!(
495                "ignored malformed compaction checkpoint at event_index={event_index}: missing_schema_version"
496            )
497        })?;
498    if schema_version != COMPACTION_SCHEMA_VERSION {
499        return Err(format!(
500            "ignored malformed compaction checkpoint at event_index={event_index}: unsupported_schema_version"
501        ));
502    }
503    let summary = event
504        .payload
505        .get("summary")
506        .and_then(Value::as_str)
507        .and_then(sanitize_compaction_summary)
508        .ok_or_else(|| {
509            format!(
510                "ignored malformed compaction checkpoint at event_index={event_index}: empty_summary"
511            )
512        })?;
513    let provider = required_non_blank_payload_string(event, "provider", event_index)?;
514    let model = required_non_blank_payload_string(event, "model", event_index)?;
515    let cutoff_event_count = event
516        .payload
517        .get("cutoff_event_count")
518        .and_then(Value::as_u64)
519        .and_then(|value| usize::try_from(value).ok())
520        .ok_or_else(|| {
521            format!(
522                "ignored malformed compaction checkpoint at event_index={event_index}: missing_cutoff_event_count"
523            )
524        })?;
525    if cutoff_event_count > event_index || cutoff_event_count > total_event_count {
526        return Err(format!(
527            "ignored malformed compaction checkpoint at event_index={event_index}: cutoff_event_count_out_of_bounds"
528        ));
529    }
530    Ok(CompactionCheckpoint {
531        summary,
532        provider,
533        model,
534        cutoff_event_count,
535        event_index,
536    })
537}
538
539fn required_non_blank_payload_string(
540    event: &SessionEvent,
541    key: &str,
542    event_index: usize,
543) -> Result<String, String> {
544    event.payload.get(key).and_then(Value::as_str).map(str::trim)
545        .filter(|value| !value.is_empty())
546        .map(ToString::to_string)
547        .ok_or_else(|| format!("ignored malformed compaction checkpoint at event_index={event_index}: missing_{key}"))
548}
549
550#[derive(Debug, Clone, Default, PartialEq, Eq)]
551pub(crate) struct SessionTitleMetadata {
552    pub(crate) latest_title: Option<String>,
553    pub(crate) user_input_count: usize,
554    pub(crate) first_user_input_text: Option<String>,
555}
556
557pub(crate) fn session_title_metadata(session: &Session) -> anyhow::Result<SessionTitleMetadata> {
558    if let Some(record) = read_session_metadata(session)? {
559        return Ok(SessionTitleMetadata {
560            latest_title: record.latest_title,
561            user_input_count: record.user_input_count,
562            first_user_input_text: record.first_user_input_text,
563        });
564    }
565    let mut metadata = SessionTitleMetadata::default();
566    session
567        .visit_events_tolerant_bounded(
568            MAX_METADATA_VISIT_LINES,
569            MAX_METADATA_VISIT_BYTES,
570            |event| match event.kind() {
571                Some(SessionEventKind::SessionTitle) => {
572                    if let Some(title) = event
573                        .payload
574                        .get("title")
575                        .and_then(Value::as_str)
576                        .and_then(sanitize_session_title)
577                    {
578                        metadata.latest_title = Some(title);
579                    }
580                }
581                Some(SessionEventKind::UserInput) => {
582                    metadata.user_input_count = metadata.user_input_count.saturating_add(1);
583                    if metadata.first_user_input_text.is_none()
584                        && let Some(text) = event.payload.get("text").and_then(Value::as_str)
585                        && !text.trim().is_empty()
586                    {
587                        metadata.first_user_input_text = Some(text.to_string());
588                    }
589                }
590                _ => {}
591            },
592        )
593        .inspect_err(|error| {
594            report_session_diagnostic(SessionDiagnosticOperation::Title, session.path(), error);
595        })?;
596    Ok(metadata)
597}
598
599pub fn latest_session_title(session: &Session) -> anyhow::Result<Option<String>> {
600    Ok(session_title_metadata(session)?.latest_title)
601}
602
603pub fn session_user_input_count(session: &Session) -> anyhow::Result<usize> {
604    Ok(session_title_metadata(session)?.user_input_count)
605}
606
607#[cfg(test)]
608mod tests {
609    use super::super::read::latest_valid_event_timestamp_streaming;
610    use super::super::write::{
611        SESSION_TITLE_EVENT, record_session_compaction, record_session_event, record_session_title,
612    };
613    use super::*;
614    use chrono::{TimeZone, Timelike};
615    use serde_json::json;
616    use tempfile::TempDir;
617
618    #[test]
619    fn compaction_checkpoint_appends_round_trip_and_redacts_summary() {
620        let temp = TempDir::new().unwrap();
621        let manager = SessionManager::new(temp.path().join("sessions"));
622        let session = manager.create().unwrap();
623        record_session_compaction(
624            &session,
625            temp.path(),
626            "summary with sk-testSecret123456",
627            "provider-a",
628            "model-a",
629            0,
630        )
631        .unwrap();
632
633        let events = session.read_events().unwrap();
634        assert_eq!(events.len(), 1);
635        assert_eq!(events[0].event_type, "compaction");
636        assert_eq!(
637            events[0].payload["schema_version"],
638            COMPACTION_SCHEMA_VERSION
639        );
640        assert_eq!(events[0].payload["provider"], "provider-a");
641        assert_eq!(events[0].payload["model"], "model-a");
642        assert_eq!(events[0].payload["cutoff_event_count"], 0);
643        assert!(
644            !events[0].payload["summary"]
645                .to_string()
646                .contains("sk-testSecret")
647        );
648
649        let (checkpoint, diagnostics) = latest_valid_compaction_checkpoint(session.id(), &events);
650        assert!(diagnostics.is_empty(), "{diagnostics:?}");
651        let checkpoint = checkpoint.unwrap();
652        assert_eq!(checkpoint.provider, "provider-a");
653        assert_eq!(checkpoint.model, "model-a");
654        assert_eq!(checkpoint.cutoff_event_count, 0);
655        assert_eq!(checkpoint.event_index, 0);
656    }
657
658    #[test]
659    fn compaction_checkpoint_rejects_empty_summary() {
660        let temp = TempDir::new().unwrap();
661        let manager = SessionManager::new(temp.path().join("sessions"));
662        let session = manager.create().unwrap();
663
664        let error = record_session_compaction(&session, temp.path(), " \n\t ", "p", "m", 0)
665            .unwrap_err()
666            .to_string();
667
668        assert!(error.contains("empty"), "{error}");
669        assert!(session.read_events().unwrap().is_empty());
670    }
671
672    #[test]
673    fn latest_valid_compaction_falls_back_after_malformed_or_mismatched_latest() {
674        let temp = TempDir::new().unwrap();
675        let manager = SessionManager::new(temp.path().join("sessions"));
676        let session = manager.open("safe").unwrap();
677        let valid = SessionEvent::new_kind(
678            SessionEventKind::Compaction,
679            session.id().to_string(),
680            temp.path().to_path_buf(),
681            json!({
682                "schema_version": COMPACTION_SCHEMA_VERSION,
683                "summary":"valid summary",
684                "provider":"p",
685                "model":"m",
686                "cutoff_event_count": 0,
687            }),
688        );
689        let malformed = SessionEvent::new_kind(
690            SessionEventKind::Compaction,
691            session.id().to_string(),
692            temp.path().to_path_buf(),
693            json!({"schema_version": COMPACTION_SCHEMA_VERSION, "summary":" "}),
694        );
695        let mismatched = SessionEvent::new_kind(
696            SessionEventKind::Compaction,
697            "other".to_string(),
698            temp.path().to_path_buf(),
699            json!({
700                "schema_version": COMPACTION_SCHEMA_VERSION,
701                "summary":"wrong session",
702                "provider":"p",
703                "model":"m",
704                "cutoff_event_count": 2,
705            }),
706        );
707
708        let (checkpoint, diagnostics) = latest_valid_compaction_checkpoint(
709            session.id(),
710            &[valid.clone(), malformed, mismatched],
711        );
712
713        assert_eq!(checkpoint.unwrap().summary, "valid summary");
714        assert_eq!(diagnostics.len(), 2);
715        assert!(
716            diagnostics
717                .iter()
718                .any(|message| message.contains("session_id_mismatch"))
719        );
720        assert!(
721            diagnostics
722                .iter()
723                .any(|message| message.contains("empty_summary"))
724        );
725        assert!(!diagnostics.join("\n").contains("wrong session"));
726    }
727
728    #[test]
729    fn session_titles_sanitize_round_trip_and_latest_wins() {
730        let temp = TempDir::new().unwrap();
731        let manager = SessionManager::new(temp.path().join("sessions"));
732        let session = manager.create().unwrap();
733
734        assert_eq!(session_title_metadata(&session).unwrap().latest_title, None);
735        assert_eq!(
736            sanitize_session_title(" \"Fix parser\"\nnow "),
737            Some("Fix parser now".to_string())
738        );
739        assert_eq!(sanitize_session_title("\n\t\r"), None);
740        assert_eq!(
741            sanitize_session_title(&"é".repeat(60))
742                .unwrap()
743                .chars()
744                .count(),
745            50
746        );
747
748        record_session_title(
749            &session,
750            temp.path(),
751            "First title",
752            "provider-a",
753            "model-a",
754        )
755        .unwrap();
756        record_session_title(
757            &session,
758            temp.path(),
759            "'Second title'",
760            "provider-b",
761            "model-b",
762        )
763        .unwrap();
764
765        let events = session.read_events().unwrap();
766        assert_eq!(events.len(), 2);
767        assert_eq!(events[0].event_type, SESSION_TITLE_EVENT);
768        assert_eq!(events[0].payload["title"], "First title");
769        assert_eq!(events[0].payload["provider"], "provider-a");
770        assert_eq!(events[0].payload["model"], "model-a");
771        assert!(events[0].payload.get("access_token").is_none());
772        assert_eq!(
773            session_title_metadata(&session)
774                .unwrap()
775                .latest_title
776                .as_deref(),
777            Some("Second title")
778        );
779    }
780    #[test]
781    fn session_user_input_count_supports_first_message_trigger() {
782        let temp = TempDir::new().unwrap();
783        let manager = SessionManager::new(temp.path().join("sessions"));
784        let session = manager.create().unwrap();
785        assert_eq!(session_user_input_count(&session).unwrap(), 0);
786        record_session_event(
787            Some(&session),
788            temp.path(),
789            "user_input",
790            json!({"text":"one"}),
791        )
792        .unwrap();
793        assert_eq!(session_user_input_count(&session).unwrap(), 1);
794        record_session_event(
795            Some(&session),
796            temp.path(),
797            "user_input",
798            json!({"text":"two"}),
799        )
800        .unwrap();
801        assert_eq!(session_user_input_count(&session).unwrap(), 2);
802    }
803
804    #[test]
805    fn session_title_metadata_uses_first_persisted_non_empty_text() {
806        let temp = TempDir::new().unwrap();
807        let manager = SessionManager::new(temp.path().join("sessions"));
808        let session = manager.create().unwrap();
809        record_session_event(
810            Some(&session),
811            temp.path(),
812            "diagnostic",
813            json!({"text":"ignore"}),
814        )
815        .unwrap();
816        record_session_event(
817            Some(&session),
818            temp.path(),
819            "user_input",
820            json!({"text":"   "}),
821        )
822        .unwrap();
823        record_session_event(
824            Some(&session),
825            temp.path(),
826            "user_input",
827            json!({"text":"first durable prompt"}),
828        )
829        .unwrap();
830        record_session_event(
831            Some(&session),
832            temp.path(),
833            "user_input",
834            json!({"text":"second prompt"}),
835        )
836        .unwrap();
837
838        assert_eq!(
839            session_title_metadata(&session)
840                .unwrap()
841                .first_user_input_text
842                .as_deref(),
843            Some("first durable prompt")
844        );
845    }
846
847    #[test]
848    fn session_title_metadata_rejects_oversized_jsonl() {
849        let temp = TempDir::new().unwrap();
850        let manager = SessionManager::new(temp.path().join("sessions"));
851        let session = manager.open("oversized-title").unwrap();
852        fs::create_dir_all(session.path().parent().unwrap()).unwrap();
853        fs::OpenOptions::new()
854            .create(true)
855            .write(true)
856            .truncate(true)
857            .open(session.path())
858            .unwrap()
859            .set_len((MAX_METADATA_VISIT_BYTES as u64) + 1)
860            .unwrap();
861        crate::sessions::store::secure_test_session_root(session.path().parent().unwrap());
862
863        let error = session_title_metadata(&session).unwrap_err().to_string();
864
865        assert!(error.contains("tolerant read limit exceeded"), "{error}");
866        assert!(error.contains("bytes"), "{error}");
867    }
868
869    #[test]
870    fn metadata_rebuild_rejects_oversized_jsonl() {
871        let temp = TempDir::new().unwrap();
872        let manager = SessionManager::new(temp.path().join("sessions"));
873        let session = manager.open("oversized-metadata").unwrap();
874        fs::create_dir_all(session.path().parent().unwrap()).unwrap();
875        fs::OpenOptions::new()
876            .create(true)
877            .write(true)
878            .truncate(true)
879            .open(session.path())
880            .unwrap()
881            .set_len((MAX_METADATA_VISIT_BYTES as u64) + 1)
882            .unwrap();
883        crate::sessions::store::secure_test_session_root(session.path().parent().unwrap());
884
885        let error = session_metadata_for_listing(&session)
886            .unwrap_err()
887            .to_string();
888
889        assert!(error.contains("tolerant read limit exceeded"), "{error}");
890        assert!(error.contains("bytes"), "{error}");
891    }
892
893    #[test]
894    fn session_title_and_user_count_tolerate_malformed_lines() {
895        let temp = TempDir::new().unwrap();
896        let manager = SessionManager::new(temp.path().join("sessions"));
897        let session = manager.open("safe").unwrap();
898        let first_title = SessionEvent::new_kind(
899            SessionEventKind::SessionTitle,
900            session.id().to_string(),
901            temp.path().to_path_buf(),
902            json!({"title":"First title", "provider":"p", "model":"m"}),
903        );
904        let user_input = SessionEvent::new_kind(
905            SessionEventKind::UserInput,
906            session.id().to_string(),
907            temp.path().to_path_buf(),
908            json!({"text":"one"}),
909        );
910        let latest_title = SessionEvent::new_kind(
911            SessionEventKind::SessionTitle,
912            session.id().to_string(),
913            temp.path().to_path_buf(),
914            json!({"title":"Latest title", "provider":"p", "model":"m"}),
915        );
916        fs::create_dir_all(session.path().parent().unwrap()).unwrap();
917        fs::write(
918            session.path(),
919            format!(
920                "{}\nnot json\n{}\n{}\n",
921                serde_json::to_string(&first_title).unwrap(),
922                serde_json::to_string(&user_input).unwrap(),
923                serde_json::to_string(&latest_title).unwrap()
924            ),
925        )
926        .unwrap();
927        crate::sessions::store::secure_test_session_root(session.path().parent().unwrap());
928
929        assert!(session.read_events().is_err());
930        let metadata = session_title_metadata(&session).unwrap();
931        assert_eq!(metadata.latest_title.as_deref(), Some("Latest title"));
932        assert_eq!(metadata.user_input_count, 1);
933        assert_eq!(metadata.first_user_input_text.as_deref(), Some("one"));
934        assert_eq!(
935            session_title_metadata(&session)
936                .unwrap()
937                .latest_title
938                .as_deref(),
939            Some("Latest title")
940        );
941        assert_eq!(session_user_input_count(&session).unwrap(), 1);
942    }
943
944    #[test]
945    fn continue_uses_most_recent_session_by_latest_event_timestamp() {
946        let temp = TempDir::new().unwrap();
947        let manager = SessionManager::new(temp.path().join("sessions"));
948        fs::create_dir_all(temp.path().join("sessions")).unwrap();
949
950        let z_old = manager.open("zzzz").unwrap();
951        let a_new = manager.open("aaaa").unwrap();
952
953        let z_old_event = event_at(&z_old, temp.path(), 2024, 1, 3);
954        fs::write(
955            z_old.path(),
956            format!("{}\n", serde_json::to_string(&z_old_event).unwrap()),
957        )
958        .unwrap();
959
960        let a_latest_outside_tail = event_at(&a_new, temp.path(), 2024, 1, 4);
961        let a_tail_older = event_at(&a_new, temp.path(), 2024, 1, 2);
962        fs::write(
963            a_new.path(),
964            format!(
965                "{}\n{}\n{}\n",
966                serde_json::to_string(&a_latest_outside_tail).unwrap(),
967                "not-json".repeat(300 * 1024),
968                serde_json::to_string(&a_tail_older).unwrap()
969            ),
970        )
971        .unwrap();
972        crate::sessions::store::secure_test_session_root(temp.path().join("sessions").as_path());
973
974        let sessions = manager.list().unwrap();
975        assert_eq!(sessions.last().unwrap().id(), "aaaa");
976        assert_eq!(manager.most_recent().unwrap().unwrap().id(), "aaaa");
977    }
978
979    #[test]
980    fn session_list_activity_streams_latest_valid_timestamp_without_event_vec() {
981        let temp = TempDir::new().unwrap();
982        let manager = SessionManager::new(temp.path().join("sessions"));
983        let older = manager.open("older").unwrap();
984        let newer = manager.open("newer").unwrap();
985        let older_event = event_at(&older, temp.path(), 2024, 1, 1);
986        let newer_event = event_at(&newer, temp.path(), 2024, 1, 2);
987        fs::create_dir_all(newer.path().parent().unwrap()).unwrap();
988        fs::write(
989            older.path(),
990            format!("{}\n", serde_json::to_string(&older_event).unwrap()),
991        )
992        .unwrap();
993        fs::write(
994            newer.path(),
995            format!(
996                "not json\n{}\n",
997                serde_json::to_string(&newer_event).unwrap()
998            ),
999        )
1000        .unwrap();
1001        crate::sessions::store::secure_test_session_root(newer.path().parent().unwrap());
1002
1003        let sessions = manager.list().unwrap();
1004
1005        assert_eq!(sessions.last().unwrap().id(), "newer");
1006        assert_eq!(
1007            latest_valid_event_timestamp_streaming(&newer),
1008            Some(SystemTime::from(newer_event.timestamp))
1009        );
1010    }
1011
1012    #[test]
1013    fn session_activity_streaming_uses_latest_valid_event_across_full_file() {
1014        let temp = TempDir::new().unwrap();
1015        let manager = SessionManager::new(temp.path().join("sessions"));
1016        let session = manager.open("tail-session").unwrap();
1017        let old = event_at(&session, temp.path(), 2024, 1, 1);
1018        let recent = event_at(&session, temp.path(), 2024, 1, 2);
1019        fs::create_dir_all(session.path().parent().unwrap()).unwrap();
1020        fs::write(
1021            session.path(),
1022            format!(
1023                "{}\n{}\nnot json\n",
1024                serde_json::to_string(&old).unwrap(),
1025                serde_json::to_string(&recent).unwrap()
1026            ),
1027        )
1028        .unwrap();
1029        crate::sessions::store::secure_test_session_root(session.path().parent().unwrap());
1030
1031        assert_eq!(
1032            latest_valid_event_timestamp_streaming(&session),
1033            Some(SystemTime::from(recent.timestamp))
1034        );
1035    }
1036
1037    #[test]
1038    fn record_session_event_propagates_append_failure() {
1039        let temp = TempDir::new().unwrap();
1040        let invalid_session =
1041            Session::unchecked_for_test("../unsafe".to_string(), temp.path().join("unsafe.jsonl"));
1042
1043        let error = record_session_event(
1044            Some(&invalid_session),
1045            temp.path(),
1046            "diagnostic",
1047            json!({"message":"persist me"}),
1048        )
1049        .unwrap_err();
1050
1051        assert!(error.to_string().contains("must not contain '..'"));
1052        assert!(!invalid_session.path().exists());
1053    }
1054
1055    #[test]
1056    fn session_metadata_sidecar_excludes_raw_transcript_and_secrets() {
1057        let temp = TempDir::new().unwrap();
1058        let manager = SessionManager::new(temp.path().join("sessions"));
1059        let session = manager.create().unwrap();
1060        session
1061            .append(&SessionEvent::new(
1062                "user_input",
1063                session.id().to_string(),
1064                temp.path().to_path_buf(),
1065                json!({"text":"raw prompt secret sk-testSecret123456", "api_key":"abc123"}),
1066            ))
1067            .unwrap();
1068        session
1069            .append(&SessionEvent::new(
1070                "tool_result",
1071                session.id().to_string(),
1072                temp.path().to_path_buf(),
1073                json!({"result":{"content":"tool output bearer secret-token"}}),
1074            ))
1075            .unwrap();
1076        record_session_title(
1077            &session,
1078            temp.path(),
1079            "Safe Indexed Title",
1080            "provider-a",
1081            "model-a",
1082        )
1083        .unwrap();
1084
1085        let sidecar = fs::read_to_string(metadata_path_for_session(&session)).unwrap();
1086
1087        assert!(sidecar.contains("Safe Indexed Title"));
1088        assert!(sidecar.contains("session_path"));
1089        for forbidden in [
1090            "raw prompt secret",
1091            "sk-testSecret123456",
1092            "api_key",
1093            "abc123",
1094            "tool output",
1095            "bearer secret-token",
1096            "provider-a",
1097            "model-a",
1098            "access_token",
1099            "account_id",
1100        ] {
1101            assert!(!sidecar.contains(forbidden), "sidecar leaked {forbidden}");
1102        }
1103    }
1104
1105    #[test]
1106    fn metadata_indexes_first_session_path_and_keeps_legacy_missing_path_none() {
1107        let temp = TempDir::new().unwrap();
1108        let manager = SessionManager::new(temp.path().join("sessions"));
1109        let scoped = temp.path().join("scoped");
1110        fs::create_dir(&scoped).unwrap();
1111        let session = manager.open("scoped-session").unwrap();
1112        session
1113            .append(&SessionEvent::new(
1114                "diagnostic",
1115                session.id().to_string(),
1116                scoped.clone(),
1117                json!({}),
1118            ))
1119            .unwrap();
1120
1121        let record: SessionMetadataRecord =
1122            serde_json::from_slice(&fs::read(metadata_path_for_session(&session)).unwrap())
1123                .unwrap();
1124        assert_eq!(record.schema_version, SESSION_METADATA_SCHEMA_VERSION);
1125        assert_eq!(record.session_path.as_deref(), Some(scoped.as_path()));
1126        let summary = manager
1127            .list_metadata_summaries()
1128            .unwrap()
1129            .into_iter()
1130            .find(|summary| summary.session.id() == session.id())
1131            .unwrap();
1132        assert_eq!(summary.session_path.as_deref(), Some(scoped.as_path()));
1133
1134        let legacy = manager.open("legacy-session").unwrap();
1135        let mut legacy_event = SessionEvent::new(
1136            "diagnostic",
1137            legacy.id().to_string(),
1138            temp.path().to_path_buf(),
1139            json!({}),
1140        );
1141        legacy_event.session_path = None;
1142        fs::create_dir_all(legacy.path().parent().unwrap()).unwrap();
1143        fs::write(
1144            legacy.path(),
1145            format!("{}\n", serde_json::to_string(&legacy_event).unwrap()),
1146        )
1147        .unwrap();
1148        crate::sessions::store::secure_test_session_root(legacy.path().parent().unwrap());
1149
1150        let legacy_record = session_metadata_for_listing(&legacy).unwrap();
1151        assert_eq!(legacy_record.session_path, None);
1152    }
1153
1154    #[test]
1155    fn most_recent_uses_valid_sidecar_without_parsing_jsonl() {
1156        let temp = TempDir::new().unwrap();
1157        let manager = SessionManager::new(temp.path().join("sessions"));
1158        let session = manager.open("indexed-session").unwrap();
1159        let event = event_at(&session, temp.path(), 2024, 1, 5);
1160        fs::create_dir_all(session.path().parent().unwrap()).unwrap();
1161        fs::write(
1162            session.path(),
1163            format!("{}\n", serde_json::to_string(&event).unwrap()),
1164        )
1165        .unwrap();
1166        crate::sessions::store::secure_test_session_root(session.path().parent().unwrap());
1167        let mut record = rebuild_session_metadata_from_jsonl(&session).unwrap();
1168        fs::write(
1169            session.path(),
1170            "not valid jsonl but marker matches sidecar\n",
1171        )
1172        .unwrap();
1173        let marker = jsonl_marker(&session).unwrap();
1174        record.jsonl_len = marker.len;
1175        record.jsonl_modified_ns = marker.modified_ns;
1176        write_session_metadata(&session, &record).unwrap();
1177
1178        let latest = manager.most_recent().unwrap().unwrap();
1179
1180        assert_eq!(latest.id(), session.id());
1181    }
1182
1183    #[test]
1184    fn corrupt_sidecar_falls_back_to_jsonl_and_rebuilds() {
1185        let temp = TempDir::new().unwrap();
1186        let manager = SessionManager::new(temp.path().join("sessions"));
1187        let session = manager.open("fallback-session").unwrap();
1188        let event = event_at(&session, temp.path(), 2024, 2, 1);
1189        fs::create_dir_all(session.path().parent().unwrap()).unwrap();
1190        fs::write(
1191            session.path(),
1192            format!("{}\n", serde_json::to_string(&event).unwrap()),
1193        )
1194        .unwrap();
1195        fs::write(metadata_path_for_session(&session), "{not json").unwrap();
1196        crate::sessions::store::secure_test_session_root(session.path().parent().unwrap());
1197
1198        let sessions = manager.list().unwrap();
1199        let rebuilt: SessionMetadataRecord =
1200            serde_json::from_slice(&fs::read(metadata_path_for_session(&session)).unwrap())
1201                .unwrap();
1202
1203        assert_eq!(sessions.len(), 1);
1204        assert_eq!(sessions[0].id(), session.id());
1205        assert_eq!(rebuilt.session_id, session.id());
1206        assert_eq!(rebuilt.latest_activity_timestamp, Some(event.timestamp));
1207    }
1208
1209    #[test]
1210    fn list_metadata_report_surfaces_invalid_session_entries_without_failing() {
1211        let temp = TempDir::new().unwrap();
1212        let manager = SessionManager::new(temp.path().join("sessions"));
1213        fs::create_dir_all(temp.path().join("sessions")).unwrap();
1214        crate::sessions::store::secure_test_session_root(temp.path().join("sessions").as_path());
1215        let valid = manager.open("valid-session").unwrap();
1216        append_event_at(&valid, temp.path(), 2024, 2, 1);
1217        fs::write(temp.path().join("sessions/bad.name.jsonl"), "{}").unwrap();
1218
1219        let report = manager.list_metadata_report().unwrap();
1220
1221        assert_eq!(report.summaries.len(), 1);
1222        assert_eq!(report.summaries[0].session.id(), "valid-session");
1223        assert_eq!(report.diagnostics.len(), 1);
1224        assert!(report.diagnostics[0].message.contains("invalid session"));
1225    }
1226
1227    #[test]
1228    fn append_without_metadata_writes_incomplete_sidecar_without_jsonl_rebuild() {
1229        let temp = TempDir::new().unwrap();
1230        let manager = SessionManager::new(temp.path().join("sessions"));
1231        let session = manager.open("missing-sidecar").unwrap();
1232        let old_title = SessionEvent::new_kind(
1233            SessionEventKind::SessionTitle,
1234            session.id().to_string(),
1235            temp.path().to_path_buf(),
1236            json!({"title":"Old Title", "provider":"p", "model":"m"}),
1237        );
1238        fs::create_dir_all(session.path().parent().unwrap()).unwrap();
1239        fs::write(
1240            session.path(),
1241            format!("{}\n", serde_json::to_string(&old_title).unwrap()),
1242        )
1243        .unwrap();
1244        crate::sessions::store::secure_test_session_root(session.path().parent().unwrap());
1245
1246        session
1247            .append(&SessionEvent::new(
1248                "diagnostic",
1249                session.id().to_string(),
1250                temp.path().to_path_buf(),
1251                json!({"message":"new append"}),
1252            ))
1253            .unwrap();
1254
1255        let append_record: SessionMetadataRecord =
1256            serde_json::from_slice(&fs::read(metadata_path_for_session(&session)).unwrap())
1257                .unwrap();
1258        assert!(!append_record.complete);
1259        assert_eq!(append_record.latest_title, None);
1260
1261        let summaries = manager.list_metadata_summaries().unwrap();
1262        let rebuilt: SessionMetadataRecord =
1263            serde_json::from_slice(&fs::read(metadata_path_for_session(&session)).unwrap())
1264                .unwrap();
1265        assert_eq!(summaries[0].latest_title.as_deref(), Some("Old Title"));
1266        assert!(rebuilt.complete);
1267    }
1268
1269    #[test]
1270    fn metadata_rebuild_does_not_publish_stale_complete_under_append() {
1271        let temp = TempDir::new().unwrap();
1272        let manager = SessionManager::new(temp.path().join("sessions"));
1273        for iteration in 0..32 {
1274            let session = manager.open(format!("rebuild-race-{iteration}")).unwrap();
1275            record_session_event(
1276                Some(&session),
1277                temp.path(),
1278                "user_input",
1279                json!({"text":"seed"}),
1280            )
1281            .unwrap();
1282            fs::remove_file(metadata_path_for_session(&session)).unwrap();
1283
1284            let append_session = session.clone();
1285            let cwd = temp.path().to_path_buf();
1286            let handle = std::thread::spawn(move || {
1287                std::thread::sleep(std::time::Duration::from_micros(50 * iteration));
1288                append_session
1289                    .append(&SessionEvent::new(
1290                        "user_input",
1291                        append_session.id().to_string(),
1292                        cwd,
1293                        json!({"text":"concurrent"}),
1294                    ))
1295                    .unwrap();
1296            });
1297
1298            let record = session_metadata_for_listing(&session).unwrap();
1299            handle.join().unwrap();
1300
1301            let final_marker = jsonl_marker(&session).unwrap();
1302            let actual_user_inputs = session
1303                .read_events_tolerant()
1304                .unwrap()
1305                .events
1306                .iter()
1307                .filter(|event| event.kind() == Some(SessionEventKind::UserInput))
1308                .count();
1309            if record.complete
1310                && record.jsonl_len == final_marker.len
1311                && record.jsonl_modified_ns == final_marker.modified_ns
1312            {
1313                assert_eq!(record.user_input_count, actual_user_inputs);
1314            }
1315        }
1316    }
1317
1318    #[test]
1319    fn metadata_sidecar_write_failure_warns_but_append_succeeds() {
1320        let temp = TempDir::new().unwrap();
1321        let manager = SessionManager::new(temp.path().join("sessions"));
1322        let session = manager.open("metadata-fails").unwrap();
1323        fs::create_dir_all(metadata_path_for_session(&session)).unwrap();
1324        crate::sessions::store::secure_test_session_root(session.path().parent().unwrap());
1325
1326        session
1327            .append(&SessionEvent::new(
1328                "user_input",
1329                session.id().to_string(),
1330                temp.path().to_path_buf(),
1331                json!({"text":"durable"}),
1332            ))
1333            .unwrap();
1334
1335        let events = session.read_events().unwrap();
1336        assert_eq!(events.len(), 1);
1337        assert_eq!(events[0].payload["text"], "durable");
1338        assert!(metadata_path_for_session(&session).is_dir());
1339    }
1340
1341    #[test]
1342    fn concurrent_session_appends_update_metadata_without_losing_latest() {
1343        let temp = TempDir::new().unwrap();
1344        let manager = SessionManager::new(temp.path().join("sessions"));
1345        let session = manager.create().unwrap();
1346        let mut handles = Vec::new();
1347        for index in 0..16 {
1348            let session = session.clone();
1349            let cwd = temp.path().to_path_buf();
1350            handles.push(std::thread::spawn(move || {
1351                let mut event = SessionEvent::new(
1352                    "diagnostic",
1353                    session.id().to_string(),
1354                    cwd,
1355                    json!({"index": index}),
1356                );
1357                event.timestamp = Utc.with_ymd_and_hms(2024, 3, 1, 0, 0, index).unwrap();
1358                session.append(&event).unwrap();
1359            }));
1360        }
1361        for handle in handles {
1362            handle.join().unwrap();
1363        }
1364
1365        let record: SessionMetadataRecord =
1366            serde_json::from_slice(&fs::read(metadata_path_for_session(&session)).unwrap())
1367                .unwrap();
1368        let tolerant = session.read_events_tolerant().unwrap();
1369
1370        assert!(tolerant.diagnostics.is_empty());
1371        assert_eq!(tolerant.events.len(), 16);
1372        assert_eq!(record.latest_activity_timestamp.unwrap().second(), 15);
1373    }
1374
1375    fn append_event_at(session: &Session, cwd: &Path, year: i32, month: u32, day: u32) {
1376        session
1377            .append(&event_at(session, cwd, year, month, day))
1378            .unwrap();
1379    }
1380
1381    fn event_at(session: &Session, cwd: &Path, year: i32, month: u32, day: u32) -> SessionEvent {
1382        let mut event = SessionEvent::new(
1383            "diagnostic",
1384            session.id().to_string(),
1385            cwd.to_path_buf(),
1386            json!({}),
1387        );
1388        event.timestamp = Utc.with_ymd_and_hms(year, month, day, 0, 0, 0).unwrap();
1389        event
1390    }
1391}