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:
asepharyana
2026-07-20 12:26:10 +07:00
parent 600ea041ef
commit a04651905f
26 changed files with 497 additions and 453 deletions
@@ -1,6 +1,7 @@
//! JSONL filebacked `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(())
}