pub mod nag;
pub mod parsers;
pub mod transcript_paths;
use std::path::{Path, PathBuf};
use std::time::Instant;
use rusqlite::OptionalExtension;
use serde::{Deserialize, Serialize};
pub use transcript_paths::{HostKind, resolve_transcript};
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct RecoverReport {
pub transcript_path: Option<PathBuf>,
pub host_kind: HostKind,
pub elapsed_ms: u64,
pub elapsed_ms_resolve_path: u64,
pub elapsed_ms_dedup_query: u64,
pub elapsed_ms_parse: u64,
pub elapsed_ms_writes: u64,
pub lines_total: u32,
pub lines_atomised: u32,
pub lines_skipped_dedup: u32,
pub lines_skipped_limit: u32,
pub memories_created: Vec<String>,
pub errors: Vec<String>,
pub schema_version_at_run: i64,
pub fast_path_hit: bool,
}
impl RecoverReport {
#[must_use]
pub fn new(host_kind: HostKind, schema_version: i64) -> Self {
Self {
transcript_path: None,
host_kind,
elapsed_ms: 0,
elapsed_ms_resolve_path: 0,
elapsed_ms_dedup_query: 0,
elapsed_ms_parse: 0,
elapsed_ms_writes: 0,
lines_total: 0,
lines_atomised: 0,
lines_skipped_dedup: 0,
lines_skipped_limit: 0,
memories_created: Vec::new(),
errors: Vec::new(),
schema_version_at_run: schema_version,
fast_path_hit: false,
}
}
}
pub struct RecoverTimer {
overall_start: Instant,
phase_start: Instant,
}
impl Default for RecoverTimer {
fn default() -> Self {
Self::new()
}
}
impl RecoverTimer {
#[must_use]
pub fn new() -> Self {
let now = Instant::now();
Self {
overall_start: now,
phase_start: now,
}
}
pub fn phase_lap(&mut self) -> u64 {
let now = Instant::now();
let ms =
u64::try_from(now.duration_since(self.phase_start).as_millis()).unwrap_or(u64::MAX);
self.phase_start = now;
ms
}
#[must_use]
pub fn overall_ms(&self) -> u64 {
u64::try_from(self.overall_start.elapsed().as_millis()).unwrap_or(u64::MAX)
}
}
pub const DEFAULT_RECOVER_LIMIT: usize = 100;
pub const QUIET_MEMORY_ID_PREVIEW_CAP: usize = 10;
#[derive(Debug, Clone)]
pub struct RecoverOpts {
pub host: HostKind,
pub transcript_override: Option<PathBuf>,
pub since_iso: Option<String>,
pub namespace: Option<String>,
pub limit: usize,
pub dry_run: bool,
pub quiet: bool,
pub agent_id: String,
}
impl RecoverOpts {
#[must_use]
pub fn for_session_start_hook(host: HostKind, agent_id: String) -> Self {
Self {
host,
transcript_override: None,
since_iso: None,
namespace: None,
limit: DEFAULT_RECOVER_LIMIT,
dry_run: false,
quiet: true,
agent_id,
}
}
}
pub fn recover_from_transcript(
db_path: &Path,
opts: &RecoverOpts,
) -> Result<RecoverReport, RecoverError> {
use parsers::TranscriptParser;
use parsers::claude_code_jsonl::ClaudeCodeJsonlParser;
let mut timer = RecoverTimer::new();
let schema_version = crate::storage::migrations::current_schema_version();
let mut report = RecoverReport::new(opts.host, schema_version);
let conn = crate::storage::open(db_path).map_err(|e| RecoverError::DbOpen(e.to_string()))?;
let path = match opts.transcript_override.clone() {
Some(p) => Some(p),
None => {
let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
match resolve_transcript(opts.host, &cwd) {
Ok(p) => p,
Err(e) => {
report.errors.push(format!("path resolve failed: {e}"));
None
}
}
}
};
let Some(path) = path else {
report.elapsed_ms_resolve_path = timer.phase_lap();
report.elapsed_ms = timer.overall_ms();
return Ok(report);
};
report.transcript_path = Some(path.clone());
let mtime = std::fs::metadata(&path)
.ok()
.and_then(|m| m.modified().ok());
report.elapsed_ms_resolve_path = timer.phase_lap();
let watermark: Option<String> = conn
.query_row(
"SELECT MAX(created_at) FROM memories WHERE agent_id_idx = ?1",
rusqlite::params![&opts.agent_id],
|row| row.get::<_, Option<String>>(0),
)
.unwrap_or(None);
report.elapsed_ms_dedup_query = timer.phase_lap();
if let (Some(mtime), Some(watermark_iso)) = (mtime, watermark.as_deref()) {
if let Ok(watermark_dt) = chrono::DateTime::parse_from_rfc3339(watermark_iso) {
let mtime_dt: chrono::DateTime<chrono::Utc> = mtime.into();
if mtime_dt <= watermark_dt.with_timezone(&chrono::Utc) {
report.fast_path_hit = true;
report.elapsed_ms = timer.overall_ms();
return Ok(report);
}
}
}
let since = opts.since_iso.clone();
let turns = match ClaudeCodeJsonlParser.parse(&path, since.as_deref()) {
Ok(t) => t,
Err(e) => {
report.errors.push(format!("parse failed: {e}"));
report.elapsed_ms = timer.overall_ms();
return Ok(report);
}
};
report.lines_total = u32::try_from(turns.len()).unwrap_or(u32::MAX);
report.elapsed_ms_parse = timer.phase_lap();
let namespace = opts
.namespace
.clone()
.unwrap_or_else(|| crate::DEFAULT_NAMESPACE.to_string());
let host_kind = opts.host.as_str().to_string();
for turn in turns {
if usize::try_from(report.lines_atomised).unwrap_or(usize::MAX) >= opts.limit {
report.lines_skipped_limit += 1;
continue;
}
let Ok(raw_sha_bytes) = hex::decode(&turn.line_sha256_hex) else {
report.errors.push(format!(
"skipping turn with malformed sha256: {}",
turn.line_sha256_hex
));
continue;
};
let sha_bytes =
hex::decode(turn.normalized_sha256_hex()).unwrap_or_else(|_| raw_sha_bytes.clone());
let already = find_existing_dedup(&conn, &turn, &raw_sha_bytes, &sha_bytes);
if already.is_some() {
report.lines_skipped_dedup += 1;
continue;
}
if opts.dry_run {
report.lines_atomised += 1;
continue;
}
match write_recovered_turn(
&conn, &turn, &sha_bytes, &namespace, &host_kind, &path, opts,
) {
Ok(memory_id) => {
report.lines_atomised += 1;
report.memories_created.push(memory_id);
}
Err(e) => report.errors.push(e),
}
}
if opts.quiet && report.memories_created.len() > QUIET_MEMORY_ID_PREVIEW_CAP {
report
.memories_created
.truncate(QUIET_MEMORY_ID_PREVIEW_CAP);
}
report.elapsed_ms_writes = timer.phase_lap();
report.elapsed_ms = timer.overall_ms();
Ok(report)
}
fn role_label(role: parsers::TurnRole) -> &'static str {
role.as_str()
}
fn find_existing_dedup(
conn: &rusqlite::Connection,
turn: &parsers::ParsedTurn,
raw_sha: &[u8],
norm_sha: &[u8],
) -> Option<String> {
if let (Some(sid), Some(tix)) = (turn.host_session_id.as_deref(), turn.host_turn_index) {
let hit: Option<String> = conn
.query_row(
"SELECT memory_id FROM transcript_line_dedup \
WHERE host_session_id = ?1 AND host_turn_index = ?2",
rusqlite::params![sid, tix],
|row| row.get(0),
)
.optional()
.unwrap_or(None);
if hit.is_some() {
return hit;
}
}
conn.query_row(
"SELECT memory_id FROM transcript_line_dedup WHERE sha256 IN (?1, ?2)",
rusqlite::params![norm_sha, raw_sha],
|row| row.get(0),
)
.optional()
.unwrap_or(None)
}
fn write_recovered_turn(
conn: &rusqlite::Connection,
turn: &parsers::ParsedTurn,
sha_bytes: &[u8],
namespace: &str,
host_kind: &str,
transcript_path: &Path,
opts: &RecoverOpts,
) -> Result<String, String> {
use crate::models::{Memory, MemoryKind, Tier};
let role = role_label(turn.role);
let now_iso = chrono::Utc::now().to_rfc3339();
let content = if turn.content_text.trim().is_empty() {
let briefs: Vec<String> = turn
.tool_calls
.iter()
.map(|tc| format!("{}: {}", tc.tool, tc.brief))
.collect();
format!("[tool calls] {}", briefs.join("; "))
} else {
turn.content_text.clone()
};
let title = format!(
"L2 recovered {role} turn {} @ {}",
turn.line_sha256_hex, turn.timestamp_iso
);
let mut tags = vec![
"captured-via-l2".to_string(),
"recovered-from-transcript".to_string(),
format!("host:{host_kind}"),
format!("role:{role}"),
];
tags.push(match turn.role {
parsers::TurnRole::User => "operator-directive".to_string(),
parsers::TurnRole::Assistant => "agent-response".to_string(),
_ => "transcript-line".to_string(),
});
let priority = if turn.role == parsers::TurnRole::User {
6
} else {
5
};
let metadata = serde_json::json!({
"agent_id": opts.agent_id,
"host_kind": host_kind,
"transcript_path": transcript_path.display().to_string(),
"line_sha256": turn.line_sha256_hex,
"role": role,
"capture_layer": "L2",
"tool_calls": turn.tool_calls,
});
let mem = Memory {
id: uuid::Uuid::new_v4().to_string(),
tier: Tier::Long,
namespace: namespace.to_string(),
title,
content,
tags,
priority,
confidence: 1.0,
source: "recover".to_string(),
metadata,
created_at: turn.timestamp_iso.clone(),
updated_at: now_iso.clone(),
last_accessed_at: Some(now_iso),
memory_kind: MemoryKind::Observation,
..Memory::default()
};
conn.execute_batch(crate::storage::connection::SQL_BEGIN_IMMEDIATE)
.map_err(|e| format!("TX_BEGIN_FAILED: {e}"))?;
let tx_result = (|| -> Result<String, String> {
let inserted_id =
crate::storage::insert(conn, &mem).map_err(|e| format!("MEMORY_INSERT_FAILED: {e}"))?;
let recovered_at_ms = chrono::Utc::now().timestamp_millis();
conn.execute(
"INSERT INTO transcript_line_dedup \
(sha256, memory_id, host_kind, transcript_path, \
host_session_id, host_turn_index, recovered_at) \
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
rusqlite::params![
sha_bytes,
inserted_id,
host_kind,
transcript_path.display().to_string(),
turn.host_session_id,
turn.host_turn_index,
recovered_at_ms,
],
)
.map_err(|e| format!("DEDUP_INSERT_FAILED: {e}"))?;
Ok(inserted_id)
})();
match tx_result {
Ok(memory_id) => {
conn.execute_batch(crate::storage::connection::SQL_COMMIT)
.map_err(|e| format!("TX_COMMIT_FAILED: {e}"))?;
Ok(memory_id)
}
Err(e) => {
let _ = conn.execute_batch(crate::storage::connection::SQL_ROLLBACK);
Err(e)
}
}
}
#[derive(Debug)]
pub enum RecoverError {
DbOpen(String),
InvalidOpts(String),
}
impl std::fmt::Display for RecoverError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::DbOpen(msg) => write!(f, "recover: db open failed: {msg}"),
Self::InvalidOpts(msg) => write!(f, "recover: invalid opts: {msg}"),
}
}
}
impl std::error::Error for RecoverError {}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
const USER_LINE_1: &str = r#"{"timestamp":"2026-05-28T12:00:00Z","type":"user","message":{"content":[{"type":"text","text":"operator directive one"}]}}"#;
const USER_LINE_2: &str = r#"{"timestamp":"2026-05-28T12:01:00Z","type":"user","message":{"content":[{"type":"text","text":"operator directive two"}]}}"#;
const USER_LINE_3: &str = r#"{"timestamp":"2026-05-28T12:02:00Z","type":"user","message":{"content":[{"type":"text","text":"operator directive three"}]}}"#;
fn fresh_dir() -> tempfile::TempDir {
let root = std::env::current_dir()
.unwrap_or_else(|_| PathBuf::from("."))
.join(".local-runs")
.join("issue-1389-recover-unit-test");
std::fs::create_dir_all(&root).ok();
tempfile::tempdir_in(&root).expect("tempdir under .local-runs")
}
fn write_transcript(dir: &Path, lines: &[&str]) -> PathBuf {
let p = dir.join("session.jsonl");
let mut f = std::fs::File::create(&p).unwrap();
for l in lines {
writeln!(f, "{l}").unwrap();
}
f.flush().unwrap();
p
}
fn base_opts(transcript: PathBuf, agent_id: &str) -> RecoverOpts {
RecoverOpts {
host: HostKind::ClaudeCode,
transcript_override: Some(transcript),
since_iso: None,
namespace: Some("test-recover".to_string()),
limit: DEFAULT_RECOVER_LIMIT,
dry_run: false,
quiet: false,
agent_id: agent_id.to_string(),
}
}
#[test]
fn gap_path_writes_one_memory_per_turn() {
let dir = fresh_dir();
let db = dir.path().join("mem.db");
let transcript = write_transcript(dir.path(), &[USER_LINE_1, USER_LINE_2]);
let report = recover_from_transcript(&db, &base_opts(transcript, "ai:test:gap")).unwrap();
assert!(!report.fast_path_hit);
assert_eq!(report.lines_total, 2);
assert_eq!(report.lines_atomised, 2);
assert_eq!(report.memories_created.len(), 2);
assert!(report.errors.is_empty(), "errors: {:?}", report.errors);
}
#[test]
fn rerun_dedups_already_recovered_turns() {
let dir = fresh_dir();
let db = dir.path().join("mem.db");
let transcript = write_transcript(dir.path(), &[USER_LINE_1, USER_LINE_2]);
let opts = base_opts(transcript, "ai:test:dedup");
let first = recover_from_transcript(&db, &opts).unwrap();
assert_eq!(first.lines_atomised, 2);
let second = recover_from_transcript(&db, &opts).unwrap();
assert_eq!(second.lines_atomised, 0);
assert_eq!(second.lines_skipped_dedup, 2);
assert!(second.memories_created.is_empty());
}
#[test]
fn limit_caps_atomised_lines() {
let dir = fresh_dir();
let db = dir.path().join("mem.db");
let transcript = write_transcript(dir.path(), &[USER_LINE_1, USER_LINE_2, USER_LINE_3]);
let mut opts = base_opts(transcript, "ai:test:limit");
opts.limit = 2;
let report = recover_from_transcript(&db, &opts).unwrap();
assert_eq!(report.lines_atomised, 2);
assert_eq!(report.lines_skipped_limit, 1);
}
#[test]
fn dry_run_persists_nothing() {
let dir = fresh_dir();
let db = dir.path().join("mem.db");
let transcript = write_transcript(dir.path(), &[USER_LINE_1, USER_LINE_2]);
let mut opts = base_opts(transcript, "ai:test:dry");
opts.dry_run = true;
let report = recover_from_transcript(&db, &opts).unwrap();
assert_eq!(report.lines_atomised, 2, "would-be writes are counted");
assert!(report.memories_created.is_empty());
let conn = crate::storage::open(&db).unwrap();
let n: i64 = conn
.query_row("SELECT COUNT(*) FROM transcript_line_dedup", [], |r| {
r.get(0)
})
.unwrap();
assert_eq!(n, 0, "dry-run must not write dedup rows");
}
#[test]
fn fast_path_short_circuits_when_watermark_newer_than_mtime() {
use crate::models::{Memory, MemoryKind, Tier};
let dir = fresh_dir();
let db = dir.path().join("mem.db");
let transcript = write_transcript(dir.path(), &[USER_LINE_1]);
{
let conn = crate::storage::open(&db).unwrap();
let mem = Memory {
id: uuid::Uuid::new_v4().to_string(),
tier: Tier::Long,
namespace: "test-recover".to_string(),
title: "watermark seed".to_string(),
content: "seed".to_string(),
priority: 5,
confidence: 1.0,
source: "test".to_string(),
metadata: serde_json::json!({"agent_id": "ai:test:fast"}),
created_at: "2999-01-01T00:00:00Z".to_string(),
updated_at: "2999-01-01T00:00:00Z".to_string(),
memory_kind: MemoryKind::Observation,
..Memory::default()
};
crate::storage::insert(&conn, &mem).unwrap();
}
let report = recover_from_transcript(&db, &base_opts(transcript, "ai:test:fast")).unwrap();
assert!(report.fast_path_hit, "expected fast-path short-circuit");
assert_eq!(report.lines_atomised, 0);
}
#[test]
fn reserialized_turn_different_whitespace_key_order_dedups_1573() {
const USER_LINE_1_RESERIALIZED: &str = r#"{ "type": "user", "message": {"content": [ {"text": "operator directive one", "type": "text"} ]}, "timestamp": "2026-05-28T12:00:00Z" }"#;
let dir = fresh_dir();
let db = dir.path().join("mem.db");
let t1 = write_transcript(dir.path(), &[USER_LINE_1]);
let first = recover_from_transcript(&db, &base_opts(t1, "ai:test:reser")).unwrap();
assert_eq!(first.lines_atomised, 1);
let p2 = dir.path().join("session-reserialized.jsonl");
std::fs::write(&p2, format!("{USER_LINE_1_RESERIALIZED}\n")).unwrap();
let second = recover_from_transcript(&db, &base_opts(p2, "ai:test:reser")).unwrap();
assert_eq!(
second.lines_atomised, 0,
"re-serialized turn must not re-atomise: {second:?}"
);
assert_eq!(second.lines_skipped_dedup, 1);
assert!(second.memories_created.is_empty());
}
#[test]
fn session_turn_composite_key_and_raw_sha_backcompat_dedup_1573() {
let dir = fresh_dir();
let db = dir.path().join("mem.db");
let conn = crate::storage::open(&db).unwrap();
let turn = parsers::ParsedTurn {
timestamp_iso: "2026-05-28T12:00:00Z".to_string(),
role: parsers::TurnRole::User,
content_text: "operator directive one".to_string(),
tool_calls: vec![],
line_sha256_hex: "aa".repeat(32),
host_session_id: Some("sess-1573".to_string()),
host_turn_index: Some(3),
};
let raw_sha = hex::decode(&turn.line_sha256_hex).unwrap();
let norm_sha = hex::decode(turn.normalized_sha256_hex()).unwrap();
assert!(find_existing_dedup(&conn, &turn, &raw_sha, &norm_sha).is_none());
conn.execute(
"INSERT INTO transcript_line_dedup \
(sha256, memory_id, host_kind, transcript_path, \
host_session_id, host_turn_index, recovered_at) \
VALUES (?1, 'mem-l4', 'claude-code', NULL, 'sess-1573', 3, 0)",
rusqlite::params![vec![0x01_u8; 32]],
)
.unwrap();
assert_eq!(
find_existing_dedup(&conn, &turn, &raw_sha, &norm_sha).as_deref(),
Some("mem-l4"),
"composite (host_session_id, host_turn_index) key must dedup"
);
let other = parsers::ParsedTurn {
host_turn_index: Some(4),
content_text: "operator directive two".to_string(),
..turn.clone()
};
let other_norm = hex::decode(other.normalized_sha256_hex()).unwrap();
assert!(find_existing_dedup(&conn, &other, &raw_sha, &other_norm).is_none());
let legacy = parsers::ParsedTurn {
host_session_id: None,
host_turn_index: None,
..turn.clone()
};
conn.execute(
"INSERT INTO transcript_line_dedup \
(sha256, memory_id, host_kind, transcript_path, \
host_session_id, host_turn_index, recovered_at) \
VALUES (?1, 'mem-legacy', 'claude-code', NULL, NULL, NULL, 0)",
rusqlite::params![&raw_sha],
)
.unwrap();
let legacy_norm = hex::decode(legacy.normalized_sha256_hex()).unwrap();
assert_eq!(
find_existing_dedup(&conn, &legacy, &raw_sha, &legacy_norm).as_deref(),
Some("mem-legacy"),
"pre-#1573 raw-line sha rows must keep deduping"
);
}
#[test]
fn missing_transcript_is_graceful() {
let dir = fresh_dir();
let db = dir.path().join("mem.db");
let missing = dir.path().join("does-not-exist.jsonl");
let report = recover_from_transcript(&db, &base_opts(missing, "ai:test:missing")).unwrap();
assert_eq!(report.lines_atomised, 0);
assert!(!report.errors.is_empty());
}
#[test]
fn recover_timer_default_and_phase_lap() {
let mut t = RecoverTimer::default();
let _ = t.phase_lap();
let _ = t.overall_ms();
}
#[test]
fn recover_report_new_initializes_host_and_schema() {
let r = RecoverReport::new(HostKind::Codex, 57);
assert_eq!(r.host_kind, HostKind::Codex);
assert_eq!(r.schema_version_at_run, 57);
assert!(!r.fast_path_hit);
}
#[test]
fn for_session_start_hook_defaults() {
let opts = RecoverOpts::for_session_start_hook(HostKind::ClaudeCode, "ai:hook".to_string());
assert_eq!(opts.host, HostKind::ClaudeCode);
assert_eq!(opts.agent_id, "ai:hook");
assert_eq!(opts.limit, DEFAULT_RECOVER_LIMIT);
assert!(opts.quiet);
assert!(!opts.dry_run);
assert!(opts.transcript_override.is_none());
assert!(opts.since_iso.is_none());
assert!(opts.namespace.is_none());
}
#[test]
fn recover_error_display_arms() {
assert_eq!(
RecoverError::DbOpen("boom".to_string()).to_string(),
"recover: db open failed: boom"
);
assert_eq!(
RecoverError::InvalidOpts("bad".to_string()).to_string(),
"recover: invalid opts: bad"
);
let e = RecoverError::DbOpen("x".to_string());
let _: &dyn std::error::Error = &e;
assert!(format!("{e:?}").contains("DbOpen"));
}
#[test]
fn db_open_failure_returns_db_open_error() {
let dir = fresh_dir();
let file_as_parent = dir.path().join("not-a-dir");
std::fs::write(&file_as_parent, b"x").unwrap();
let bad_db = file_as_parent.join("child.db");
let transcript = write_transcript(dir.path(), &[USER_LINE_1]);
let err =
recover_from_transcript(&bad_db, &base_opts(transcript, "ai:test:dberr")).unwrap_err();
assert!(matches!(err, RecoverError::DbOpen(_)), "got {err:?}");
}
#[test]
fn no_transcript_override_resolves_via_cwd_and_returns_none_gracefully() {
let dir = fresh_dir();
let db = dir.path().join("mem.db");
let opts = RecoverOpts {
host: HostKind::ClaudeCode,
transcript_override: None,
since_iso: None,
namespace: Some("test-recover".to_string()),
limit: DEFAULT_RECOVER_LIMIT,
dry_run: false,
quiet: false,
agent_id: "ai:test:cwd".to_string(),
};
let report = recover_from_transcript(&db, &opts).unwrap();
assert!(report.errors.is_empty() || report.transcript_path.is_some());
}
#[test]
fn tool_call_only_turn_produces_tool_calls_content() {
let dir = fresh_dir();
let db = dir.path().join("mem.db");
let tool_only = r#"{"timestamp":"2026-05-28T13:00:00Z","type":"assistant","message":{"content":[{"type":"tool_use","name":"Bash","input":{"command":"ls"}}]}}"#;
let transcript = write_transcript(dir.path(), &[tool_only]);
let report =
recover_from_transcript(&db, &base_opts(transcript, "ai:test:toolonly")).unwrap();
assert_eq!(report.lines_atomised, 1, "errors: {:?}", report.errors);
let conn = crate::storage::open(&db).unwrap();
let content: String = conn
.query_row(
"SELECT content FROM memories WHERE id = ?1",
rusqlite::params![&report.memories_created[0]],
|r| r.get(0),
)
.unwrap();
assert!(content.starts_with("[tool calls]"), "got: {content}");
assert!(content.contains("Bash"));
}
#[test]
fn quiet_mode_truncates_memory_id_preview() {
let dir = fresh_dir();
let db = dir.path().join("mem.db");
let lines: Vec<String> = (0..(QUIET_MEMORY_ID_PREVIEW_CAP + 5))
.map(|i| {
format!(
r#"{{"timestamp":"2026-05-28T12:{:02}:00Z","type":"user","message":{{"content":[{{"type":"text","text":"directive {i}"}}]}}}}"#,
i % 60
)
})
.collect();
let refs: Vec<&str> = lines.iter().map(String::as_str).collect();
let transcript = write_transcript(dir.path(), &refs);
let mut opts = base_opts(transcript, "ai:test:quiet");
opts.quiet = true;
let report = recover_from_transcript(&db, &opts).unwrap();
assert!(usize::try_from(report.lines_atomised).unwrap() > QUIET_MEMORY_ID_PREVIEW_CAP);
assert_eq!(
report.memories_created.len(),
QUIET_MEMORY_ID_PREVIEW_CAP,
"quiet mode must cap the echoed id list"
);
}
}