use std::{
collections::HashMap,
path::{Path, PathBuf},
sync::Mutex,
};
pub(crate) const HARNESS_SESSION_FILE_NAME: &str = "harness-session.json";
pub(crate) const HARNESS_SESSION_TYPE: &str = "murmur.harness-session";
pub(crate) const FORGET_BY_CLI: &str = "cli";
pub(crate) const FORGET_BY_A2A: &str = "a2a";
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub(crate) struct HarnessSessionEntry {
#[serde(rename = "type")]
pub kind: String,
pub session_id: String,
pub harness: String,
pub driver: String,
pub created_ms: u64,
pub updated_ms: u64,
}
pub(crate) struct HarnessSessionMap {
root: Option<PathBuf>,
workdir: PathBuf,
seen: Mutex<HashMap<String, String>>,
failure_reported: Mutex<bool>,
}
impl HarnessSessionMap {
pub(crate) fn new(root: Option<PathBuf>, workdir: &Path) -> Self {
Self {
root,
workdir: workdir.to_path_buf(),
seen: Mutex::new(HashMap::new()),
failure_reported: Mutex::new(false),
}
}
pub(crate) fn root(&self) -> Option<&Path> {
self.root.as_deref()
}
pub(crate) fn entry_path(&self, context_id: &str) -> Option<PathBuf> {
let root = self.root()?;
if crate::state_store::validate_store_name(context_id).is_err() {
return None;
}
Some(entry_file(root, context_id))
}
pub(crate) fn get(&self, context_id: &str) -> Option<String> {
if let Ok(seen) = self.seen.lock() {
if let Some(id) = seen.get(context_id) {
return Some(id.clone());
}
}
let entry = read_entry(&self.entry_path(context_id)?)?;
Some(entry.session_id)
}
pub(crate) fn put(&self, context_id: &str, session_id: &str, harness: &str, driver: &str) {
if session_id.is_empty() {
return;
}
if let Ok(mut seen) = self.seen.lock() {
seen.insert(context_id.to_string(), session_id.to_string());
}
let Some(path) = self.entry_path(context_id) else {
return;
};
let now_ms = crate::retention::now_ms();
let entry = HarnessSessionEntry {
kind: HARNESS_SESSION_TYPE.to_string(),
session_id: session_id.to_string(),
harness: harness.to_string(),
driver: driver.to_string(),
created_ms: read_entry(&path).map(|e| e.created_ms).unwrap_or(now_ms),
updated_ms: now_ms,
};
if let Err(reason) = write_entry(&path, &entry) {
self.report_once(&format!(
"[harness-session] the session id for context '{context_id}' could not be written \
to {} ({reason}); this conversation is remembered only until this capsule stops",
path.display()
));
}
}
pub(crate) fn forget(&self, context_id: &str) -> Option<String> {
let dropped = self.get(context_id);
if let Ok(mut seen) = self.seen.lock() {
seen.remove(context_id);
}
if let Some(path) = self.entry_path(context_id) {
if let Err(reason) = remove_entry(&path) {
self.report_once(&format!(
"[harness-session] the session id for context '{context_id}' could not be \
removed from {} ({reason}); this context is forgotten only until this \
capsule stops",
path.display()
));
}
}
dropped
}
fn report_once(&self, message: &str) {
if let Ok(mut reported) = self.failure_reported.lock() {
if *reported {
return;
}
*reported = true;
}
crate::conversation::report(&self.workdir, message);
}
}
pub(crate) fn resolve_harness_session_root(
context: Option<&murmur_artifact::ContextConfig>,
capsule_name: &str,
inference: &murmur_artifact::InferenceConfig,
workdir: &Path,
) -> Option<PathBuf> {
if inference.transport != "process" {
return None;
}
let record = crate::conversation::resolve_record_name(context, capsule_name)?;
match crate::conversation::record_root(&record) {
Ok(root) => Some(root),
Err(reason) => {
crate::conversation::report(
workdir,
&format!(
"[harness-session] the conversation root for '{record}' could not be located \
({reason}); this capsule's conversations last only as long as it runs"
),
);
None
}
}
}
pub(crate) fn entry_file(root: &Path, context_id: &str) -> PathBuf {
root.join(context_id).join(HARNESS_SESSION_FILE_NAME)
}
pub(crate) fn new_harness_session_id() -> String {
uuid::Uuid::now_v7().to_string()
}
fn read_entry(path: &Path) -> Option<HarnessSessionEntry> {
let raw = std::fs::read(path).ok()?;
let entry: HarnessSessionEntry = serde_json::from_slice(&raw).ok()?;
if entry.kind != HARNESS_SESSION_TYPE || entry.session_id.is_empty() {
return None;
}
Some(entry)
}
fn remove_entry(path: &Path) -> Result<(), String> {
match std::fs::remove_file(path) {
Ok(()) => Ok(()),
Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(()),
Err(err) => Err(err.to_string()),
}
}
fn write_entry(path: &Path, entry: &HarnessSessionEntry) -> Result<(), String> {
let dir = path
.parent()
.ok_or_else(|| format!("{} has no parent directory", path.display()))?;
crate::conversation::ensure_context_dirs(dir)?;
let contents = serde_json::to_vec(entry)
.map_err(|err| format!("failed to encode the harness session entry: {err}"))?;
crate::murmur_home::write_private_file(path, &contents)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::murmur_home::{mode_of, run_with_home};
const HARNESS: &str = "fixture-harness";
const DRIVER: &str = "fixture-process-driver";
fn map_in(home: &Path) -> HarnessSessionMap {
HarnessSessionMap::new(
Some(home.join(".murmur/conversations/capsule")),
&home.join("session"),
)
}
#[test]
fn a_minted_session_id_is_a_bare_uuid() {
let id = new_harness_session_id();
assert_eq!(id.len(), 36, "{id}");
assert!(
id.chars()
.all(|c| c == '-' || c.is_ascii_digit() || ('a'..='f').contains(&c)),
"{id}"
);
assert_ne!(id, new_harness_session_id());
}
#[test]
fn entry_file_sits_beside_the_record() {
assert_eq!(
entry_file(Path::new("/root"), "ctx_1"),
Path::new("/root/ctx_1/harness-session.json")
);
}
#[test]
fn a_context_id_that_is_not_a_path_segment_gets_no_file() {
let map = HarnessSessionMap::new(Some(PathBuf::from("/root")), Path::new("/tmp"));
assert_eq!(map.entry_path("../escape"), None);
assert_eq!(map.entry_path(".hidden"), None);
assert!(map.entry_path("ctx_1").is_some());
}
#[test]
fn a_launch_with_no_root_still_threads_in_memory() {
let map = HarnessSessionMap::new(None, Path::new("/tmp"));
assert_eq!(map.root(), None);
assert_eq!(map.entry_path("ctx_1"), None);
map.put("ctx_1", "sess-1", HARNESS, DRIVER);
assert_eq!(map.get("ctx_1").as_deref(), Some("sess-1"));
assert_eq!(map.get("ctx_2"), None);
}
#[test]
fn forgetting_a_context_with_no_entry_drops_nothing() {
let map = HarnessSessionMap::new(None, Path::new("/tmp"));
assert_eq!(map.forget("ctx_1"), None);
}
#[test]
fn a_launch_with_no_root_still_forgets_in_memory() {
let map = HarnessSessionMap::new(None, Path::new("/tmp"));
map.put("ctx_1", "sess-1", HARNESS, DRIVER);
assert_eq!(map.forget("ctx_1").as_deref(), Some("sess-1"));
assert_eq!(map.get("ctx_1"), None);
assert_eq!(map.forget("ctx_1"), None);
}
#[test]
fn an_empty_session_id_is_not_remembered() {
let map = HarnessSessionMap::new(None, Path::new("/tmp"));
map.put("ctx_1", "", HARNESS, DRIVER);
assert_eq!(map.get("ctx_1"), None);
}
#[test]
fn harness_session_round_trip_through_a_home() {
let home = tempfile::tempdir().unwrap();
run_with_home(
"harness_session::tests::harness_session_round_trip_inner",
home.path(),
);
}
#[test]
#[ignore = "run by harness_session_round_trip_through_a_home"]
fn harness_session_round_trip_inner() {
if !crate::murmur_home::in_scratch_home() {
return;
}
let home = PathBuf::from(std::env::var("HOME").unwrap());
let map = map_in(&home);
map.put("ctx_1", "sess-1", HARNESS, DRIVER);
let path = map.entry_path("ctx_1").unwrap();
let entry = read_entry(&path).expect("the entry is readable");
assert_eq!(entry.kind, HARNESS_SESSION_TYPE);
assert_eq!(entry.session_id, "sess-1");
assert_eq!(entry.harness, HARNESS);
assert_eq!(entry.driver, DRIVER);
assert_eq!(entry.created_ms, entry.updated_ms);
assert_eq!(mode_of(&path), 0o600);
for dir in [
path.parent().unwrap(),
path.parent().unwrap().parent().unwrap(),
path.parent().unwrap().parent().unwrap().parent().unwrap(),
] {
assert_eq!(mode_of(dir), 0o700, "{}", dir.display());
}
let reread = map_in(&home);
assert_eq!(reread.get("ctx_1").as_deref(), Some("sess-1"));
std::thread::sleep(std::time::Duration::from_millis(2));
reread.put("ctx_1", "sess-2", HARNESS, DRIVER);
let updated = read_entry(&path).unwrap();
assert_eq!(updated.session_id, "sess-2");
assert_eq!(updated.created_ms, entry.created_ms);
assert!(updated.updated_ms > entry.created_ms);
assert_eq!(reread.forget("ctx_1").as_deref(), Some("sess-2"));
assert!(!path.exists(), "{}", path.display());
assert_eq!(map_in(&home).get("ctx_1"), None);
assert_eq!(reread.forget("ctx_1"), None);
}
#[test]
fn a_file_that_is_not_a_map_entry_reads_as_absent() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join(HARNESS_SESSION_FILE_NAME);
std::fs::write(&path, "{ not json").unwrap();
assert_eq!(read_entry(&path), None);
std::fs::write(&path, r#"{"type":"something.else","session_id":"s"}"#).unwrap();
assert_eq!(read_entry(&path), None);
std::fs::write(
&path,
r#"{"type":"murmur.harness-session","session_id":"","harness":"h","driver":"d","created_ms":1,"updated_ms":1}"#,
)
.unwrap();
assert_eq!(read_entry(&path), None);
assert_eq!(read_entry(&dir.path().join("absent.json")), None);
}
#[test]
fn an_unwritable_root_is_reported_once_and_survived() {
let dir = tempfile::tempdir().unwrap();
let blocked = dir.path().join("blocked");
std::fs::write(&blocked, b"not a directory").unwrap();
let workdir = dir.path().join("session");
std::fs::create_dir_all(&workdir).unwrap();
let map = HarnessSessionMap::new(Some(blocked.clone()), &workdir);
map.put("ctx_1", "sess-1", HARNESS, DRIVER);
map.put("ctx_1", "sess-2", HARNESS, DRIVER);
assert!(!blocked.is_dir());
assert_eq!(map.get("ctx_1").as_deref(), Some("sess-2"));
let log = std::fs::read_to_string(workdir.join("logs/bootstrap.log")).unwrap_or_default();
assert_eq!(
log.matches("[harness-session]").count(),
1,
"one report, not one per task:\n{log}"
);
}
}