Publish harness and TUI open-source
initial sync from the monorepo
This commit is contained in:
commit
c68e39f604
2734 changed files with 1437016 additions and 0 deletions
488
crates/codegen/xai-grok-memory/src/dream_lock.rs
Normal file
488
crates/codegen/xai-grok-memory/src/dream_lock.rs
Normal file
|
|
@ -0,0 +1,488 @@
|
|||
//! Dream lock file and session counting infrastructure.
|
||||
//!
|
||||
//! Coordination primitives for background memory consolidation ("dream"):
|
||||
//! - [`DreamLock`]: PID-based lock file with mtime tracking
|
||||
//! - [`sessions_since`]: counts session files modified after a given timestamp
|
||||
|
||||
use std::fs;
|
||||
use std::io;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::SystemTime;
|
||||
|
||||
const LOCK_FILE_NAME: &str = ".dream-lock";
|
||||
|
||||
/// Whether a process with the given PID is alive.
|
||||
///
|
||||
/// Local copy kept dependency-free of `crate::util` so the memory subsystem
|
||||
/// can be extracted into its own crate. Mirrors `crate::util::is_process_alive`.
|
||||
#[cfg(unix)]
|
||||
fn is_process_alive(pid: u32) -> bool {
|
||||
use nix::errno::Errno;
|
||||
use nix::sys::signal::kill;
|
||||
use nix::unistd::Pid;
|
||||
|
||||
// Signal 0 probes existence; EPERM means alive under a different UID.
|
||||
match kill(Pid::from_raw(pid as i32), None) {
|
||||
Ok(()) => true,
|
||||
Err(Errno::ESRCH) => false,
|
||||
Err(_) => true,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn is_process_alive(pid: u32) -> bool {
|
||||
use windows::Win32::Foundation::{CloseHandle, WAIT_TIMEOUT};
|
||||
use windows::Win32::System::Threading::{
|
||||
OpenProcess, PROCESS_SYNCHRONIZE, WaitForSingleObject,
|
||||
};
|
||||
|
||||
// SAFETY: OpenProcess returns Err on absence/permission failure;
|
||||
// PROCESS_SYNCHRONIZE is the minimum right needed for WaitForSingleObject.
|
||||
let Ok(handle) = (unsafe { OpenProcess(PROCESS_SYNCHRONIZE, false, pid) }) else {
|
||||
return false;
|
||||
};
|
||||
|
||||
// SAFETY: handle is valid; timeout 0 means "poll, don't block."
|
||||
let wait_result = unsafe { WaitForSingleObject(handle, 0) };
|
||||
// SAFETY: handle is owned by us; close regardless of wait result.
|
||||
let _ = unsafe { CloseHandle(handle) };
|
||||
|
||||
wait_result == WAIT_TIMEOUT
|
||||
}
|
||||
|
||||
pub struct DreamLock {
|
||||
path: PathBuf,
|
||||
}
|
||||
|
||||
impl DreamLock {
|
||||
pub fn new(workspace_dir: &Path) -> Self {
|
||||
Self {
|
||||
path: workspace_dir.join(LOCK_FILE_NAME),
|
||||
}
|
||||
}
|
||||
|
||||
/// Read the last consolidation timestamp (lock file mtime).
|
||||
/// Returns `None` if the lock file doesn't exist.
|
||||
pub fn last_consolidated_at(&self) -> io::Result<Option<SystemTime>> {
|
||||
match fs::metadata(&self.path) {
|
||||
Ok(meta) => Ok(Some(meta.modified()?)),
|
||||
Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(None),
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
}
|
||||
|
||||
/// Try to acquire the lock for consolidation.
|
||||
///
|
||||
/// Returns `Ok(Some(prior))` on success, where `prior` is the previous mtime
|
||||
/// (`None` if the file didn't exist). Pass to [`Self::rollback`] on failure.
|
||||
/// Returns `Ok(None)` if held by a live, non-stale process.
|
||||
///
|
||||
/// Reclaims stale locks when the holder PID is dead or age exceeds `stale_secs`.
|
||||
///
|
||||
/// Note: this is best-effort coordination, not mutual exclusion. The
|
||||
/// write-then-verify protocol reduces but cannot eliminate races — two
|
||||
/// processes may rarely both believe they acquired. Callers must tolerate
|
||||
/// duplicate consolidation (dream is idempotent).
|
||||
pub fn try_acquire(&self, stale_secs: u64) -> io::Result<Option<Option<SystemTime>>> {
|
||||
let prior = match fs::metadata(&self.path) {
|
||||
Ok(meta) => {
|
||||
let mtime = meta.modified()?;
|
||||
if let Ok(content) = fs::read_to_string(&self.path)
|
||||
&& let Ok(pid) = content.trim().parse::<u32>()
|
||||
{
|
||||
let age = SystemTime::now()
|
||||
.duration_since(mtime)
|
||||
.unwrap_or_default()
|
||||
.as_secs();
|
||||
if age < stale_secs && is_process_alive(pid) {
|
||||
return Ok(None);
|
||||
}
|
||||
}
|
||||
Some(mtime)
|
||||
}
|
||||
Err(e) if e.kind() == io::ErrorKind::NotFound => None,
|
||||
Err(e) => return Err(e),
|
||||
};
|
||||
|
||||
if let Some(parent) = self.path.parent() {
|
||||
fs::create_dir_all(parent)?;
|
||||
}
|
||||
let our_pid = std::process::id();
|
||||
fs::write(&self.path, our_pid.to_string())?;
|
||||
|
||||
// Re-read to verify we won the race
|
||||
let content = fs::read_to_string(&self.path)?;
|
||||
if content.trim().parse::<u32>().ok() == Some(our_pid) {
|
||||
Ok(Some(prior))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
/// Restore lock state after a failed dream.
|
||||
/// If `prior` is `None` (no prior file), deletes the lock file.
|
||||
pub fn rollback(&self, prior: Option<SystemTime>) -> io::Result<()> {
|
||||
match prior {
|
||||
None => {
|
||||
if let Err(e) = fs::remove_file(&self.path)
|
||||
&& e.kind() != io::ErrorKind::NotFound
|
||||
{
|
||||
return Err(e);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Some(mtime) => {
|
||||
// Clear the PID body so our alive PID doesn't block future reclaimers.
|
||||
fs::write(&self.path, "")?;
|
||||
let file = fs::File::options().write(true).open(&self.path)?;
|
||||
file.set_times(fs::FileTimes::new().set_modified(mtime))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Stamp the lock file with the current time to record a consolidation.
|
||||
pub fn record_consolidation(&self) -> io::Result<()> {
|
||||
if let Some(parent) = self.path.parent() {
|
||||
fs::create_dir_all(parent)?;
|
||||
}
|
||||
fs::write(&self.path, std::process::id().to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// Count session files modified after `since`, excluding the current session.
|
||||
///
|
||||
/// Returns sorted file stems of matching `.md` files in `sessions_dir`.
|
||||
pub fn sessions_since(
|
||||
sessions_dir: &Path,
|
||||
since: SystemTime,
|
||||
exclude_sid8: Option<&str>,
|
||||
) -> io::Result<Vec<String>> {
|
||||
let entries = match fs::read_dir(sessions_dir) {
|
||||
Ok(entries) => entries,
|
||||
Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(Vec::new()),
|
||||
Err(e) => return Err(e),
|
||||
};
|
||||
|
||||
let mut result = Vec::new();
|
||||
for entry in entries {
|
||||
let entry = entry?;
|
||||
let path = entry.path();
|
||||
|
||||
if path.extension().and_then(|e| e.to_str()) != Some("md") {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(exclude) = exclude_sid8
|
||||
&& path
|
||||
.file_stem()
|
||||
.and_then(|s| s.to_str())
|
||||
.is_some_and(|stem| stem.ends_with(exclude))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if entry.metadata()?.modified()? > since
|
||||
&& let Some(stem) = path.file_stem().and_then(|s| s.to_str())
|
||||
{
|
||||
result.push(stem.to_owned());
|
||||
}
|
||||
}
|
||||
|
||||
result.sort();
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use filetime::FileTime;
|
||||
use std::time::Duration;
|
||||
use tempfile::TempDir;
|
||||
|
||||
// --- DreamLock tests ---
|
||||
|
||||
#[test]
|
||||
fn no_file_means_no_prior_consolidation() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
let lock = DreamLock::new(dir.path());
|
||||
assert!(lock.last_consolidated_at().unwrap().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn acquire_on_empty_dir_writes_pid() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
let lock = DreamLock::new(dir.path());
|
||||
|
||||
let prior = lock.try_acquire(300).unwrap().expect("should acquire");
|
||||
assert!(prior.is_none(), "no prior file existed");
|
||||
|
||||
let content = fs::read_to_string(&lock.path).unwrap();
|
||||
assert_eq!(content, std::process::id().to_string());
|
||||
assert!(lock.last_consolidated_at().unwrap().is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rollback_none_deletes_file() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
let lock = DreamLock::new(dir.path());
|
||||
|
||||
let prior = lock.try_acquire(300).unwrap().unwrap();
|
||||
assert!(lock.path.exists());
|
||||
|
||||
lock.rollback(prior).unwrap();
|
||||
assert!(!lock.path.exists());
|
||||
assert!(lock.last_consolidated_at().unwrap().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rollback_restores_prior_mtime() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
let lock = DreamLock::new(dir.path());
|
||||
|
||||
let old_time = SystemTime::now() - Duration::from_secs(7200);
|
||||
fs::write(&lock.path, "4000000000").unwrap(); // dead PID
|
||||
filetime::set_file_mtime(&lock.path, FileTime::from_system_time(old_time)).unwrap();
|
||||
|
||||
let prior = lock
|
||||
.try_acquire(300)
|
||||
.unwrap()
|
||||
.expect("should reclaim dead PID");
|
||||
let prior_mtime = prior.expect("prior file existed");
|
||||
|
||||
// mtime after acquire is fresh (from fs::write)
|
||||
let fresh = lock.last_consolidated_at().unwrap().unwrap();
|
||||
let fresh_age = SystemTime::now().duration_since(fresh).unwrap_or_default();
|
||||
assert!(fresh_age.as_secs() < 5);
|
||||
|
||||
// Rollback restores old mtime
|
||||
lock.rollback(Some(prior_mtime)).unwrap();
|
||||
let restored = lock.last_consolidated_at().unwrap().unwrap();
|
||||
let drift = restored
|
||||
.duration_since(old_time)
|
||||
.or_else(|_| old_time.duration_since(restored))
|
||||
.unwrap();
|
||||
assert!(drift.as_secs() < 2, "mtime should be restored");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dead_pid_is_reclaimed() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
let lock = DreamLock::new(dir.path());
|
||||
|
||||
fs::write(&lock.path, "4000000000").unwrap();
|
||||
assert!(
|
||||
lock.try_acquire(300).unwrap().is_some(),
|
||||
"dead PID should be reclaimable"
|
||||
);
|
||||
|
||||
let content = fs::read_to_string(&lock.path).unwrap();
|
||||
assert_eq!(content, std::process::id().to_string());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn live_pid_blocks_acquisition() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
let lock = DreamLock::new(dir.path());
|
||||
|
||||
assert!(lock.try_acquire(300).unwrap().is_some(), "first acquire");
|
||||
assert!(
|
||||
lock.try_acquire(300).unwrap().is_none(),
|
||||
"second acquire should be blocked by live PID"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stale_age_allows_reclaim_even_if_alive() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
let lock = DreamLock::new(dir.path());
|
||||
|
||||
fs::write(&lock.path, std::process::id().to_string()).unwrap();
|
||||
let old = SystemTime::now() - Duration::from_secs(600);
|
||||
filetime::set_file_mtime(&lock.path, FileTime::from_system_time(old)).unwrap();
|
||||
|
||||
// stale_secs=300, age=600 → stale, should reclaim
|
||||
assert!(
|
||||
lock.try_acquire(300).unwrap().is_some(),
|
||||
"stale lock should be reclaimable"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn record_consolidation_creates_file() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
let lock = DreamLock::new(dir.path());
|
||||
|
||||
lock.record_consolidation().unwrap();
|
||||
assert!(lock.path.exists());
|
||||
|
||||
let age = SystemTime::now()
|
||||
.duration_since(lock.last_consolidated_at().unwrap().unwrap())
|
||||
.unwrap_or_default();
|
||||
assert!(age.as_secs() < 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn record_consolidation_updates_mtime() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
let lock = DreamLock::new(dir.path());
|
||||
|
||||
fs::write(&lock.path, "12345").unwrap();
|
||||
let old = SystemTime::now() - Duration::from_secs(7200);
|
||||
filetime::set_file_mtime(&lock.path, FileTime::from_system_time(old)).unwrap();
|
||||
|
||||
lock.record_consolidation().unwrap();
|
||||
|
||||
let age = SystemTime::now()
|
||||
.duration_since(lock.last_consolidated_at().unwrap().unwrap())
|
||||
.unwrap_or_default();
|
||||
assert!(age.as_secs() < 5, "mtime should be ~now");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn full_lifecycle_acquire_consolidate_blocks_reacquire() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
let lock = DreamLock::new(dir.path());
|
||||
|
||||
let prior = lock.try_acquire(300).unwrap().unwrap();
|
||||
assert!(prior.is_none());
|
||||
|
||||
lock.record_consolidation().unwrap();
|
||||
|
||||
assert!(
|
||||
lock.try_acquire(300).unwrap().is_none(),
|
||||
"fresh consolidation should block re-acquire"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rollback_on_nonexistent_file_is_noop() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
let lock = DreamLock::new(dir.path());
|
||||
lock.rollback(None).unwrap(); // no file to delete, should be fine
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corrupted_lock_body_is_reclaimable() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
let lock = DreamLock::new(dir.path());
|
||||
|
||||
fs::write(&lock.path, "not-a-pid").unwrap();
|
||||
assert!(
|
||||
lock.try_acquire(300).unwrap().is_some(),
|
||||
"unparseable PID should be reclaimable"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_lock_body_is_reclaimable() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
let lock = DreamLock::new(dir.path());
|
||||
|
||||
fs::write(&lock.path, "").unwrap();
|
||||
assert!(
|
||||
lock.try_acquire(300).unwrap().is_some(),
|
||||
"empty body should be reclaimable"
|
||||
);
|
||||
}
|
||||
|
||||
// --- sessions_since tests ---
|
||||
|
||||
fn write_session(dir: &Path, name: &str, age_secs: u64) {
|
||||
fs::create_dir_all(dir).unwrap();
|
||||
let path = dir.join(format!("{name}.md"));
|
||||
fs::write(&path, "test").unwrap();
|
||||
let t = SystemTime::now() - Duration::from_secs(age_secs);
|
||||
filetime::set_file_mtime(&path, FileTime::from_system_time(t)).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn filters_by_mtime() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
let sessions = dir.path().join("sessions");
|
||||
let cutoff = SystemTime::now() - Duration::from_secs(3600);
|
||||
|
||||
write_session(&sessions, "2026-01-01-proj-aaa11111", 1800); // 30min ago, after cutoff
|
||||
write_session(&sessions, "2025-12-31-proj-bbb22222", 7200); // 2h ago, before cutoff
|
||||
|
||||
let result = sessions_since(&sessions, cutoff, None).unwrap();
|
||||
assert_eq!(result, vec!["2026-01-01-proj-aaa11111"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mtime_at_exact_cutoff_is_excluded() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
let sessions = dir.path().join("sessions");
|
||||
let cutoff = SystemTime::now() - Duration::from_secs(3600);
|
||||
|
||||
// Set mtime to the exact cutoff value (not strictly after)
|
||||
fs::create_dir_all(&sessions).unwrap();
|
||||
let path = sessions.join("2026-01-01-proj-exact000.md");
|
||||
fs::write(&path, "test").unwrap();
|
||||
filetime::set_file_mtime(&path, FileTime::from_system_time(cutoff)).unwrap();
|
||||
|
||||
let result = sessions_since(&sessions, cutoff, None).unwrap();
|
||||
assert!(
|
||||
result.is_empty(),
|
||||
"mtime == cutoff should be excluded (strict >)"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn excludes_current_session() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
let sessions = dir.path().join("sessions");
|
||||
let cutoff = SystemTime::now() - Duration::from_secs(86400);
|
||||
|
||||
write_session(&sessions, "2026-01-01-proj-aaa11111", 100);
|
||||
write_session(&sessions, "2026-01-01-proj-bbb22222", 100);
|
||||
|
||||
let result = sessions_since(&sessions, cutoff, Some("bbb22222")).unwrap();
|
||||
assert_eq!(result, vec!["2026-01-01-proj-aaa11111"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_dir_returns_empty_vec() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
let sessions = dir.path().join("sessions");
|
||||
fs::create_dir_all(&sessions).unwrap();
|
||||
|
||||
let result = sessions_since(&sessions, SystemTime::UNIX_EPOCH, None).unwrap();
|
||||
assert!(result.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nonexistent_dir_returns_empty_vec() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
let sessions = dir.path().join("nonexistent");
|
||||
|
||||
let result = sessions_since(&sessions, SystemTime::UNIX_EPOCH, None).unwrap();
|
||||
assert!(result.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ignores_non_md_files() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
let sessions = dir.path().join("sessions");
|
||||
fs::create_dir_all(&sessions).unwrap();
|
||||
|
||||
write_session(&sessions, "2026-01-01-proj-aaa11111", 0);
|
||||
fs::write(sessions.join("notes.txt"), "not a session").unwrap();
|
||||
fs::write(sessions.join("data.json"), "{}").unwrap();
|
||||
|
||||
let result = sessions_since(&sessions, SystemTime::UNIX_EPOCH, None).unwrap();
|
||||
assert_eq!(result, vec!["2026-01-01-proj-aaa11111"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn returns_sorted_stems() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
let sessions = dir.path().join("sessions");
|
||||
|
||||
write_session(&sessions, "zzz-session", 0);
|
||||
write_session(&sessions, "aaa-session", 0);
|
||||
write_session(&sessions, "mmm-session", 0);
|
||||
|
||||
let result = sessions_since(&sessions, SystemTime::UNIX_EPOCH, None).unwrap();
|
||||
assert_eq!(result, vec!["aaa-session", "mmm-session", "zzz-session"]);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue