1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
//! Completed workspace mutations, by session and then by the tool call that
//! made them.
//!
//! A call the loop stops before it returns has no result of its own. This
//! record is what lets it still report that it changed the workspace
//! (harn#9611). It depends on nothing above it, so the agent loop can read it
//! without reaching up into session state; recording against the *current*
//! call lives in `agent_sessions`, which knows which call that is.
use std::collections::{BTreeMap, BTreeSet};
use std::sync::{Mutex, OnceLock};
type Store = BTreeMap<String, BTreeMap<String, BTreeSet<String>>>;
static STORE: OnceLock<Mutex<Store>> = OnceLock::new();
fn store() -> &'static Mutex<Store> {
STORE.get_or_init(|| Mutex::new(BTreeMap::new()))
}
/// Record that `tool_call_id` in `session_id` finished mutating `path`. Call
/// it only after the mutation succeeded; a no-op when any argument is empty.
pub(crate) fn record(session_id: &str, tool_call_id: &str, path: &str) {
if session_id.is_empty() || tool_call_id.is_empty() || path.is_empty() {
return;
}
if let Ok(mut store) = store().lock() {
store
.entry(session_id.to_string())
.or_default()
.entry(tool_call_id.to_string())
.or_default()
.insert(path.to_string());
}
}
/// Read and release the paths the call finished mutating, sorted; empty when
/// it recorded none.
pub(crate) fn take(session_id: &str, tool_call_id: &str) -> Vec<String> {
let Ok(mut store) = store().lock() else {
return Vec::new();
};
let Some(calls) = store.get_mut(session_id) else {
return Vec::new();
};
let paths = calls.remove(tool_call_id).unwrap_or_default();
if calls.is_empty() {
store.remove(session_id);
}
paths.into_iter().collect()
}
/// Drop every call record a session still holds.
pub(crate) fn clear_session(session_id: &str) {
if let Ok(mut store) = store().lock() {
store.remove(session_id);
}
}
/// Drop every session's call records at process teardown.
pub(crate) fn clear_all() {
if let Ok(mut store) = store().lock() {
store.clear();
}
}