fix: finalize stopped subworker sessions
This commit is contained in:
@@ -72,13 +72,20 @@ impl Tool for EditTool {
|
||||
})
|
||||
.await
|
||||
.map_err(ToolsError::from)?;
|
||||
self.tracker.record_workdir_hash(&path, result.content_hash);
|
||||
let replacements = result.replacements;
|
||||
self.tracker.record_workdir_edit(
|
||||
&path,
|
||||
result.content_hash,
|
||||
replacements,
|
||||
params.new_string.lines().count(),
|
||||
params.old_string.lines().count(),
|
||||
);
|
||||
|
||||
let summary = format!(
|
||||
"Edited {} ({} replacement{})",
|
||||
path,
|
||||
result.replacements,
|
||||
if result.replacements == 1 { "" } else { "s" }
|
||||
replacements,
|
||||
if replacements == 1 { "" } else { "s" }
|
||||
);
|
||||
let preview = make_preview(¶ms.new_string, ¶ms.new_string);
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ pub use error::ToolsError;
|
||||
pub use glob::glob_tool;
|
||||
pub use grep::grep_tool;
|
||||
pub use read::read_tool;
|
||||
pub use tracker::Tracker;
|
||||
pub use tracker::{ChangeStat, Tracker};
|
||||
pub use view_image::view_image_tool;
|
||||
pub use web::{web_fetch_tool, web_search_tool};
|
||||
pub use write::write_tool;
|
||||
|
||||
@@ -119,12 +119,22 @@ fn normalize_path_lexically(path: &Path) -> PathBuf {
|
||||
normalized
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
|
||||
pub struct ChangeStat {
|
||||
pub added: u64,
|
||||
pub deleted: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct Inner {
|
||||
/// Hash of each file's last observed contents, keyed by canonical path.
|
||||
hashes: HashMap<PathBuf, ContentHash>,
|
||||
/// Line count paired with observations that included the file content.
|
||||
line_counts: HashMap<PathBuf, usize>,
|
||||
/// LRU list of touched files. Front = most recently touched.
|
||||
recency: VecDeque<PathBuf>,
|
||||
/// Successful Write/Edit mutations attributed to this session's tools.
|
||||
change_stat: ChangeStat,
|
||||
}
|
||||
|
||||
/// Canonical-path keyed tracker of file observations and their recency.
|
||||
@@ -187,8 +197,27 @@ impl Tracker {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn record_workdir_content(&self, path: &workdir::WorkdirPath, bytes: &[u8]) {
|
||||
self.record_workdir_hash(path, hash_bytes(bytes));
|
||||
pub fn record_workdir_content(&self, path: &workdir::WorkdirPath, content: &[u8]) {
|
||||
let key = PathBuf::from(path.as_str());
|
||||
let hash = hash_bytes(content);
|
||||
let line_count = String::from_utf8_lossy(content).lines().count();
|
||||
let mut inner = self.inner.lock().unwrap_or_else(|e| e.into_inner());
|
||||
inner.line_counts.insert(key.clone(), line_count);
|
||||
inner.hashes.insert(key.clone(), hash);
|
||||
inner.recency.retain(|candidate| candidate != &key);
|
||||
inner.recency.push_front(key);
|
||||
if inner.recency.len() > RECENCY_CAPACITY {
|
||||
inner.recency.pop_back();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn observed_workdir_line_count(&self, path: &workdir::WorkdirPath) -> Option<usize> {
|
||||
self.inner
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner())
|
||||
.line_counts
|
||||
.get(Path::new(path.as_str()))
|
||||
.copied()
|
||||
}
|
||||
|
||||
pub fn record_workdir_hash(&self, path: &workdir::WorkdirPath, hash: workdir::ContentHash) {
|
||||
@@ -202,6 +231,50 @@ impl Tracker {
|
||||
}
|
||||
}
|
||||
|
||||
/// Record a successful, session-attributable source mutation.
|
||||
///
|
||||
/// Callers supply line counts derived from the exact replacement accepted
|
||||
/// by a Write/Edit tool. Bash and external process mutations are excluded
|
||||
/// because this tracker cannot attribute them to one tool operation
|
||||
/// authoritatively.
|
||||
pub fn record_change(&self, added: usize, deleted: usize) {
|
||||
let mut inner = self.inner.lock().unwrap_or_else(|e| e.into_inner());
|
||||
inner.change_stat.added = inner.change_stat.added.saturating_add(added as u64);
|
||||
inner.change_stat.deleted = inner.change_stat.deleted.saturating_add(deleted as u64);
|
||||
}
|
||||
|
||||
pub fn record_workdir_edit(
|
||||
&self,
|
||||
path: &workdir::WorkdirPath,
|
||||
hash: workdir::ContentHash,
|
||||
replacements: usize,
|
||||
added_lines_per_replacement: usize,
|
||||
deleted_lines_per_replacement: usize,
|
||||
) {
|
||||
let added = added_lines_per_replacement.saturating_mul(replacements);
|
||||
let deleted = deleted_lines_per_replacement.saturating_mul(replacements);
|
||||
let key = PathBuf::from(path.as_str());
|
||||
let mut inner = self.inner.lock().unwrap_or_else(|e| e.into_inner());
|
||||
inner.change_stat.added = inner.change_stat.added.saturating_add(added as u64);
|
||||
inner.change_stat.deleted = inner.change_stat.deleted.saturating_add(deleted as u64);
|
||||
if let Some(line_count) = inner.line_counts.get_mut(&key) {
|
||||
*line_count = line_count.saturating_sub(deleted).saturating_add(added);
|
||||
}
|
||||
inner.hashes.insert(key.clone(), hash);
|
||||
inner.recency.retain(|candidate| candidate != &key);
|
||||
inner.recency.push_front(key);
|
||||
if inner.recency.len() > RECENCY_CAPACITY {
|
||||
inner.recency.pop_back();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn change_stat(&self) -> ChangeStat {
|
||||
self.inner
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner())
|
||||
.change_stat
|
||||
}
|
||||
|
||||
pub fn expected_workdir_hash(
|
||||
&self,
|
||||
path: &workdir::WorkdirPath,
|
||||
@@ -458,6 +531,21 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn change_stat_saturates_and_accumulates_tracked_mutations() {
|
||||
let tracker = Tracker::new();
|
||||
tracker.record_change(7, 3);
|
||||
tracker.record_change(5, 2);
|
||||
|
||||
assert_eq!(
|
||||
tracker.change_stat(),
|
||||
ChangeStat {
|
||||
added: 12,
|
||||
deleted: 5,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn mutation_guard_blocks_equivalent_paths_until_drop() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
|
||||
@@ -50,6 +50,7 @@ impl Tool for WriteTool {
|
||||
Err(error) => return Err(ToolsError::from(error).into()),
|
||||
};
|
||||
|
||||
let old_line_count = self.tracker.observed_workdir_line_count(&path).unwrap_or(0);
|
||||
let outcome = self
|
||||
.session
|
||||
.write(WriteRequest {
|
||||
@@ -60,6 +61,8 @@ impl Tool for WriteTool {
|
||||
.await
|
||||
.map_err(ToolsError::from)?;
|
||||
|
||||
self.tracker
|
||||
.record_change(params.content.lines().count(), old_line_count);
|
||||
self.tracker
|
||||
.record_workdir_content(&path, params.content.as_bytes());
|
||||
|
||||
|
||||
Reference in New Issue
Block a user