Merge branch 'main' of git.arirex.me:iris/ai-app

This commit is contained in:
iris committed 2026-08-31 22:20:51 -04:00
commit c9d74b63f2
9 files changed
+154 -17

No files matched your search

+59 -1
View File
@@ -257,6 +257,30 @@ fn parse_listing(found: &str) -> Result<Vec<Importable>> {
(false, false) => InUse::Unknown,
};
}
// One row per session id, because the id is what everything downstream
// addresses: `--resume` takes it, deleting globs for it, and the
// in-flight registry is keyed on it. So two rows sharing an id are two
// rows that no operation can tell apart -- and the phone keys its list
// on it too, which turned this into a crash rather than a confusion.
//
// It is a real state of the machine, not corruption: resuming a session
// from a different working directory makes the CLI write a second file
// under that directory's project folder with the same id. One of the two
// is then usually a stub of a few hundred bytes and the other is the
// conversation somebody means.
//
// So the copy with the most in it wins, and the row's `cwd` comes from
// that same copy -- which is the directory `--resume` will find it under.
// Ties go to the more recent, and the *stub* is often the more recent, so
// the size has to be the first key rather than the tie-break.
sessions.sort_by(|a, b| {
b.lines
.cmp(&a.lines)
.then_with(|| b.modified.total_cmp(&a.modified))
});
let mut seen = std::collections::HashSet::new();
sessions.retain(|session| seen.insert(session.id.clone()));
// Most recent first, and only that. Naming was tried as the first key
// and is a worse list: it buries what somebody was just doing under
// everything they ever named, and the reason to open this screen is
@@ -672,12 +696,15 @@ pub async fn delete(transport: &Transport, id: &str) -> Result<()> {
// `context_of` below already resolved an id the cheap way; this is the
// same lookup, and the two now agree.
ensure!(is_session_id(id), "not a Claude Code session id: {id}");
// Every copy, not the first. The same id can name a file under two
// project directories -- see the de-duplication in `parse_listing` --
// and stopping at the first left the other behind, so the row came back
// on the next listing after a delete that had reported success.
let script = r#"
for f in "$HOME"/.claude/projects/*/"$1".jsonl; do
[ -f "$f" ] || continue
rm -f "$f" || exit 1
printf '%s\n' "$f"
exit 0
done
"#;
let launch = Launch::new(
@@ -852,6 +879,37 @@ pub async fn replay_after(
mod tests {
use super::*;
/// One session id, two files, one row.
///
/// Resuming a session from a different working directory makes the CLI
/// write a second file with the same id under that directory's project
/// folder, so this is an ordinary state of a machine rather than a
/// corrupt one. Everything downstream addresses a session by id, and
/// the phone keys its list on it, so two rows sharing one was a crash.
///
/// The stub is deliberately the *newer* of the two here, because that
/// is how the real case looked: ordering by recency alone picks the
/// near-empty copy and describes the session by the wrong cwd.
#[test]
fn a_session_recorded_under_two_projects_is_offered_once() {
let id = "3114dee1-2f95-4de0-9c04-3d6fcc594afe";
let said = r#"{"cwd":"/home/bob/repos/survey","message":{"role":"user","content":"the real conversation"}}"#;
let stub = r#"{"cwd":"/home/bob/repos/elsewhere","message":{"role":"user","content":"resumed here once"}}"#;
let listing = format!(
"LIVEKNOWN\n\
1000.0\t412\t160638\t\t/home/bob/.claude/projects/-home-bob-repos-survey/{id}.jsonl\t{said}\n\
2000.0\t4\t614\t\t/home/bob/.claude/projects/-home-bob-repos-elsewhere/{id}.jsonl\t{stub}\n"
);
let rows = parse_listing(&listing).expect("parse");
assert_eq!(rows.len(), 1, "one id is one row: {rows:#?}");
assert_eq!(rows[0].lines, 412, "the conversation, not the stub");
// The cwd has to come from the copy that was kept, because that is
// the directory `--resume` will find those 412 lines under.
assert_eq!(rows[0].cwd, "/home/bob/repos/survey");
}
/// The guard on the only thing this module ever puts in a glob.
///
/// Worth a test of its own because what it protects is a `rm`: `delete`