feat(token): add refresh token verification to TokenService
feat(bootstrap): create temporary settings and config files to prevent data loss refactor(edit_log): switch from Vec to VecDeque for efficient memory management fix(gateway): ensure store directories are created before starting the API server refactor(bgbash): implement a global singleton for BashControl feat(auth): enhance session authentication middleware to use SessionRepository fix(edit_log_repo): update to use VecDeque for in-memory edit log storage fix(memory_repo): add newline escaping for frontmatter fields fix(session_lock_repo): improve error handling for lock file operations fix(bash_tools): prevent path traversal in job_id argument refactor(delete): enforce empty directory deletion in file system tools fix(edit): optimize string replacement to only replace the first occurrence fix(git_cred): improve credential management with piped input to git commands feat(git_operator): add safety filter to block destructive git operations fix(shell): register background jobs in Bash control feat(spawn): add access tier specification for pipeline stages refactor(hive_mind): run directives concurrently for improved performance fix(auth): update refresh token verification in the refresh handler fix(chat): optimize LLM client usage based on model matching fix(conversations): enhance message deletion to target specific indices feat(api): add JWT authentication middleware for all API routes fix(state): implement refresh token verification in JwtTokenService fix(daemon): improve usage tracking with saturating addition fix(tui): handle compacted messages in the TUI state management
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
//! JSONL file–backed `EditLogRepository`.
|
||||
//! Stores `EditLog` as an append-only newline-delimited JSON file.
|
||||
|
||||
use std::collections::VecDeque;
|
||||
use std::io::{BufRead, BufReader, Write};
|
||||
use std::path::Path;
|
||||
|
||||
@@ -18,21 +19,21 @@ impl JsonlEditLogRepository {
|
||||
Self
|
||||
}
|
||||
|
||||
fn load_from_disk(path: &Path) -> Vec<EditLogEntry> {
|
||||
fn load_from_disk(path: &Path) -> VecDeque<EditLogEntry> {
|
||||
let Ok(file) = std::fs::File::open(path) else {
|
||||
return Vec::new();
|
||||
return VecDeque::new();
|
||||
};
|
||||
let reader = BufReader::new(file);
|
||||
let mut entries: Vec<EditLogEntry> = Vec::new();
|
||||
let mut entries: VecDeque<EditLogEntry> = VecDeque::new();
|
||||
for line in reader.lines() {
|
||||
let Ok(line) = line else {
|
||||
continue;
|
||||
};
|
||||
if let Ok(entry) = serde_json::from_str::<EditLogEntry>(&line) {
|
||||
if entries.len() >= MAX_MEMORY_ENTRIES {
|
||||
entries.remove(0);
|
||||
entries.pop_front();
|
||||
}
|
||||
entries.push(entry);
|
||||
entries.push_back(entry);
|
||||
}
|
||||
}
|
||||
entries
|
||||
@@ -74,14 +75,14 @@ impl EditLogRepository for JsonlEditLogRepository {
|
||||
file.write_all(line.as_bytes())?;
|
||||
file.sync_all()?;
|
||||
}
|
||||
log.entries.push(entry);
|
||||
log.entries.push_back(entry);
|
||||
if log.entries.len() > MAX_MEMORY_ENTRIES {
|
||||
log.entries.remove(0);
|
||||
log.entries.pop_front();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn entries(&self, log: &EditLog) -> Vec<EditLogEntry> {
|
||||
log.entries.clone()
|
||||
log.entries.clone().into_iter().collect()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,26 +17,37 @@ impl MarkdownMemoryRepository {
|
||||
Self
|
||||
}
|
||||
|
||||
/// Escape newlines in field values so they do not break the
|
||||
/// line-oriented frontmatter parser.
|
||||
fn escape_newlines(s: &str) -> String {
|
||||
s.replace('\n', "\\n")
|
||||
}
|
||||
|
||||
/// Unescape `\n` back to actual newlines after frontmatter parsing.
|
||||
fn unescape_newlines(s: &str) -> String {
|
||||
s.replace("\\n", "\n")
|
||||
}
|
||||
|
||||
fn build_frontmatter(memory: &Memory) -> String {
|
||||
let outcome_line = memory
|
||||
.outcome
|
||||
.as_ref()
|
||||
.map(|o| format!("outcome: {o}\n"))
|
||||
.map(|o| format!("outcome: {}\n", Self::escape_newlines(o)))
|
||||
.unwrap_or_default();
|
||||
let scope_line = memory
|
||||
.scope
|
||||
.as_ref()
|
||||
.map(|s| format!("scope: {s}\n"))
|
||||
.map(|s| format!("scope: {}\n", Self::escape_newlines(s)))
|
||||
.unwrap_or_default();
|
||||
let before_line = memory
|
||||
.before_snippet
|
||||
.as_ref()
|
||||
.map(|s| format!("before: {s}\n"))
|
||||
.map(|s| format!("before: {}\n", Self::escape_newlines(s)))
|
||||
.unwrap_or_default();
|
||||
let after_line = memory
|
||||
.after_snippet
|
||||
.as_ref()
|
||||
.map(|s| format!("after: {s}\n"))
|
||||
.map(|s| format!("after: {}\n", Self::escape_newlines(s)))
|
||||
.unwrap_or_default();
|
||||
let prov_line = if memory.provenances.is_empty() {
|
||||
String::new()
|
||||
@@ -101,14 +112,30 @@ impl MarkdownMemoryRepository {
|
||||
.get("updated_at")
|
||||
.and_then(|v| v.parse().ok())
|
||||
.unwrap_or(0),
|
||||
outcome: front.get("outcome").cloned().filter(|s| !s.is_empty()),
|
||||
outcome: front
|
||||
.get("outcome")
|
||||
.cloned()
|
||||
.filter(|s| !s.is_empty())
|
||||
.map(|s| Self::unescape_newlines(&s)),
|
||||
lifecycle: front
|
||||
.get("lifecycle")
|
||||
.cloned()
|
||||
.unwrap_or_else(|| "new".to_string()),
|
||||
scope: front.get("scope").cloned().filter(|s| !s.is_empty()),
|
||||
before_snippet: front.get("before").cloned().filter(|s| !s.is_empty()),
|
||||
after_snippet: front.get("after").cloned().filter(|s| !s.is_empty()),
|
||||
scope: front
|
||||
.get("scope")
|
||||
.cloned()
|
||||
.filter(|s| !s.is_empty())
|
||||
.map(|s| Self::unescape_newlines(&s)),
|
||||
before_snippet: front
|
||||
.get("before")
|
||||
.cloned()
|
||||
.filter(|s| !s.is_empty())
|
||||
.map(|s| Self::unescape_newlines(&s)),
|
||||
after_snippet: front
|
||||
.get("after")
|
||||
.cloned()
|
||||
.filter(|s| !s.is_empty())
|
||||
.map(|s| Self::unescape_newlines(&s)),
|
||||
provenances: front
|
||||
.get("provenances")
|
||||
.cloned()
|
||||
|
||||
@@ -36,7 +36,7 @@ impl SessionLockRepository for FileSystemSessionLockRepository {
|
||||
Err(e) => return Err(RepositoryError::Io(e)),
|
||||
}
|
||||
|
||||
let content = std::fs::read_to_string(&path).unwrap_or_default();
|
||||
let content = std::fs::read_to_string(&path).map_err(RepositoryError::Io)?;
|
||||
if let Ok(existing_pid) = content.trim().parse::<u32>() {
|
||||
if self.is_alive(existing_pid) {
|
||||
return Ok(false);
|
||||
@@ -46,10 +46,14 @@ impl SessionLockRepository for FileSystemSessionLockRepository {
|
||||
let tmp = path.with_extension("lock.tmp");
|
||||
{
|
||||
let mut tmp_file = std::fs::OpenOptions::new()
|
||||
.create(true)
|
||||
.truncate(true)
|
||||
.create_new(true)
|
||||
.write(true)
|
||||
.open(&tmp)?;
|
||||
.open(&tmp)
|
||||
.map_err(|_| {
|
||||
RepositoryError::Other(
|
||||
"another process is replacing the lock".to_string(),
|
||||
)
|
||||
})?;
|
||||
write!(tmp_file, "{pid}")?;
|
||||
tmp_file.sync_all()?;
|
||||
}
|
||||
@@ -62,7 +66,7 @@ impl SessionLockRepository for FileSystemSessionLockRepository {
|
||||
|
||||
fn unlock(&self, session_dir: &Path) -> Result<(), RepositoryError> {
|
||||
let path = session_dir.join(".lock");
|
||||
let _ = std::fs::remove_file(path);
|
||||
std::fs::remove_file(path)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user