From d34d36a48bfe03640d8e6aa6dab4e93fe27ddd6d Mon Sep 17 00:00:00 2001 From: Eureka Date: Wed, 11 Nov 2026 19:14:03 +0000 Subject: [PATCH] sources: chat cursor and purge-tombstone keys are never read as repo names by the forge retire Co-Authored-By: Claude Sonnet 5.5 --- crates/huginn-daemon/src/sources/chat.rs | 17 +++++++++++++++++ crates/huginn-daemon/src/sources/mod.rs | 3 ++- crates/huginn-daemon/src/sources/tests.rs | 21 +++++++++++++++++++++ 3 files changed, 40 insertions(+), 1 deletion(-) create mode 100644 crates/huginn-daemon/src/sources/chat.rs diff --git a/crates/huginn-daemon/src/sources/chat.rs b/crates/huginn-daemon/src/sources/chat.rs new file mode 100644 index 0000000..5efa0ce --- /dev/null +++ b/crates/huginn-daemon/src/sources/chat.rs @@ -0,0 +1,17 @@ +//! Chat ingest: joined-room history into the shared source collection, beside +//! the forge's repos. `refresh_chat` is the only writer of `corpus=chat` points +//! and of the cursors object's chat keys. + +/// A chat cursor is the key `chat:`; a forge repo name cannot contain +/// a colon. The only definition of the prefix. +pub const CHAT_CURSOR_PREFIX: &str = "chat:"; + +/// A steward's purge tombstone is the key `chat-purge:`. The only +/// definition of the prefix. +pub const CHAT_PURGE_PREFIX: &str = "chat-purge:"; + +/// Whether a cursors-object key is chat's (a room cursor or a purge tombstone) +/// and not a forge repo name. The only test that tells the two apart. +pub fn is_chat_key(key: &str) -> bool { + key.starts_with(CHAT_CURSOR_PREFIX) || key.starts_with(CHAT_PURGE_PREFIX) +} diff --git a/crates/huginn-daemon/src/sources/mod.rs b/crates/huginn-daemon/src/sources/mod.rs index 963296b..22c010b 100644 --- a/crates/huginn-daemon/src/sources/mod.rs +++ b/crates/huginn-daemon/src/sources/mod.rs @@ -19,6 +19,7 @@ //! the delete and the upserts leaves no cursor, and the repo is ingested whole. //! The cursors object is written whole by this job alone, once per repo. +pub mod chat; pub mod chunk; pub mod forge; pub mod git; @@ -395,7 +396,7 @@ fn retire( failed: &mut Vec, ) { let listed = |name: &str| catalog.iter().any(|repo| repo.name == name); - let mut gone: BTreeSet = cursors.keys().filter(|name| !listed(name)).cloned().collect(); + let mut gone: BTreeSet = cursors.keys().filter(|name| !chat::is_chat_key(name) && !listed(name)).cloned().collect(); if let Ok(entries) = std::fs::read_dir(mirrors) { for entry in entries.flatten() { if let Some(name) = entry.file_name().to_string_lossy().strip_suffix(".git") diff --git a/crates/huginn-daemon/src/sources/tests.rs b/crates/huginn-daemon/src/sources/tests.rs index fbb364e..a7203a4 100644 --- a/crates/huginn-daemon/src/sources/tests.rs +++ b/crates/huginn-daemon/src/sources/tests.rs @@ -1155,3 +1155,24 @@ fn a_window_asks_the_index_only_and_needs_no_embedder() { let signature = &signature[..signature.find('{').unwrap()]; assert!(!signature.contains("Embedder"), "{signature}"); } + +/// A chat cursor and a purge tombstone share the forge's cursors object; the +/// forge retire never reads either as a repo name, and still retires a repo. +#[test] +fn chat_cursor_and_purge_tombstone_keys_survive_the_forge_retire() { + let (keep, gone) = (Remote::new(), Remote::new()); + keep.commit(&[("a.rs", b"a\n")], &[]); + gone.commit(&[("b.rs", b"b\n")], &[]); + let mut rig = Rig::new(); + rig.converged(&mut FakeForge::of(&[("Keep", &keep), ("Gone", &gone)])); + let mut cursors = rig.index.cursors(); + cursors.insert("chat:!r:s".into(), "1:$e".into()); + cursors.insert("chat-purge:!r:s".into(), "1..2".into()); + rig.index.set_cursors(&cursors).unwrap(); + rig.index.upsert_chat(&[chat_point("!r:s", None, 0, 4, &[("$e", 5)])]).unwrap(); + rig.converged(&mut FakeForge::of(&[("Keep", &keep)])); + let after = rig.index.cursors(); + assert_eq!(after.keys().map(String::as_str).collect::>(), ["Keep", "chat-purge:!r:s", "chat:!r:s"], "Gone retired; both chat keys kept"); + assert!(rig.index.state.lock().unwrap().deletes.iter().all(|(repo, _)| !repo.starts_with("chat"))); + assert_eq!(rig.index.state.lock().unwrap().chat.len(), 1); +}