Skip to main content

lean_ctx/tools/
ctx_session.rs

1use crate::core::session::SessionState;
2
3#[derive(Clone, Copy, Debug)]
4pub struct SessionToolOptions<'a> {
5    pub format: Option<&'a str>,
6    pub path: Option<&'a str>,
7    pub write: bool,
8    pub privacy: Option<&'a str>,
9    /// For `action=configure`: set terse output mode when `Some`.
10    pub terse: Option<bool>,
11}
12
13pub fn handle(
14    session: &mut SessionState,
15    tool_calls: &[(String, u64)],
16    action: &str,
17    value: Option<&str>,
18    session_id: Option<&str>,
19    opts: SessionToolOptions<'_>,
20) -> String {
21    match action {
22        "status" => session.format_compact(),
23
24        "load" => {
25            let loaded = if let Some(id) = session_id {
26                SessionState::load_by_id(id)
27            } else {
28                SessionState::load_latest()
29            };
30
31            if let Some(prev) = loaded {
32                let summary = prev.format_compact();
33                *session = prev;
34                format!("Session loaded.\n{summary}")
35            } else {
36                let id_str = session_id.unwrap_or("latest");
37                format!("No session found (id: {id_str}). Starting fresh.")
38            }
39        }
40
41        "save" => {
42            match session.save() {
43                Ok(()) => format!("Session {} saved (v{}).", session.id, session.version),
44                Err(e) => format!("Save failed: {e}"),
45            }
46        }
47
48        "export" => {
49            let requested_privacy = crate::core::ccp_session_bundle::BundlePrivacyV1::parse(opts.privacy);
50            if requested_privacy == crate::core::ccp_session_bundle::BundlePrivacyV1::Full
51                && crate::core::roles::active_role_name() != "admin"
52            {
53                return "ERROR: privacy=full requires role 'admin'.".to_string();
54            }
55
56            let bundle =
57                crate::core::ccp_session_bundle::build_bundle_v1(session, requested_privacy);
58            let json = match crate::core::ccp_session_bundle::serialize_bundle_v1_pretty(&bundle) {
59                Ok(s) => s,
60                Err(e) => return e,
61            };
62
63            let format = opts
64                .format
65                .unwrap_or(if opts.write { "summary" } else { "json" });
66            let root = session.project_root.clone().unwrap_or_else(|| {
67                std::env::current_dir().map_or_else(
68                    |_| ".".to_string(),
69                    |p| p.to_string_lossy().to_string(),
70                )
71            });
72            let root_path = std::path::PathBuf::from(&root);
73
74            let mut written: Option<String> = None;
75            if opts.write || opts.path.is_some() {
76                let ts = chrono::Utc::now().format("%Y%m%dT%H%M%SZ").to_string();
77                let candidate = if let Some(p) = opts.path.or(value) {
78                    let p = std::path::PathBuf::from(p);
79                    if p.is_absolute() { p } else { root_path.join(p) }
80                } else {
81                    root_path
82                        .join(".lean-ctx")
83                        .join("session_bundles")
84                        .join(format!("ccp-session-bundle-v1_{}_{}.json", bundle.session.id, ts))
85                };
86
87                let jailed = match crate::core::io_boundary::jail_and_check_path(
88                    "ctx_session.export",
89                    candidate.as_path(),
90                    root_path.as_path(),
91                ) {
92                    Ok((p, _warning)) => p,
93                    Err(e) => return e,
94                };
95
96                if let Err(e) = crate::core::ccp_session_bundle::write_bundle_v1(&jailed, &json) {
97                    return format!("Export write failed: {e}");
98                }
99                written = Some(jailed.to_string_lossy().to_string());
100            }
101
102            match format {
103                "summary" => {
104                    let mut out = format!(
105                        "CCP session bundle exported (v{}).\n\
106schema_version: {}\n\
107session_id: {}\n\
108bytes: {}\n",
109                        bundle.session.version,
110                        bundle.schema_version,
111                        bundle.session.id,
112                        json.len()
113                    );
114                    if let Some(p) = written {
115                        out.push_str(&format!("path: {p}\n"));
116                    }
117                    if let Some(h) = bundle.project.project_root_hash {
118                        out.push_str(&format!("project_root_hash: {h}\n"));
119                    }
120                    if let Some(h) = bundle.project.project_identity_hash {
121                        out.push_str(&format!("project_identity_hash: {h}\n"));
122                    }
123                    out
124                }
125                _ => {
126                    if let Some(p) = written {
127                        format!("{json}\n\npath: {p}")
128                    } else {
129                        json
130                    }
131                }
132            }
133        }
134
135        "import" => {
136            let root = session.project_root.clone().unwrap_or_else(|| {
137                std::env::current_dir().map_or_else(
138                    |_| ".".to_string(),
139                    |p| p.to_string_lossy().to_string(),
140                )
141            });
142            let root_path = std::path::PathBuf::from(&root);
143
144            let Some(p) = opts.path.or(value) else {
145                return "ERROR: path is required for action=import".to_string();
146            };
147
148            let candidate = {
149                let p = std::path::PathBuf::from(p);
150                if p.is_absolute() { p } else { root_path.join(p) }
151            };
152            let jailed = match crate::core::io_boundary::jail_and_check_path(
153                "ctx_session.import",
154                candidate.as_path(),
155                root_path.as_path(),
156            ) {
157                Ok((p, _warning)) => p,
158                Err(e) => return e,
159            };
160
161            let bundle = match crate::core::ccp_session_bundle::read_bundle_v1(&jailed) {
162                Ok(b) => b,
163                Err(e) => return format!("Import failed: {e}"),
164            };
165
166            // Replayability hint: compare project identity hashes (best-effort).
167            let current_root_hash = crate::core::project_hash::hash_project_root(&root);
168            let current_identity_hash = crate::core::project_hash::project_identity(&root)
169                .as_deref()
170                .map(|s| {
171                    use md5::{Digest, Md5};
172                    let mut h = Md5::new();
173                    h.update(s.as_bytes());
174                    format!("{:x}", h.finalize())
175                });
176
177            let mut warning: Option<String> = None;
178            if let Some(ref exported) = bundle.project.project_root_hash {
179                if exported != &current_root_hash {
180                    warning = Some("WARNING: project_root_hash mismatch (importing into different project root).".to_string());
181                }
182            }
183            if let (Some(exported), Some(current)) =
184                (bundle.project.project_identity_hash.as_ref(), current_identity_hash.as_ref())
185            {
186                if exported != current {
187                    warning = Some("WARNING: project_identity_hash mismatch (importing into different project identity).".to_string());
188                }
189            }
190
191            let report = crate::core::ccp_session_bundle::import_bundle_v1_into_session(
192                session,
193                &bundle,
194                Some(&root),
195            );
196            let _ = session.save();
197
198            let mut out = format!(
199                "CCP session bundle imported.\n\
200session_id: {}\n\
201version: {}\n\
202files_touched: {}\n\
203stale_files: {}\n",
204                report.session_id, report.version, report.files_touched, report.stale_files
205            );
206            if let Some(w) = warning {
207                out.push_str(&format!("{w}\n"));
208            }
209            out
210        }
211
212        "task" => {
213            let desc = value.unwrap_or("(no description)");
214            session.set_task(desc, None);
215            format!("Task set: {desc}")
216        }
217
218        "finding" => {
219            let summary = value.unwrap_or("(no summary)");
220            let (file, line, text) = parse_finding_value(summary);
221            session.add_finding(file.as_deref(), line, text);
222            format!("Finding added: {summary}")
223        }
224
225        "decision" => {
226            let desc = value.unwrap_or("(no description)");
227            session.add_decision(desc, None);
228            format!("Decision recorded: {desc}")
229        }
230
231        "reset" => {
232            let _ = session.save();
233            let old_id = session.id.clone();
234            *session = SessionState::new();
235            crate::core::budget_tracker::BudgetTracker::global().reset();
236            // Clear the persistent context ledger so pressure resets to 0%
237            let mut ledger = crate::core::context_ledger::ContextLedger::load();
238            ledger.reset();
239            ledger.save();
240            if let Ok(data_dir) = crate::core::data_dir::lean_ctx_data_dir() {
241                let radar_path = data_dir.join("context_radar.jsonl");
242                let prev = data_dir.join("context_radar.prev.jsonl");
243                let _ = std::fs::rename(&radar_path, &prev);
244            }
245            format!(
246                "Session reset. Previous: {old_id}. New: {}. Ledger cleared (0% pressure).",
247                session.id
248            )
249        }
250
251        "list" => {
252            let sessions = SessionState::list_sessions();
253            if sessions.is_empty() {
254                return "No sessions found.".to_string();
255            }
256            let mut lines = vec![format!("Sessions ({}):", sessions.len())];
257            for s in sessions.iter().take(10) {
258                let task = s.task.as_deref().unwrap_or("(no task)");
259                let task_short: String = task.chars().take(40).collect();
260                lines.push(format!(
261                    "  {} v{} | {} calls | {} tok | {}",
262                    s.id, s.version, s.tool_calls, s.tokens_saved, task_short
263                ));
264            }
265            if sessions.len() > 10 {
266                lines.push(format!("  ... +{} more", sessions.len() - 10));
267            }
268            lines.join("\n")
269        }
270
271        "cleanup" => {
272            let removed = SessionState::cleanup_old_sessions(7);
273            format!("Cleaned up {removed} old session(s) (>7 days).")
274        }
275
276        "configure" => match opts.terse {
277            Some(enabled) => {
278                session.terse_mode = enabled;
279                session.increment();
280                format!("Session configured: terse_mode={enabled}")
281            }
282            None => format!("Session config: terse_mode={}", session.terse_mode),
283        },
284
285        "snapshot" => match session.save_compaction_snapshot() {
286            Ok(snapshot) => {
287                format!(
288                    "Compaction snapshot saved ({} bytes).\n{snapshot}",
289                    snapshot.len()
290                )
291            }
292            Err(e) => format!("Snapshot failed: {e}"),
293        },
294
295        "restore" => {
296            let snapshot = if let Some(id) = session_id {
297                SessionState::load_compaction_snapshot(id)
298            } else {
299                SessionState::load_latest_snapshot()
300            };
301            match snapshot {
302                Some(s) => format!("Session restored from compaction snapshot:\n{s}"),
303                None => "No compaction snapshot found. Session continues fresh.".to_string(),
304            }
305        }
306
307        "resume" => session.build_resume_block(),
308
309        "profile" => {
310            use crate::core::profiles;
311            if let Some(name) = value {
312                if let Ok(p) = profiles::set_active_profile(name) {
313                    format!(
314                        "Profile switched to '{name}'.\n\
315                         Read mode: {}, Budget: {} tokens, CRP: {}, Density: {}",
316                        p.read.default_mode_effective(),
317                        p.budget.max_context_tokens_effective(),
318                        p.compression.crp_mode_effective(),
319                        p.compression.output_density_effective(),
320                    )
321                } else {
322                    let available: Vec<String> =
323                        profiles::list_profiles().iter().map(|p| p.name.clone()).collect();
324                    format!(
325                        "Profile '{name}' not found. Available: {}",
326                        available.join(", ")
327                    )
328                }
329            } else {
330                let name = profiles::active_profile_name();
331                let p = profiles::active_profile();
332                let list = profiles::list_profiles();
333                let mut out = format!(
334                    "Active profile: {name}\n\
335                     Read: {}, Budget: {} tok, CRP: {}, Density: {}\n\n\
336                     Available profiles:",
337                    p.read.default_mode_effective(),
338                    p.budget.max_context_tokens_effective(),
339                    p.compression.crp_mode_effective(),
340                    p.compression.output_density_effective(),
341                );
342                for info in &list {
343                    let marker = if info.name == name { " *" } else { "  " };
344                    out.push_str(&format!(
345                        "\n{marker} {:<14} ({}) {}",
346                        info.name, info.source, info.description
347                    ));
348                }
349                out.push_str("\n\nSwitch: ctx_session action=profile value=<name>");
350                out
351            }
352        }
353
354        "budget" => {
355            use crate::core::budget_tracker::BudgetTracker;
356            let snap = BudgetTracker::global().check();
357            let mut out = snap.format_compact();
358
359            if let Ok(data_dir) = crate::core::data_dir::lean_ctx_data_dir() {
360                let window = crate::core::context_radar::default_window_for_client("cursor");
361                let radar = crate::core::context_radar::ContextRadar::load(&data_dir, window);
362                let radar_display = radar.format_display();
363                if !radar_display.is_empty() {
364                    out.push_str("\n\n");
365                    out.push_str(&radar_display);
366                }
367            }
368            out
369        }
370
371        "role" => {
372            use crate::core::roles;
373            if let Some(name) = value {
374                match roles::set_active_role(name) {
375                    Ok(r) => {
376                        crate::core::budget_tracker::BudgetTracker::global().reset();
377                        format!(
378                            "Role switched to '{name}'.\n\
379                             Shell: {}, Budget: {} tokens / {} shell / ${:.2}\n\
380                             Tools: {}",
381                            r.role.shell_policy,
382                            r.limits.max_context_tokens,
383                            r.limits.max_shell_invocations,
384                            r.limits.max_cost_usd,
385                            if r.tools.allowed.iter().any(|a| a == "*") {
386                                let denied = if r.tools.denied.is_empty() {
387                                    "none".to_string()
388                                } else {
389                                    format!("denied: {}", r.tools.denied.join(", "))
390                                };
391                                format!("* (all), {denied}")
392                            } else {
393                                r.tools.allowed.join(", ")
394                            }
395                        )
396                    }
397                    Err(e) => {
398                        let available: Vec<String> =
399                            roles::list_roles().iter().map(|r| r.name.clone()).collect();
400                        format!("{e}. Available: {}", available.join(", "))
401                    }
402                }
403            } else {
404                let name = roles::active_role_name();
405                let r = roles::active_role();
406                let list = roles::list_roles();
407                let mut out = format!(
408                    "Active role: {name}\n\
409                     Description: {}\n\
410                     Shell policy: {}, Budget: {} tokens / {} shell / ${:.2}\n\n\
411                     Available roles:",
412                    r.role.description,
413                    r.role.shell_policy,
414                    r.limits.max_context_tokens,
415                    r.limits.max_shell_invocations,
416                    r.limits.max_cost_usd,
417                );
418                for info in &list {
419                    let marker = if info.is_active { " *" } else { "  " };
420                    out.push_str(&format!(
421                        "\n{marker} {:<14} ({}) {}",
422                        info.name, info.source, info.description
423                    ));
424                }
425                out.push_str("\n\nSwitch: ctx_session action=role value=<name>");
426                out
427            }
428        }
429
430        "diff" => {
431            let parts: Vec<&str> = value.unwrap_or("").split_whitespace().collect();
432            if parts.len() < 2 {
433                return "Usage: ctx_session diff <session_id_a> <session_id_b> [format]\n\
434                        Formats: summary (default), json\n\
435                        Example: ctx_session diff abc123 def456 json"
436                    .to_string();
437            }
438            let id_a = parts[0];
439            let id_b = parts[1];
440            let format = parts.get(2).copied().unwrap_or("summary");
441
442            let sess_a = SessionState::load_by_id(id_a);
443            let sess_b = SessionState::load_by_id(id_b);
444
445            match (sess_a, sess_b) {
446                (Some(a), Some(b)) => {
447                    let d = crate::core::session_diff::diff_sessions(&a, &b);
448                    match format {
449                        "json" => d.format_json(),
450                        _ => d.format_summary(),
451                    }
452                }
453                (None, _) => format!("Session not found: {id_a}"),
454                (_, None) => format!("Session not found: {id_b}"),
455            }
456        }
457
458        "slo" => {
459            match value {
460                Some("reload") => {
461                    crate::core::slo::reload();
462                    "SLO definitions reloaded from disk.".to_string()
463                }
464                Some("history") => {
465                    let hist = crate::core::slo::violation_history(20);
466                    if hist.is_empty() {
467                        "No SLO violations recorded.".to_string()
468                    } else {
469                        let mut out = format!("SLO violations (last {}):\n", hist.len());
470                        for v in &hist {
471                            out.push_str(&format!(
472                                "  {} {} ({}) {:.2} vs {:.2} → {}\n",
473                                v.timestamp, v.slo_name, v.metric, v.actual, v.threshold, v.action
474                            ));
475                        }
476                        out
477                    }
478                }
479                Some("clear") => {
480                    crate::core::slo::clear_violations();
481                    "SLO violation history cleared.".to_string()
482                }
483                _ => {
484                    let snap = crate::core::slo::evaluate_quiet();
485                    snap.format_compact()
486                }
487            }
488        }
489
490        "output_stats" => {
491            let snap = crate::core::output_verification::stats_snapshot();
492            snap.format_compact()
493        }
494
495        "verify" => {
496            let snap = crate::core::output_verification::stats_snapshot();
497            format!(
498                "DEPRECATION: action=\"verify\" is renamed to action=\"output_stats\" (ctx_verify is the full observability stack).\n{}",
499                snap.format_compact()
500            )
501        }
502
503        "episodes" => {
504            let project_root = session.project_root.clone().unwrap_or_else(|| {
505                std::env::current_dir().map_or_else(
506                    |_| "unknown".to_string(),
507                    |p| p.to_string_lossy().to_string(),
508                )
509            });
510            let policy = match crate::core::config::Config::load().memory_policy_effective() {
511                Ok(p) => p,
512                Err(e) => {
513                    let path = crate::core::config::Config::path().map_or_else(
514                        || "~/.lean-ctx/config.toml".to_string(),
515                        |p| p.display().to_string(),
516                    );
517                    return format!("Error: invalid memory policy: {e}\nFix: edit {path}");
518                }
519            };
520            let hash = crate::core::project_hash::hash_project_root(&project_root);
521            let mut store = crate::core::episodic_memory::EpisodicStore::load_or_create(&hash);
522
523            match value {
524                Some("record") => {
525                    let ep = crate::core::episodic_memory::create_episode_from_session(
526                        session,
527                        tool_calls,
528                    );
529                    let id = ep.id.clone();
530                    store.record_episode(ep, &policy.episodic);
531                    if let Err(e) = store.save() {
532                        return format!("Episode record failed: {e}");
533                    }
534                    crate::core::events::emit(crate::core::events::EventKind::KnowledgeUpdate {
535                        category: "episodic".to_string(),
536                        key: id.clone(),
537                        action: "record".to_string(),
538                    });
539                    format!("Episode recorded: {id}")
540                }
541                Some(v) if v.starts_with("search ") => {
542                    let q = v.trim_start_matches("search ").trim();
543                    let hits = store.search(q);
544                    if hits.is_empty() {
545                        return "No episodes matched.".to_string();
546                    }
547                    let mut out = format!("Episodes matched ({}):", hits.len());
548                    for ep in hits.into_iter().take(10) {
549                        let task: String = ep.task_description.chars().take(50).collect();
550                        out.push_str(&format!(
551                            "\n  {} | {} | {} | {}",
552                            ep.id,
553                            ep.timestamp,
554                            ep.outcome.label(),
555                            task
556                        ));
557                    }
558                    out
559                }
560                Some(v) if v.starts_with("file ") => {
561                    let f = v.trim_start_matches("file ").trim();
562                    let hits = store.by_file(f);
563                    let mut out = format!("Episodes for file match '{f}' ({}):", hits.len());
564                    for ep in hits.into_iter().take(10) {
565                        let task: String = ep.task_description.chars().take(50).collect();
566                        out.push_str(&format!(
567                            "\n  {} | {} | {} | {}",
568                            ep.id,
569                            ep.timestamp,
570                            ep.outcome.label(),
571                            task
572                        ));
573                    }
574                    out
575                }
576                Some(v) if v.starts_with("outcome ") => {
577                    let label = v.trim_start_matches("outcome ").trim();
578                    let hits = store.by_outcome(label);
579                    let mut out = format!("Episodes outcome '{label}' ({}):", hits.len());
580                    for ep in hits.into_iter().take(10) {
581                        let task: String = ep.task_description.chars().take(50).collect();
582                        out.push_str(&format!("\n  {} | {} | {}", ep.id, ep.timestamp, task));
583                    }
584                    out
585                }
586                _ => {
587                    let stats = store.stats();
588                    let recent = store.recent(10);
589                    let mut out = format!(
590                        "Episodic memory: {} episodes, success_rate={:.0}%, tokens_total={}\n\nRecent:",
591                        stats.total_episodes,
592                        stats.success_rate * 100.0,
593                        stats.total_tokens
594                    );
595                    for ep in recent {
596                        let task: String = ep.task_description.chars().take(60).collect();
597                        out.push_str(&format!(
598                            "\n  {} | {} | {} | {}",
599                            ep.id,
600                            ep.timestamp,
601                            ep.outcome.label(),
602                            task
603                        ));
604                    }
605                    out.push_str("\n\nActions: ctx_session action=episodes value=record|\"search <q>\"|\"file <path>\"|\"outcome success|failure|partial|unknown\"");
606                    out
607                }
608            }
609        }
610
611        "procedures" => {
612            let project_root = session.project_root.clone().unwrap_or_else(|| {
613                std::env::current_dir().map_or_else(
614                    |_| "unknown".to_string(),
615                    |p| p.to_string_lossy().to_string(),
616                )
617            });
618            let policy = match crate::core::config::Config::load().memory_policy_effective() {
619                Ok(p) => p,
620                Err(e) => {
621                    let path = crate::core::config::Config::path().map_or_else(
622                        || "~/.lean-ctx/config.toml".to_string(),
623                        |p| p.display().to_string(),
624                    );
625                    return format!("Error: invalid memory policy: {e}\nFix: edit {path}");
626                }
627            };
628            let hash = crate::core::project_hash::hash_project_root(&project_root);
629            let episodes = crate::core::episodic_memory::EpisodicStore::load_or_create(&hash);
630            let mut procs = crate::core::procedural_memory::ProceduralStore::load_or_create(&hash);
631
632            match value {
633                Some("detect") => {
634                    procs.detect_patterns(&episodes.episodes, &policy.procedural);
635                    if let Err(e) = procs.save() {
636                        return format!("Procedure detect failed: {e}");
637                    }
638                    crate::core::events::emit(crate::core::events::EventKind::KnowledgeUpdate {
639                        category: "procedural".to_string(),
640                        key: hash.clone(),
641                        action: "detect".to_string(),
642                    });
643                    format!(
644                        "Procedures updated. Total procedures: {} (episodes: {}).",
645                        procs.procedures.len(),
646                        episodes.episodes.len()
647                    )
648                }
649                Some(v) if v.starts_with("suggest ") => {
650                    let task = v.trim_start_matches("suggest ").trim();
651                    let hits = procs.suggest(task);
652                    if hits.is_empty() {
653                        return "No procedures matched.".to_string();
654                    }
655                    let mut out = format!("Procedures suggested ({}):", hits.len());
656                    for p in hits.into_iter().take(10) {
657                        out.push_str(&format!(
658                            "\n  {} | conf={:.0}% | success={:.0}% | steps={}",
659                            p.name,
660                            p.confidence * 100.0,
661                            p.success_rate() * 100.0,
662                            p.steps.len()
663                        ));
664                    }
665                    out
666                }
667                _ => {
668                    let task = session
669                        .task
670                        .as_ref()
671                        .map(|t| t.description.clone())
672                        .unwrap_or_default();
673                    let suggestions = if task.is_empty() {
674                        Vec::new()
675                    } else {
676                        procs.suggest(&task)
677                    };
678
679                    let mut out = format!(
680                        "Procedural memory: {} procedures (episodes: {})",
681                        procs.procedures.len(),
682                        episodes.episodes.len()
683                    );
684
685                    if !task.is_empty() {
686                        out.push_str(&format!("\nTask: {}", task.chars().take(80).collect::<String>()));
687                        if !suggestions.is_empty() {
688                            out.push_str("\n\nSuggested:");
689                            for p in suggestions.into_iter().take(5) {
690                                out.push_str(&format!(
691                                    "\n  {} | conf={:.0}% | success={:.0}% | steps={}",
692                                    p.name,
693                                    p.confidence * 100.0,
694                                    p.success_rate() * 100.0,
695                                    p.steps.len()
696                                ));
697                            }
698                        }
699                    }
700
701                    out.push_str("\n\nActions: ctx_session action=procedures value=detect|\"suggest <task>\"");
702                    out
703                }
704            }
705        }
706
707        _ => format!("Unknown action: {action}. Use: status, load, save, task, finding, decision, reset, list, cleanup, snapshot, restore, resume, configure, profile, role, budget, slo, diff, output_stats, verify, export, import, episodes, procedures"),
708    }
709}
710
711fn parse_finding_value(value: &str) -> (Option<String>, Option<u32>, &str) {
712    // Format: "file.rs:42 — summary text" or just "summary text"
713    if let Some(dash_pos) = value.find(" \u{2014} ").or_else(|| value.find(" - ")) {
714        let location = &value[..dash_pos];
715        let sep_len = 3;
716        let text = &value[dash_pos + sep_len..];
717
718        if let Some(colon_pos) = location.rfind(':') {
719            let file = &location[..colon_pos];
720            if let Ok(line) = location[colon_pos + 1..].parse::<u32>() {
721                return (Some(file.to_string()), Some(line), text);
722            }
723        }
724        return (Some(location.to_string()), None, text);
725    }
726    (None, None, value)
727}