session: trace resolve/attach/compact points for #2707 wrong-session theory
This commit is contained in:
parent
7a826f9ee2
commit
1bbc09e9dd
4 changed files with 72 additions and 12 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
|
@ -1646,6 +1646,7 @@ dependencies = [
|
|||
"tempfile",
|
||||
"thiserror 2.0.18",
|
||||
"tokio",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ serde = { workspace = true }
|
|||
serde_json.workspace = true
|
||||
thiserror.workspace = true
|
||||
tokio.workspace = true
|
||||
tracing.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3"
|
||||
|
|
|
|||
|
|
@ -69,7 +69,14 @@ impl<P: CompactionPolicy> InfiniteSession<P> {
|
|||
// Resolve resume-vs-create once, up front, so `created` reflects the
|
||||
// session state at the START of the run: the reactive-retry path can't
|
||||
// read it from the retry (the session exists by then).
|
||||
let existed = self.store.find_by_title(&self.name).is_some();
|
||||
let resolved = self.store.find_by_title(&self.name);
|
||||
let existed = resolved.is_some();
|
||||
tracing::info!(
|
||||
title = %self.name,
|
||||
existed,
|
||||
path = resolved.as_deref().map(|p| p.display().to_string()).unwrap_or_default(),
|
||||
"InfiniteSession::run: resolved session before attach"
|
||||
);
|
||||
let meter = TelemetrySink::new(sink);
|
||||
let created = match self.attempt(config, prompt, &meter, existed).await {
|
||||
Ok(created) => created,
|
||||
|
|
@ -124,8 +131,19 @@ impl<P: CompactionPolicy> InfiniteSession<P> {
|
|||
///
|
||||
/// Propagates any error other than `SessionNotFound` from the compact run.
|
||||
pub async fn compact(&self, config: &Config, sink: &impl Sink) -> Result<()> {
|
||||
match Claude::run(config, &Attach::Resume(self.name.clone()), "/compact", sink).await {
|
||||
Err(Error::SessionNotFound) => Ok(()),
|
||||
let resolved = self.store.find_by_title(&self.name);
|
||||
tracing::info!(
|
||||
title = %self.name,
|
||||
path = resolved.as_deref().map(|p| p.display().to_string()).unwrap_or_default(),
|
||||
"InfiniteSession::compact: attaching /compact to this session"
|
||||
);
|
||||
let result =
|
||||
Claude::run(config, &Attach::Resume(self.name.clone()), "/compact", sink).await;
|
||||
match result {
|
||||
Err(Error::SessionNotFound) => {
|
||||
tracing::info!(title = %self.name, "InfiniteSession::compact: session not found, no-op");
|
||||
Ok(())
|
||||
}
|
||||
other => other,
|
||||
}
|
||||
}
|
||||
|
|
@ -148,11 +166,20 @@ impl<P: CompactionPolicy> InfiniteSession<P> {
|
|||
} else {
|
||||
Attach::Create(self.name.clone())
|
||||
};
|
||||
tracing::info!(
|
||||
title = %self.name,
|
||||
mode = if existed { "resume" } else { "create" },
|
||||
"InfiniteSession::attempt: attaching turn"
|
||||
);
|
||||
match Claude::run(config, &attach, prompt, sink).await {
|
||||
Ok(()) => Ok(!existed),
|
||||
// We thought it existed but the resume missed (raced an archive) —
|
||||
// self-heal by creating.
|
||||
Err(Error::SessionNotFound) if existed => {
|
||||
tracing::warn!(
|
||||
title = %self.name,
|
||||
"InfiniteSession::attempt: resume missed (raced an archive?) — self-healing via create"
|
||||
);
|
||||
Claude::run(config, &Attach::Create(self.name.clone()), prompt, sink).await?;
|
||||
Ok(true)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,12 +44,21 @@ impl SessionStore {
|
|||
/// Find the `<uuid>.jsonl` in the project dir whose `customTitle` equals
|
||||
/// `title` (the value `--name` sets, stored in a `custom-title` event).
|
||||
///
|
||||
/// Reads each session file line by line and stops at the first match, so a
|
||||
/// huge transcript isn't slurped into memory. Returns `None` if no session
|
||||
/// carries the title or the project dir is absent. Non-`.jsonl` files
|
||||
/// (including anything already archived to `*.jsonl.archived`) are skipped.
|
||||
/// Reads each session file line by line, so a huge transcript isn't
|
||||
/// slurped into memory. Returns `None` if no session carries the title or
|
||||
/// the project dir is absent. Non-`.jsonl` files (including anything
|
||||
/// already archived to `*.jsonl.archived`) are skipped.
|
||||
///
|
||||
/// Unlike the old first-match-wins scan, this keeps going past the first
|
||||
/// hit so a *second* same-titled file — which would otherwise resolve to
|
||||
/// whichever one `read_dir` happens to yield first, i.e. filesystem-order
|
||||
/// luck — logs a `tracing::warn!` instead of silently picking one. The
|
||||
/// first match found is still what's returned (unchanged resolution
|
||||
/// behaviour); this is a diagnostic for the "wrong session resumed"
|
||||
/// report, not a fix.
|
||||
#[must_use]
|
||||
pub fn find_by_title(&self, title: &str) -> Option<PathBuf> {
|
||||
let mut found: Option<PathBuf> = None;
|
||||
for entry in std::fs::read_dir(self.project_dir()).ok()?.flatten() {
|
||||
let path = entry.path();
|
||||
if path.extension().and_then(|e| e.to_str()) != Some("jsonl") {
|
||||
|
|
@ -58,15 +67,27 @@ impl SessionStore {
|
|||
let Ok(file) = std::fs::File::open(&path) else {
|
||||
continue;
|
||||
};
|
||||
if std::io::BufReader::new(file)
|
||||
let matches = std::io::BufReader::new(file)
|
||||
.lines()
|
||||
.map_while(std::result::Result::ok)
|
||||
.any(|line| line_sets_title(&line, title))
|
||||
{
|
||||
return Some(path);
|
||||
.any(|line| line_sets_title(&line, title));
|
||||
if !matches {
|
||||
continue;
|
||||
}
|
||||
match &found {
|
||||
None => found = Some(path),
|
||||
Some(first) => {
|
||||
tracing::warn!(
|
||||
title,
|
||||
first = %first.display(),
|
||||
also = %path.display(),
|
||||
"find_by_title: multiple session files share this title — \
|
||||
resuming the first one found (readdir order, not deterministic)"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
found
|
||||
}
|
||||
|
||||
/// Archive the session titled `title` by renaming its backing file
|
||||
|
|
@ -83,12 +104,22 @@ impl SessionStore {
|
|||
/// [`Error::Io`] if the rename fails.
|
||||
pub fn archive_by_title(&self, title: &str) -> Result<Option<PathBuf>> {
|
||||
let Some(path) = self.find_by_title(title) else {
|
||||
tracing::info!(
|
||||
title,
|
||||
"archive_by_title: no session file found for this title"
|
||||
);
|
||||
return Ok(None);
|
||||
};
|
||||
let mut target = path.clone().into_os_string();
|
||||
target.push(".archived");
|
||||
let target = PathBuf::from(target);
|
||||
std::fs::rename(&path, &target).map_err(Error::Io)?;
|
||||
tracing::info!(
|
||||
title,
|
||||
from = %path.display(),
|
||||
to = %target.display(),
|
||||
"archive_by_title: renamed session file"
|
||||
);
|
||||
Ok(Some(target))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue