fix(hive-claude): match session by parsed top-level customTitle, not transcript substring
This commit is contained in:
parent
8a78b8a464
commit
25540755f4
2 changed files with 82 additions and 2 deletions
|
|
@ -11,3 +11,6 @@ serde = { workspace = true }
|
|||
serde_json.workspace = true
|
||||
thiserror.workspace = true
|
||||
tokio.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3"
|
||||
|
|
|
|||
|
|
@ -50,7 +50,6 @@ impl SessionStore {
|
|||
/// (including anything already archived to `*.jsonl.archived`) are skipped.
|
||||
#[must_use]
|
||||
pub fn find_by_title(&self, title: &str) -> Option<PathBuf> {
|
||||
let marker = format!("\"customTitle\":\"{title}\"");
|
||||
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") {
|
||||
|
|
@ -62,7 +61,7 @@ impl SessionStore {
|
|||
if std::io::BufReader::new(file)
|
||||
.lines()
|
||||
.map_while(std::result::Result::ok)
|
||||
.any(|line| line.contains(&marker))
|
||||
.any(|line| line_sets_title(&line, title))
|
||||
{
|
||||
return Some(path);
|
||||
}
|
||||
|
|
@ -93,3 +92,81 @@ impl SessionStore {
|
|||
Ok(Some(target))
|
||||
}
|
||||
}
|
||||
|
||||
/// True if `line` is the session's `custom-title` event whose **top-level**
|
||||
/// `customTitle` field equals `title`.
|
||||
///
|
||||
/// Parsing the line (rather than substring-matching the whole transcript) is
|
||||
/// what makes this robust: a message that merely *quotes* the marker in its
|
||||
/// content has no top-level `customTitle` key, so it can't cause a false match
|
||||
/// on the wrong session file; the compact-vs-spaced JSON form is irrelevant;
|
||||
/// and titles containing `"` / `\` are handled by the parser. Verified shape
|
||||
/// (claude 2.1.x): `{"type":"custom-title","customTitle":"<title>",…}`.
|
||||
///
|
||||
/// The `contains` pre-check keeps the common case cheap — only the rare line
|
||||
/// mentioning `customTitle` is parsed as JSON, not every transcript line.
|
||||
fn line_sets_title(line: &str, title: &str) -> bool {
|
||||
if !line.contains("customTitle") {
|
||||
return false;
|
||||
}
|
||||
let Ok(value) = serde_json::from_str::<serde_json::Value>(line) else {
|
||||
return false;
|
||||
};
|
||||
value.get("customTitle").and_then(|t| t.as_str()) == Some(title)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn write(dir: &std::path::Path, name: &str, lines: &[&str]) {
|
||||
std::fs::write(dir.join(name), lines.join("\n")).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn find_by_title_matches_only_the_custom_title_event() {
|
||||
let home = tempfile::tempdir().unwrap();
|
||||
let cwd = std::path::Path::new("/agents/iris/state");
|
||||
let store = SessionStore::new(home.path(), cwd);
|
||||
let proj = store.project_dir();
|
||||
std::fs::create_dir_all(&proj).unwrap();
|
||||
|
||||
// The real titled session.
|
||||
write(
|
||||
&proj,
|
||||
"real.jsonl",
|
||||
&[
|
||||
r#"{"type":"summary","summary":"x"}"#,
|
||||
r#"{"type":"custom-title","customTitle":"iris","sessionId":"real"}"#,
|
||||
],
|
||||
);
|
||||
// A DIFFERENT session whose transcript merely quotes the marker string
|
||||
// in message content — must NOT match.
|
||||
write(
|
||||
&proj,
|
||||
"other.jsonl",
|
||||
&[
|
||||
r#"{"type":"custom-title","customTitle":"someone-else","sessionId":"other"}"#,
|
||||
r#"{"type":"assistant","message":{"content":[{"type":"text","text":"the file had \"customTitle\":\"iris\" in it"}]}}"#,
|
||||
],
|
||||
);
|
||||
|
||||
assert_eq!(store.find_by_title("iris"), Some(proj.join("real.jsonl")));
|
||||
assert_eq!(store.find_by_title("nobody"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn find_by_title_tolerates_spaced_json_and_escapes() {
|
||||
let home = tempfile::tempdir().unwrap();
|
||||
let store = SessionStore::new(home.path(), std::path::Path::new("/x"));
|
||||
let proj = store.project_dir();
|
||||
std::fs::create_dir_all(&proj).unwrap();
|
||||
// Spaced JSON form + a title needing JSON escaping.
|
||||
write(
|
||||
&proj,
|
||||
"s.jsonl",
|
||||
&[r#"{ "type": "custom-title", "customTitle": "a\"b" }"#],
|
||||
);
|
||||
assert_eq!(store.find_by_title("a\"b"), Some(proj.join("s.jsonl")));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue