Phase 6c fix: TagStore list/get see stamped tags #73

Merged
osobh merged 1 commits from phase-6c-fix-tags-list-stamped into main 2026-07-14 19:25:21 +00:00
+69 -4
View File
@@ -234,7 +234,15 @@ impl TagStore {
let path = self.tag_path(key); let path = self.tag_path(key);
let bytes = match tokio::fs::read(&path).await { let bytes = match tokio::fs::read(&path).await {
Ok(b) => b, Ok(b) => b,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(None), Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
// Phase 6c fix (2026-07-14): legacy tag file absent
// → fall through to the stamped store so modern pins
// are visible via the plain `get` API.
return Ok(self
.get_stamped(key)
.await?
.map(|s| s.value));
}
Err(e) => return Err(anyhow::Error::from(e)), Err(e) => return Err(anyhow::Error::from(e)),
}; };
let (parsed_key, value) = decode_record(&bytes).with_context(|| { let (parsed_key, value) = decode_record(&bytes).with_context(|| {
@@ -284,9 +292,13 @@ impl TagStore {
let tags_root = self.root.join("tags"); let tags_root = self.root.join("tags");
let mut entries: Vec<TagEntry> = Vec::new(); let mut entries: Vec<TagEntry> = Vec::new();
let mut top = tokio::fs::read_dir(&tags_root) let mut top = match tokio::fs::read_dir(&tags_root).await {
.await Ok(t) => t,
.with_context(|| format!("reading {}", tags_root.display()))?; Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
return self.list_stamped_only().await
}
Err(e) => return Err(e).with_context(|| format!("reading {}", tags_root.display())),
};
while let Some(bucket) = top.next_entry().await? { while let Some(bucket) = top.next_entry().await? {
if !bucket.file_type().await?.is_dir() { if !bucket.file_type().await?.is_dir() {
continue; continue;
@@ -305,10 +317,63 @@ impl TagStore {
} }
} }
} }
// Phase 6c fix (2026-07-14): fold in stamped tags too.
// Pin/PutTagVersioned writes here; list() must see them or
// downstream consumers (FUSE tag layer, humans running
// `list-tags`) miss modern pins entirely.
let stamped = self.list_stamped_only().await?;
let seen: std::collections::HashSet<String> =
entries.iter().map(|e| e.key.clone()).collect();
for e in stamped {
if !seen.contains(&e.key) {
entries.push(e);
}
}
entries.sort_by(|a, b| a.key.cmp(&b.key)); entries.sort_by(|a, b| a.key.cmp(&b.key));
Ok(entries) Ok(entries)
} }
async fn list_stamped_only(&self) -> Result<Vec<TagEntry>> {
let stamped_root = self.root.join("tags-v2");
let mut entries: Vec<TagEntry> = Vec::new();
let mut top = match tokio::fs::read_dir(&stamped_root).await {
Ok(t) => t,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(entries),
Err(e) => {
return Err(e).with_context(|| format!("reading {}", stamped_root.display()))
}
};
while let Some(bucket) = top.next_entry().await? {
if !bucket.file_type().await?.is_dir() {
continue;
}
let mut inner = tokio::fs::read_dir(bucket.path()).await?;
while let Some(entry) = inner.next_entry().await? {
if !entry.file_type().await?.is_file() {
continue;
}
if entry
.file_name()
.to_str()
.is_none_or(|n| !n.ends_with(".svtag"))
{
continue;
}
let bytes = match tokio::fs::read(entry.path()).await {
Ok(b) => b,
Err(_) => continue,
};
if let Ok((key, stamped)) = decode_stamped_record(&bytes) {
entries.push(TagEntry {
key,
value_hex: hex32(&stamped.value),
});
}
}
}
Ok(entries)
}
/// Phase 4 (2026-07-13): union of every 32-byte value referenced /// Phase 4 (2026-07-13): union of every 32-byte value referenced
/// by a tag in either the legacy `tags/` or the Phase-3c /// by a tag in either the legacy `tags/` or the Phase-3c
/// `tags-v2/` namespace. Feeds pin-aware eviction: any blob whose /// `tags-v2/` namespace. Feeds pin-aware eviction: any blob whose