use crate::{
instructions::InstructionFile,
sessions::{
MAX_METADATA_VISIT_BYTES, MAX_METADATA_VISIT_LINES, Session, SessionEventKind,
report_session_diagnostic,
},
};
use std::{collections::HashSet, path::PathBuf};
#[derive(Debug, Default)]
pub(crate) struct SubdirInstructionState {
claimed: HashSet<PathBuf>,
}
impl SubdirInstructionState {
pub(crate) fn new(initial_instructions: &[InstructionFile], session: Option<&Session>) -> Self {
let mut state = Self::default();
state.seed_initial(initial_instructions);
if let Some(session) = session {
state.seed_from_full_session_log(session);
}
state
}
pub(crate) fn claimed_mut(&mut self) -> &mut HashSet<PathBuf> {
&mut self.claimed
}
pub(crate) fn seed_initial(&mut self, instructions: &[InstructionFile]) {
for instruction in instructions {
let canonical = instruction
.path
.canonicalize()
.unwrap_or_else(|_| instruction.path.clone());
self.claimed.insert(canonical);
}
}
pub(crate) fn seed_from_full_session_log(&mut self, session: &Session) {
let events = match session
.read_events_tolerant_bounded(MAX_METADATA_VISIT_LINES, MAX_METADATA_VISIT_BYTES)
{
Ok(events) => events,
Err(error) => {
report_session_diagnostic(
crate::sessions::SessionDiagnosticOperation::Replay,
session.path(),
&error,
);
return;
}
};
for event in events.events {
match event.kind() {
Some(SessionEventKind::SubdirInstructionLoad) => {
if event
.payload
.get("source")
.and_then(serde_json::Value::as_str)
== Some("subdir_agents")
&& event
.payload
.get("status")
.and_then(serde_json::Value::as_str)
== Some("success")
&& let Some(path) = event
.payload
.get("path")
.and_then(serde_json::Value::as_str)
{
self.claimed.insert(PathBuf::from(path));
}
}
Some(SessionEventKind::ProviderContextItem) => {
if event
.payload
.get("source")
.and_then(serde_json::Value::as_str)
== Some("subdir_agents")
&& let Some(path) = event
.payload
.get("path")
.and_then(serde_json::Value::as_str)
{
self.claimed.insert(PathBuf::from(path));
}
}
_ => {}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
instructions::{InstructionFile, InstructionSourceKind},
sessions::{SessionManager, record_session_event},
};
use serde_json::json;
#[test]
fn oversized_session_log_does_not_seed_subdir_claims() {
let temp = tempfile::TempDir::new().unwrap();
let session = SessionManager::new(temp.path().join("sessions"))
.open("oversized")
.unwrap();
std::fs::create_dir_all(session.path().parent().unwrap()).unwrap();
std::fs::OpenOptions::new()
.create(true)
.write(true)
.truncate(true)
.open(session.path())
.unwrap()
.set_len((crate::sessions::MAX_METADATA_VISIT_BYTES as u64) + 1)
.unwrap();
let state = SubdirInstructionState::new(&[], Some(&session));
assert!(state.claimed.is_empty());
}
#[test]
fn seeds_initial_and_full_session_audit_paths() {
let temp = tempfile::TempDir::new().unwrap();
let session = SessionManager::new(temp.path().join("sessions"))
.create()
.unwrap();
let initial = temp.path().join("AGENTS.md");
std::fs::write(&initial, "root").unwrap();
let subdir = temp.path().join("a/AGENTS.md");
std::fs::create_dir_all(subdir.parent().unwrap()).unwrap();
std::fs::write(&subdir, "a").unwrap();
record_session_event(
Some(&session),
temp.path(),
SessionEventKind::SubdirInstructionLoad,
json!({
"source":"subdir_agents",
"path": subdir.canonicalize().unwrap(),
"status":"success",
"bytes":1,
"content_hash":"sha256:test"
}),
)
.unwrap();
let state = SubdirInstructionState::new(
&[InstructionFile {
kind: InstructionSourceKind::Repository,
path: initial.clone(),
content: "root".to_string(),
}],
Some(&session),
);
assert!(state.claimed.contains(&initial.canonicalize().unwrap()));
assert!(state.claimed.contains(&subdir.canonicalize().unwrap()));
}
}