agrep 0.4.0__tar.gz → 0.4.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {agrep-0.4.0 → agrep-0.4.2}/Cargo.lock +2 -2
- {agrep-0.4.0 → agrep-0.4.2}/Cargo.toml +1 -1
- {agrep-0.4.0 → agrep-0.4.2}/PKG-INFO +1 -1
- {agrep-0.4.0 → agrep-0.4.2}/agrep/__init__.py +1 -1
- {agrep-0.4.0 → agrep-0.4.2}/crates/agrep-cli/src/index_lock.rs +6 -0
- {agrep-0.4.0 → agrep-0.4.2}/crates/agrep-cli/src/main.rs +341 -87
- {agrep-0.4.0 → agrep-0.4.2}/py/audit.py +6 -5
- {agrep-0.4.0 → agrep-0.4.2}/py/doctor.py +7 -4
- {agrep-0.4.0 → agrep-0.4.2}/py/embed.py +10 -4
- {agrep-0.4.0 → agrep-0.4.2}/py/indexd_runtime.py +8 -4
- {agrep-0.4.0 → agrep-0.4.2}/py/semantic.py +4 -0
- {agrep-0.4.0 → agrep-0.4.2}/LICENSE +0 -0
- {agrep-0.4.0 → agrep-0.4.2}/README.md +0 -0
- {agrep-0.4.0 → agrep-0.4.2}/THIRD_PARTY_LICENSES.txt +0 -0
- {agrep-0.4.0 → agrep-0.4.2}/agrep/__main__.py +0 -0
- {agrep-0.4.0 → agrep-0.4.2}/cli.py +0 -0
- {agrep-0.4.0 → agrep-0.4.2}/crates/agrep-cli/Cargo.toml +0 -0
- {agrep-0.4.0 → agrep-0.4.2}/crates/agrep-core/Cargo.toml +0 -0
- {agrep-0.4.0 → agrep-0.4.2}/crates/agrep-core/src/boundary_rank.rs +0 -0
- {agrep-0.4.0 → agrep-0.4.2}/crates/agrep-core/src/boundary_stats.rs +0 -0
- {agrep-0.4.0 → agrep-0.4.2}/crates/agrep-core/src/cache.rs +0 -0
- {agrep-0.4.0 → agrep-0.4.2}/crates/agrep-core/src/emit.rs +0 -0
- {agrep-0.4.0 → agrep-0.4.2}/crates/agrep-core/src/fallback_scan.rs +0 -0
- {agrep-0.4.0 → agrep-0.4.2}/crates/agrep-core/src/ingest/antigravity.rs +0 -0
- {agrep-0.4.0 → agrep-0.4.2}/crates/agrep-core/src/ingest/claude.rs +0 -0
- {agrep-0.4.0 → agrep-0.4.2}/crates/agrep-core/src/ingest/cline.rs +0 -0
- {agrep-0.4.0 → agrep-0.4.2}/crates/agrep-core/src/ingest/codex.rs +0 -0
- {agrep-0.4.0 → agrep-0.4.2}/crates/agrep-core/src/ingest/crush.rs +0 -0
- {agrep-0.4.0 → agrep-0.4.2}/crates/agrep-core/src/ingest/cursor.rs +0 -0
- {agrep-0.4.0 → agrep-0.4.2}/crates/agrep-core/src/ingest/gemini.rs +0 -0
- {agrep-0.4.0 → agrep-0.4.2}/crates/agrep-core/src/ingest/kimi.rs +0 -0
- {agrep-0.4.0 → agrep-0.4.2}/crates/agrep-core/src/ingest/mod.rs +0 -0
- {agrep-0.4.0 → agrep-0.4.2}/crates/agrep-core/src/ingest/opencode.rs +0 -0
- {agrep-0.4.0 → agrep-0.4.2}/crates/agrep-core/src/ingest/parse_guard.rs +0 -0
- {agrep-0.4.0 → agrep-0.4.2}/crates/agrep-core/src/ingest/parse_timestamp.rs +0 -0
- {agrep-0.4.0 → agrep-0.4.2}/crates/agrep-core/src/ingest/pi.rs +0 -0
- {agrep-0.4.0 → agrep-0.4.2}/crates/agrep-core/src/ingest/registry.rs +0 -0
- {agrep-0.4.0 → agrep-0.4.2}/crates/agrep-core/src/ingest_cache.rs +0 -0
- {agrep-0.4.0 → agrep-0.4.2}/crates/agrep-core/src/intake.rs +0 -0
- {agrep-0.4.0 → agrep-0.4.2}/crates/agrep-core/src/lib.rs +0 -0
- {agrep-0.4.0 → agrep-0.4.2}/crates/agrep-core/src/mapped_file.rs +0 -0
- {agrep-0.4.0 → agrep-0.4.2}/crates/agrep-core/src/model.rs +0 -0
- {agrep-0.4.0 → agrep-0.4.2}/crates/agrep-core/src/row_class.rs +0 -0
- {agrep-0.4.0 → agrep-0.4.2}/crates/agrep-core/src/semantic_q8.rs +0 -0
- {agrep-0.4.0 → agrep-0.4.2}/crates/agrep-core/src/unicode_v16.rs +0 -0
- {agrep-0.4.0 → agrep-0.4.2}/hatch_build.py +0 -0
- {agrep-0.4.0 → agrep-0.4.2}/hatch_sdist_build.py +0 -0
- {agrep-0.4.0 → agrep-0.4.2}/py/README.md +0 -0
- {agrep-0.4.0 → agrep-0.4.2}/py/archive.py +0 -0
- {agrep-0.4.0 → agrep-0.4.2}/py/around.py +0 -0
- {agrep-0.4.0 → agrep-0.4.2}/py/ask.py +0 -0
- {agrep-0.4.0 → agrep-0.4.2}/py/boundary_rank.py +0 -0
- {agrep-0.4.0 → agrep-0.4.2}/py/common.py +0 -0
- {agrep-0.4.0 → agrep-0.4.2}/py/compact.py +0 -0
- {agrep-0.4.0 → agrep-0.4.2}/py/conceptpair.py +0 -0
- {agrep-0.4.0 → agrep-0.4.2}/py/console.py +0 -0
- {agrep-0.4.0 → agrep-0.4.2}/py/corpusdb.py +0 -0
- {agrep-0.4.0 → agrep-0.4.2}/py/display_policy.py +0 -0
- {agrep-0.4.0 → agrep-0.4.2}/py/dist.py +0 -0
- {agrep-0.4.0 → agrep-0.4.2}/py/embedder.py +0 -0
- {agrep-0.4.0 → agrep-0.4.2}/py/embedding_segments.py +0 -0
- {agrep-0.4.0 → agrep-0.4.2}/py/embedding_store.py +0 -0
- {agrep-0.4.0 → agrep-0.4.2}/py/events.py +0 -0
- {agrep-0.4.0 → agrep-0.4.2}/py/explore.py +0 -0
- {agrep-0.4.0 → agrep-0.4.2}/py/fileops.py +0 -0
- {agrep-0.4.0 → agrep-0.4.2}/py/hookinstall.py +0 -0
- {agrep-0.4.0 → agrep-0.4.2}/py/hookless/__init__.py +0 -0
- {agrep-0.4.0 → agrep-0.4.2}/py/hookless/_log.py +0 -0
- {agrep-0.4.0 → agrep-0.4.2}/py/hookless/agent_registry.json +0 -0
- {agrep-0.4.0 → agrep-0.4.2}/py/hookless/capture.py +0 -0
- {agrep-0.4.0 → agrep-0.4.2}/py/hookless/fswatch.py +0 -0
- {agrep-0.4.0 → agrep-0.4.2}/py/hookless/live.py +0 -0
- {agrep-0.4.0 → agrep-0.4.2}/py/hookless/locators.py +0 -0
- {agrep-0.4.0 → agrep-0.4.2}/py/hookless/native.py +0 -0
- {agrep-0.4.0 → agrep-0.4.2}/py/hookless/proc.py +0 -0
- {agrep-0.4.0 → agrep-0.4.2}/py/hookless/procscan.py +0 -0
- {agrep-0.4.0 → agrep-0.4.2}/py/hookless/registry.py +0 -0
- {agrep-0.4.0 → agrep-0.4.2}/py/hooks/codex-postcompact.md +0 -0
- {agrep-0.4.0 → agrep-0.4.2}/py/hooks/codex_compact_payload.py +0 -0
- {agrep-0.4.0 → agrep-0.4.2}/py/hooks/compact-contract.md +0 -0
- {agrep-0.4.0 → agrep-0.4.2}/py/hooks/pi_postcompact.ts +0 -0
- {agrep-0.4.0 → agrep-0.4.2}/py/index_lock.py +0 -0
- {agrep-0.4.0 → agrep-0.4.2}/py/indexd.py +0 -0
- {agrep-0.4.0 → agrep-0.4.2}/py/indexer.py +0 -0
- {agrep-0.4.0 → agrep-0.4.2}/py/install_lag.py +0 -0
- {agrep-0.4.0 → agrep-0.4.2}/py/legacy_cleanup.py +0 -0
- {agrep-0.4.0 → agrep-0.4.2}/py/lifetime.py +0 -0
- {agrep-0.4.0 → agrep-0.4.2}/py/livetui.py +0 -0
- {agrep-0.4.0 → agrep-0.4.2}/py/mlx_embed.py +0 -0
- {agrep-0.4.0 → agrep-0.4.2}/py/mlx_modernbert.py +0 -0
- {agrep-0.4.0 → agrep-0.4.2}/py/nudge_codex.md +0 -0
- {agrep-0.4.0 → agrep-0.4.2}/py/nudge_default.md +0 -0
- {agrep-0.4.0 → agrep-0.4.2}/py/ownerfile.py +0 -0
- {agrep-0.4.0 → agrep-0.4.2}/py/postcompact.py +0 -0
- {agrep-0.4.0 → agrep-0.4.2}/py/proc.py +0 -0
- {agrep-0.4.0 → agrep-0.4.2}/py/recall.py +0 -0
- {agrep-0.4.0 → agrep-0.4.2}/py/regex_guard.py +0 -0
- {agrep-0.4.0 → agrep-0.4.2}/py/removal_fence.py +0 -0
- {agrep-0.4.0 → agrep-0.4.2}/py/resident.py +0 -0
- {agrep-0.4.0 → agrep-0.4.2}/py/resources.py +0 -0
- {agrep-0.4.0 → agrep-0.4.2}/py/resume.py +0 -0
- {agrep-0.4.0 → agrep-0.4.2}/py/runtime_manifest.json +0 -0
- {agrep-0.4.0 → agrep-0.4.2}/py/search.py +0 -0
- {agrep-0.4.0 → agrep-0.4.2}/py/segment_query.py +0 -0
- {agrep-0.4.0 → agrep-0.4.2}/py/semantic_q8.py +0 -0
- {agrep-0.4.0 → agrep-0.4.2}/py/semantic_segment_build.py +0 -0
- {agrep-0.4.0 → agrep-0.4.2}/py/semantic_segment_compact.py +0 -0
- {agrep-0.4.0 → agrep-0.4.2}/py/semworker.py +0 -0
- {agrep-0.4.0 → agrep-0.4.2}/py/server.py +0 -0
- {agrep-0.4.0 → agrep-0.4.2}/py/session_context.py +0 -0
- {agrep-0.4.0 → agrep-0.4.2}/py/settings.py +0 -0
- {agrep-0.4.0 → agrep-0.4.2}/py/summary.py +0 -0
- {agrep-0.4.0 → agrep-0.4.2}/py/surface_policy.py +0 -0
- {agrep-0.4.0 → agrep-0.4.2}/py/tail.py +0 -0
- {agrep-0.4.0 → agrep-0.4.2}/py/teach.py +0 -0
- {agrep-0.4.0 → agrep-0.4.2}/py/why.py +0 -0
- {agrep-0.4.0 → agrep-0.4.2}/py/winjob.py +0 -0
- {agrep-0.4.0 → agrep-0.4.2}/pyproject.toml +0 -0
- {agrep-0.4.0 → agrep-0.4.2}/reindex.py +0 -0
- {agrep-0.4.0 → agrep-0.4.2}/rust-toolchain.toml +0 -0
- {agrep-0.4.0 → agrep-0.4.2}/sdist_sources.py +0 -0
- {agrep-0.4.0 → agrep-0.4.2}/web/app.html +0 -0
|
@@ -10,7 +10,7 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
|
|
|
10
10
|
|
|
11
11
|
[[package]]
|
|
12
12
|
name = "agrep-core"
|
|
13
|
-
version = "0.4.
|
|
13
|
+
version = "0.4.2"
|
|
14
14
|
dependencies = [
|
|
15
15
|
"anyhow",
|
|
16
16
|
"bincode",
|
|
@@ -30,7 +30,7 @@ dependencies = [
|
|
|
30
30
|
|
|
31
31
|
[[package]]
|
|
32
32
|
name = "agrep-ingest"
|
|
33
|
-
version = "0.4.
|
|
33
|
+
version = "0.4.2"
|
|
34
34
|
dependencies = [
|
|
35
35
|
"agrep-core",
|
|
36
36
|
"anyhow",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agrep
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.2
|
|
4
4
|
Summary: Find, open, resume and watch your AI agents' work across Claude, Codex, opencode, Antigravity, Kimi, Cline, Gemini, Crush, Cursor, pi and oh-my-pi - one local index.
|
|
5
5
|
Project-URL: Homepage, https://github.com/dannyisbad/agrep
|
|
6
6
|
Project-URL: Repository, https://github.com/dannyisbad/agrep
|
|
@@ -77,6 +77,12 @@ pub(crate) fn current_process_start_identity() -> Option<String> {
|
|
|
77
77
|
process_start_identity(std::process::id())
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
+
pub(crate) fn process_is_exact_live(pid: u32, start: &str) -> bool {
|
|
81
|
+
!["", "None", "unknown"].contains(&start)
|
|
82
|
+
&& process_liveness(pid) == Liveness::Alive
|
|
83
|
+
&& process_start_identity(pid).as_deref() == Some(start)
|
|
84
|
+
}
|
|
85
|
+
|
|
80
86
|
pub(crate) fn observe_live_holder(path: &Path, label: &str) -> io::Result<Option<LiveHolder>> {
|
|
81
87
|
let observed = match snapshot(path) {
|
|
82
88
|
Ok(observed) => observed,
|
|
@@ -228,6 +228,7 @@ const ROOT_STAGING_ARTIFACTS: &[&str] = &[
|
|
|
228
228
|
".source_absence_pending",
|
|
229
229
|
".source-health.json",
|
|
230
230
|
".source_snapshot.bin",
|
|
231
|
+
".source_snapshot.seal",
|
|
231
232
|
".token_material.json",
|
|
232
233
|
"boundary_stats.json",
|
|
233
234
|
"corpus.db",
|
|
@@ -256,6 +257,7 @@ const CORPUS_DB_TRIGGER_NAMES: [&str; 7] = [
|
|
|
256
257
|
];
|
|
257
258
|
const DERIVED_ADOPTION_OWNER_TOKEN_ENV: &str = "AGREP_DERIVED_ADOPTION_OWNER_TOKEN";
|
|
258
259
|
const DERIVED_ADOPTION_CLAIM_TOKEN_ENV: &str = "AGREP_DERIVED_ADOPTION_CLAIM_TOKEN";
|
|
260
|
+
const DERIVED_ADOPTION_WAIT_MS_ENV: &str = "AGREP_DERIVED_ADOPTION_WAIT_MS";
|
|
259
261
|
const DERIVED_WRITER_IDENTITY_BLOCKED_ENV: &str = "AGREP_DERIVED_WRITER_IDENTITY_BLOCKED";
|
|
260
262
|
|
|
261
263
|
#[derive(serde::Deserialize, serde::Serialize)]
|
|
@@ -891,7 +893,13 @@ fn read_daemon_owner_fields(path: &Path, name: &str) -> Result<HashMap<String, S
|
|
|
891
893
|
}
|
|
892
894
|
};
|
|
893
895
|
let text = match std::str::from_utf8(&body) {
|
|
894
|
-
Ok(text)
|
|
896
|
+
Ok(text)
|
|
897
|
+
if text.ends_with('\n')
|
|
898
|
+
&& text.bytes().filter(|byte| *byte == b'\n').count() == 1
|
|
899
|
+
&& !text.contains('\0') =>
|
|
900
|
+
{
|
|
901
|
+
text
|
|
902
|
+
}
|
|
895
903
|
_ => return Err(format!("freshness-daemon owner {name} is malformed")),
|
|
896
904
|
};
|
|
897
905
|
let mut fields = HashMap::new();
|
|
@@ -908,15 +916,85 @@ fn read_daemon_owner_fields(path: &Path, name: &str) -> Result<HashMap<String, S
|
|
|
908
916
|
Ok(fields)
|
|
909
917
|
}
|
|
910
918
|
|
|
911
|
-
|
|
919
|
+
#[derive(Debug, PartialEq, Eq)]
|
|
920
|
+
enum AdoptionFence {
|
|
921
|
+
LiveSameWriter(u32),
|
|
922
|
+
Refused(String),
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
impl From<String> for AdoptionFence {
|
|
926
|
+
fn from(reason: String) -> Self {
|
|
927
|
+
Self::Refused(reason)
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
impl std::fmt::Display for AdoptionFence {
|
|
932
|
+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
933
|
+
match self {
|
|
934
|
+
Self::LiveSameWriter(pid) => {
|
|
935
|
+
write!(f, "this build is already adopting the index (pid {pid})")
|
|
936
|
+
}
|
|
937
|
+
Self::Refused(reason) => f.write_str(reason),
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
fn live_same_writer_adoption(owners: &[(String, PathBuf)], writer: &str) -> Option<u32> {
|
|
943
|
+
let mut claim = None;
|
|
944
|
+
for (name, path) in owners {
|
|
945
|
+
if !matches!(name.as_str(), ".indexd.lock" | ".indexd.v2.lock") {
|
|
946
|
+
return None;
|
|
947
|
+
}
|
|
948
|
+
let fields = read_daemon_owner_fields(path, name).ok()?;
|
|
949
|
+
let pid_text = fields.get("pid")?;
|
|
950
|
+
if !pid_text.bytes().all(|byte| byte.is_ascii_digit()) {
|
|
951
|
+
return None;
|
|
952
|
+
}
|
|
953
|
+
let pid = pid_text.parse::<u32>().ok()?;
|
|
954
|
+
let start = fields.get("start")?;
|
|
955
|
+
let token = fields.get("token")?;
|
|
956
|
+
if pid == 0
|
|
957
|
+
|| pid > index_lock::MAX_PID
|
|
958
|
+
|| !index_lock::process_is_exact_live(pid, start)
|
|
959
|
+
|| fields.get("writer").map(String::as_str) != Some(writer)
|
|
960
|
+
|| token.len() != 32
|
|
961
|
+
|| !token
|
|
962
|
+
.bytes()
|
|
963
|
+
.all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
|
|
964
|
+
{
|
|
965
|
+
return None;
|
|
966
|
+
}
|
|
967
|
+
if fields.get("state").map(String::as_str) == Some("derived-adoption") {
|
|
968
|
+
if fields.len() != 5 {
|
|
969
|
+
return None;
|
|
970
|
+
}
|
|
971
|
+
let identity = (pid, token.clone());
|
|
972
|
+
if claim.as_ref().is_some_and(|prior| prior != &identity) {
|
|
973
|
+
return None;
|
|
974
|
+
}
|
|
975
|
+
claim = Some(identity);
|
|
976
|
+
} else if name != ".indexd.v2.lock"
|
|
977
|
+
|| fields.get("protocol").map(String::as_str) != Some("2")
|
|
978
|
+
|| std::env::var(DERIVED_ADOPTION_OWNER_TOKEN_ENV).as_deref() != Ok(token.as_str())
|
|
979
|
+
{
|
|
980
|
+
return None;
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
claim.map(|(pid, _)| pid)
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
fn adoption_daemon_fence(data: &Path) -> Option<AdoptionFence> {
|
|
912
987
|
let entries = match fs::read_dir(data) {
|
|
913
988
|
Ok(entries) => entries,
|
|
914
989
|
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return None,
|
|
915
990
|
Err(error) => {
|
|
916
|
-
return Some(
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
991
|
+
return Some(
|
|
992
|
+
format!(
|
|
993
|
+
"cannot inspect freshness-daemon ownership in {}: {error}",
|
|
994
|
+
data.display()
|
|
995
|
+
)
|
|
996
|
+
.into(),
|
|
997
|
+
);
|
|
920
998
|
}
|
|
921
999
|
};
|
|
922
1000
|
let mut owners = Vec::new();
|
|
@@ -924,10 +1002,13 @@ fn adoption_daemon_fence(data: &Path) -> Option<String> {
|
|
|
924
1002
|
let entry = match entry {
|
|
925
1003
|
Ok(entry) => entry,
|
|
926
1004
|
Err(error) => {
|
|
927
|
-
return Some(
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
1005
|
+
return Some(
|
|
1006
|
+
format!(
|
|
1007
|
+
"cannot inspect freshness-daemon ownership in {}: {error}",
|
|
1008
|
+
data.display()
|
|
1009
|
+
)
|
|
1010
|
+
.into(),
|
|
1011
|
+
);
|
|
931
1012
|
}
|
|
932
1013
|
};
|
|
933
1014
|
let Some(name) = entry.file_name().to_str().map(str::to_owned) else {
|
|
@@ -940,10 +1021,13 @@ fn adoption_daemon_fence(data: &Path) -> Option<String> {
|
|
|
940
1021
|
Ok(false) => owners.push((name, path)),
|
|
941
1022
|
Err(error) if error.kind() == std::io::ErrorKind::NotFound => continue,
|
|
942
1023
|
Err(error) => {
|
|
943
|
-
return Some(
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
1024
|
+
return Some(
|
|
1025
|
+
format!(
|
|
1026
|
+
"cannot inspect freshness-daemon owner {}: {error}",
|
|
1027
|
+
path.display()
|
|
1028
|
+
)
|
|
1029
|
+
.into(),
|
|
1030
|
+
);
|
|
947
1031
|
}
|
|
948
1032
|
}
|
|
949
1033
|
}
|
|
@@ -978,25 +1062,34 @@ fn adoption_daemon_fence(data: &Path) -> Option<String> {
|
|
|
978
1062
|
if external.is_empty() {
|
|
979
1063
|
return None;
|
|
980
1064
|
}
|
|
1065
|
+
if let Some(pid) = live_same_writer_adoption(&external, ¤t) {
|
|
1066
|
+
return Some(AdoptionFence::LiveSameWriter(pid));
|
|
1067
|
+
}
|
|
981
1068
|
if external.len() != 1 || external[0].0 == ".indexd.lock" {
|
|
982
|
-
return Some(
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
1069
|
+
return Some(
|
|
1070
|
+
format!(
|
|
1071
|
+
"legacy or ambiguous freshness-daemon ownership is present ({})",
|
|
1072
|
+
external
|
|
1073
|
+
.iter()
|
|
1074
|
+
.map(|(name, _)| name.as_str())
|
|
1075
|
+
.collect::<Vec<_>>()
|
|
1076
|
+
.join(", ")
|
|
1077
|
+
)
|
|
1078
|
+
.into(),
|
|
1079
|
+
);
|
|
990
1080
|
}
|
|
991
1081
|
let fields = match read_daemon_owner_fields(&external[0].1, &external[0].0) {
|
|
992
1082
|
Ok(fields) => fields,
|
|
993
|
-
Err(reason) => return Some(reason),
|
|
1083
|
+
Err(reason) => return Some(reason.into()),
|
|
994
1084
|
};
|
|
995
1085
|
if fields.get("writer").map(String::as_str) != Some(current.as_str()) {
|
|
996
|
-
return Some(
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1086
|
+
return Some(
|
|
1087
|
+
format!(
|
|
1088
|
+
"freshness-daemon owner {} belongs to a different writing build",
|
|
1089
|
+
external[0].0
|
|
1090
|
+
)
|
|
1091
|
+
.into(),
|
|
1092
|
+
);
|
|
1000
1093
|
}
|
|
1001
1094
|
let expected_token = match std::env::var(DERIVED_ADOPTION_OWNER_TOKEN_ENV) {
|
|
1002
1095
|
Ok(value)
|
|
@@ -1008,17 +1101,23 @@ fn adoption_daemon_fence(data: &Path) -> Option<String> {
|
|
|
1008
1101
|
value
|
|
1009
1102
|
}
|
|
1010
1103
|
_ => {
|
|
1011
|
-
return Some(
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1104
|
+
return Some(
|
|
1105
|
+
format!(
|
|
1106
|
+
"freshness-daemon owner {} is not authorized for this derived write",
|
|
1107
|
+
external[0].0
|
|
1108
|
+
)
|
|
1109
|
+
.into(),
|
|
1110
|
+
);
|
|
1015
1111
|
}
|
|
1016
1112
|
};
|
|
1017
1113
|
if fields.get("token").map(String::as_str) != Some(expected_token.as_str()) {
|
|
1018
|
-
return Some(
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1114
|
+
return Some(
|
|
1115
|
+
format!(
|
|
1116
|
+
"freshness-daemon owner {} belongs to a different writing generation",
|
|
1117
|
+
external[0].0
|
|
1118
|
+
)
|
|
1119
|
+
.into(),
|
|
1120
|
+
);
|
|
1022
1121
|
}
|
|
1023
1122
|
None
|
|
1024
1123
|
}
|
|
@@ -1032,7 +1131,7 @@ struct AdoptionClaim {
|
|
|
1032
1131
|
}
|
|
1033
1132
|
|
|
1034
1133
|
impl AdoptionClaim {
|
|
1035
|
-
fn acquire(data: &Path) -> Result<Self,
|
|
1134
|
+
fn acquire(data: &Path) -> Result<Self, AdoptionFence> {
|
|
1036
1135
|
if let Some(reason) = adoption_daemon_fence(data) {
|
|
1037
1136
|
return Err(reason);
|
|
1038
1137
|
}
|
|
@@ -1072,7 +1171,8 @@ impl AdoptionClaim {
|
|
|
1072
1171
|
return Err(format!(
|
|
1073
1172
|
"cannot publish derived-adoption claim {}: {error}",
|
|
1074
1173
|
path.display()
|
|
1075
|
-
)
|
|
1174
|
+
)
|
|
1175
|
+
.into());
|
|
1076
1176
|
}
|
|
1077
1177
|
}
|
|
1078
1178
|
}
|
|
@@ -4258,7 +4358,51 @@ fn published_token_inventory(
|
|
|
4258
4358
|
.then(|| cache.token_namespaces())
|
|
4259
4359
|
}
|
|
4260
4360
|
|
|
4361
|
+
/// Whether the published source snapshot is sealed to the published messages.jsonl, by the identity
|
|
4362
|
+
/// the released-row record uses. A pass that replaced the rows and died before its snapshot, or
|
|
4363
|
+
/// held it, leaves them unsealed.
|
|
4364
|
+
fn source_snapshot_sealed(data: &Path, generation: Option<&cache::GenerationSeal>) -> bool {
|
|
4365
|
+
let Some(generation) = generation else {
|
|
4366
|
+
return false;
|
|
4367
|
+
};
|
|
4368
|
+
let seal_path = data.join(SOURCE_SNAPSHOT_SEAL_FILE);
|
|
4369
|
+
read_optional_bytes(&seal_path, SOURCE_SNAPSHOT_SEAL_MAX_BYTES)
|
|
4370
|
+
.ok()
|
|
4371
|
+
.flatten()
|
|
4372
|
+
.is_some_and(|recorded| {
|
|
4373
|
+
String::from_utf8_lossy(&recorded).trim() == generation_seal_hex(generation)
|
|
4374
|
+
})
|
|
4375
|
+
}
|
|
4376
|
+
|
|
4377
|
+
/// Bind the published source snapshot to the published messages.jsonl. Written after the snapshot,
|
|
4378
|
+
/// so a kill between the two leaves a seal no later pass matches; a seal already in place costs a read.
|
|
4379
|
+
fn seal_source_snapshot(data: &Path) -> anyhow::Result<()> {
|
|
4380
|
+
let seal_path = data.join(SOURCE_SNAPSHOT_SEAL_FILE);
|
|
4381
|
+
match cache::published_generation_seal(data) {
|
|
4382
|
+
Some(seal) => {
|
|
4383
|
+
let hex = generation_seal_hex(&seal);
|
|
4384
|
+
if regular_file_has_bytes(&seal_path, hex.as_bytes())? {
|
|
4385
|
+
return Ok(());
|
|
4386
|
+
}
|
|
4387
|
+
cache::write_bytes_atomic(&seal_path, hex.as_bytes())
|
|
4388
|
+
}
|
|
4389
|
+
None => cache::remove_if_exists(&seal_path),
|
|
4390
|
+
}
|
|
4391
|
+
}
|
|
4392
|
+
|
|
4393
|
+
fn generation_seal_hex(seal: &cache::GenerationSeal) -> String {
|
|
4394
|
+
use std::fmt::Write as _;
|
|
4395
|
+
|
|
4396
|
+
let mut hex = String::with_capacity(32);
|
|
4397
|
+
for byte in seal {
|
|
4398
|
+
let _ = write!(hex, "{byte:02x}");
|
|
4399
|
+
}
|
|
4400
|
+
hex
|
|
4401
|
+
}
|
|
4402
|
+
|
|
4261
4403
|
const SOURCE_SNAPSHOT_FILE: &str = ".source_snapshot.bin";
|
|
4404
|
+
const SOURCE_SNAPSHOT_SEAL_FILE: &str = ".source_snapshot.seal";
|
|
4405
|
+
const SOURCE_SNAPSHOT_SEAL_MAX_BYTES: u64 = 4096;
|
|
4262
4406
|
const INGEST_PENDING_FILE: &str = ".ingest_pending.bin";
|
|
4263
4407
|
const SOURCE_ABSENCE_FILE: &str = ".source_absence_pending";
|
|
4264
4408
|
const HARNESS_POLICY_SNAPSHOT_FILE: &str = ".harness_prefixes.snapshot";
|
|
@@ -5091,54 +5235,85 @@ fn index_cmd(agent: &str, full: bool) -> anyhow::Result<()> {
|
|
|
5091
5235
|
disclose_read_only_ownership(&format!("derived writer identity is unavailable: {reason}"));
|
|
5092
5236
|
return Ok(());
|
|
5093
5237
|
}
|
|
5094
|
-
|
|
5095
|
-
|
|
5096
|
-
|
|
5097
|
-
|
|
5098
|
-
let
|
|
5099
|
-
|
|
5100
|
-
|
|
5101
|
-
|
|
5102
|
-
|
|
5103
|
-
let
|
|
5104
|
-
|
|
5105
|
-
|
|
5106
|
-
|
|
5107
|
-
|
|
5108
|
-
|
|
5109
|
-
|
|
5110
|
-
|
|
5111
|
-
|
|
5112
|
-
|
|
5113
|
-
|
|
5114
|
-
|
|
5115
|
-
|
|
5116
|
-
|
|
5117
|
-
|
|
5238
|
+
let wait_budget = std::env::var(DERIVED_ADOPTION_WAIT_MS_ENV)
|
|
5239
|
+
.ok()
|
|
5240
|
+
.and_then(|value| value.parse::<u64>().ok())
|
|
5241
|
+
.map(|millis| std::time::Duration::from_millis(millis.min(180_000)));
|
|
5242
|
+
let mut waiting_since = None;
|
|
5243
|
+
let mut wait_on_adoption = |fence: &AdoptionFence| -> anyhow::Result<bool> {
|
|
5244
|
+
let (AdoptionFence::LiveSameWriter(pid), Some(budget)) = (fence, wait_budget) else {
|
|
5245
|
+
return Ok(false);
|
|
5246
|
+
};
|
|
5247
|
+
let started = waiting_since.get_or_insert_with(|| {
|
|
5248
|
+
eprintln!(
|
|
5249
|
+
"waiting for the freshness daemon to finish adopting the index (pid {pid}) ..."
|
|
5250
|
+
);
|
|
5251
|
+
Instant::now()
|
|
5252
|
+
});
|
|
5253
|
+
let remaining = budget.saturating_sub(started.elapsed());
|
|
5254
|
+
anyhow::ensure!(
|
|
5255
|
+
!remaining.is_zero(),
|
|
5256
|
+
"freshness daemon is still adopting the index (pid {pid}); \
|
|
5257
|
+
the wait budget expired, but this adoption finishes on its own; \
|
|
5258
|
+
run `agrep index` again after it completes"
|
|
5259
|
+
);
|
|
5260
|
+
std::thread::sleep(remaining.min(std::time::Duration::from_millis(100)));
|
|
5261
|
+
Ok(true)
|
|
5262
|
+
};
|
|
5263
|
+
let (adoption_claim, ownership_was_current) = loop {
|
|
5264
|
+
// The incumbent may be replacing ownership artifacts or holding a SQLite
|
|
5265
|
+
// journal; wait before probing that family or taking its index lock.
|
|
5266
|
+
if wait_budget.is_some() {
|
|
5267
|
+
if let Some(fence) = adoption_daemon_fence(&data) {
|
|
5268
|
+
if wait_on_adoption(&fence)? {
|
|
5269
|
+
continue;
|
|
5270
|
+
}
|
|
5271
|
+
}
|
|
5118
5272
|
}
|
|
5119
|
-
// A
|
|
5120
|
-
//
|
|
5121
|
-
|
|
5122
|
-
|
|
5123
|
-
|
|
5124
|
-
|
|
5273
|
+
// A rollback journal blocks the ownership probe itself and nothing on the
|
|
5274
|
+
// box would ever clear it. Reclaim it first, under the index lock the
|
|
5275
|
+
// writer takes anyway, then read ownership for real.
|
|
5276
|
+
let corpus = data.join("corpus.db");
|
|
5277
|
+
let corpus_missing = matches!(
|
|
5278
|
+
agrep_core::ingest::registry::regular_file_edge_snapshot(&corpus, 0),
|
|
5279
|
+
Ok(None)
|
|
5280
|
+
);
|
|
5281
|
+
if !corpus_missing
|
|
5282
|
+
&& hot_rollback_journal(&corpus).map_or(true, |journal| journal.is_some())
|
|
5283
|
+
{
|
|
5284
|
+
let mut journal_lock = acquire_index_lock()?;
|
|
5285
|
+
let reclaimed = reclaim_cold_rollback_journal(&corpus);
|
|
5286
|
+
journal_lock.release()?;
|
|
5287
|
+
if let Err(detail) = reclaimed {
|
|
5288
|
+
disclose_read_only_ownership(&detail);
|
|
5125
5289
|
return Ok(());
|
|
5126
5290
|
}
|
|
5127
|
-
}
|
|
5128
|
-
|
|
5129
|
-
|
|
5130
|
-
|
|
5131
|
-
|
|
5132
|
-
|
|
5291
|
+
}
|
|
5292
|
+
let ownership = derived_write_ownership(&data);
|
|
5293
|
+
if let DerivedWriteOwnership::Refused(reason)
|
|
5294
|
+
| DerivedWriteOwnership::PostAdoptionClobber(reason) = &ownership
|
|
5295
|
+
{
|
|
5296
|
+
disclose_read_only_ownership(reason);
|
|
5297
|
+
return Ok(());
|
|
5298
|
+
}
|
|
5299
|
+
// Retry the exclusive claim before taking .index.lock; waiting with either
|
|
5300
|
+
// lock held would prevent the incumbent from completing its publication.
|
|
5301
|
+
match AdoptionClaim::acquire(&data) {
|
|
5302
|
+
Ok(claim) => {
|
|
5303
|
+
break (Some(claim), ownership == DerivedWriteOwnership::Current);
|
|
5133
5304
|
}
|
|
5134
|
-
|
|
5135
|
-
|
|
5136
|
-
|
|
5137
|
-
|
|
5305
|
+
Err(fence) => {
|
|
5306
|
+
if wait_on_adoption(&fence)? {
|
|
5307
|
+
continue;
|
|
5308
|
+
}
|
|
5309
|
+
let reason = match ownership {
|
|
5310
|
+
DerivedWriteOwnership::Foreign(reason) => format!("{reason}; {fence}"),
|
|
5311
|
+
_ => fence.to_string(),
|
|
5312
|
+
};
|
|
5138
5313
|
disclose_read_only_ownership(&reason);
|
|
5139
5314
|
return Ok(());
|
|
5140
5315
|
}
|
|
5141
|
-
}
|
|
5316
|
+
}
|
|
5142
5317
|
};
|
|
5143
5318
|
let lock_path = data.join(".index.lock");
|
|
5144
5319
|
if ownership_was_current && !full && !agrep_core::emit::on() {
|
|
@@ -5207,7 +5382,7 @@ fn index_cmd_locked(
|
|
|
5207
5382
|
return Ok(());
|
|
5208
5383
|
}
|
|
5209
5384
|
if let Some(reason) = adoption_daemon_fence(&data) {
|
|
5210
|
-
disclose_read_only_ownership(&reason);
|
|
5385
|
+
disclose_read_only_ownership(&reason.to_string());
|
|
5211
5386
|
return Ok(());
|
|
5212
5387
|
}
|
|
5213
5388
|
consume_retained_corpus_owner(&data)?;
|
|
@@ -5221,7 +5396,7 @@ fn index_cmd_locked(
|
|
|
5221
5396
|
return Ok(());
|
|
5222
5397
|
}
|
|
5223
5398
|
if let Some(reason) = adoption_daemon_fence(&data) {
|
|
5224
|
-
disclose_read_only_ownership(&reason);
|
|
5399
|
+
disclose_read_only_ownership(&reason.to_string());
|
|
5225
5400
|
return Ok(());
|
|
5226
5401
|
}
|
|
5227
5402
|
false
|
|
@@ -5381,6 +5556,8 @@ fn index_cmd_locked(
|
|
|
5381
5556
|
}
|
|
5382
5557
|
// The published generation stands as the pass that published it disclosed it; no
|
|
5383
5558
|
// refused attempt intervened, since one leaves the pending marker that bars this lane.
|
|
5559
|
+
// Its snapshot is current, whatever outran its seal (a kill before it, an older build).
|
|
5560
|
+
seal_source_snapshot(&data)?;
|
|
5384
5561
|
lap!("source-check");
|
|
5385
5562
|
println!(
|
|
5386
5563
|
" unchanged since last index ({} messages); skipped ingest + writes ({:.0}ms)",
|
|
@@ -5578,10 +5755,14 @@ fn index_cmd_locked(
|
|
|
5578
5755
|
expected_paths.extend(paths);
|
|
5579
5756
|
}
|
|
5580
5757
|
pcache.set_repair_expectations(expected_agents, expected_paths);
|
|
5581
|
-
// The publication guard's own inventory, independent of the deletion machinery above so
|
|
5582
|
-
//
|
|
5583
|
-
//
|
|
5584
|
-
|
|
5758
|
+
// The publication guard's own inventory, independent of the deletion machinery above so --full
|
|
5759
|
+
// still converges real deletions: what the published generation contained, or a proven-empty set
|
|
5760
|
+
// when nothing was published. A snapshot an unfinished publication outran inventories older rows.
|
|
5761
|
+
let generation = cache::published_generation_seal(&data);
|
|
5762
|
+
let published_inventory = published_source
|
|
5763
|
+
.as_deref()
|
|
5764
|
+
.filter(|_| pending_source.is_none() || source_snapshot_sealed(&data, generation.as_ref()));
|
|
5765
|
+
if let Some(published) = published_inventory {
|
|
5585
5766
|
let (_, paths) = ingest::registry::source_snapshot_expectations(published);
|
|
5586
5767
|
pcache.set_published_material(paths);
|
|
5587
5768
|
// That inventory lists only what it could read. Name the scopes it could not, so a
|
|
@@ -5595,8 +5776,8 @@ fn index_cmd_locked(
|
|
|
5595
5776
|
} else if !prior_generation {
|
|
5596
5777
|
pcache.set_published_material(HashSet::new());
|
|
5597
5778
|
} else if pcache.decoded_last_good_base() {
|
|
5598
|
-
// Publication withheld every snapshot (0.3.2 held it beside any source issue,
|
|
5599
|
-
//
|
|
5779
|
+
// Publication withheld every snapshot (0.3.2 held it beside any source issue, `--emit-rows`
|
|
5780
|
+
// takes none) or sealed it beside older rows. Every Stat and token row published was cached
|
|
5600
5781
|
// first, as `published_token_inventory` relies on; whole-store rows need not have been.
|
|
5601
5782
|
pcache.set_published_material_from_cache();
|
|
5602
5783
|
}
|
|
@@ -5660,7 +5841,7 @@ fn index_cmd_locked(
|
|
|
5660
5841
|
source_issues
|
|
5661
5842
|
.iter()
|
|
5662
5843
|
.map(|issue| (issue.agent(), Path::new(issue.path()))),
|
|
5663
|
-
|
|
5844
|
+
generation,
|
|
5664
5845
|
);
|
|
5665
5846
|
}
|
|
5666
5847
|
lap!("load-cache");
|
|
@@ -5995,6 +6176,7 @@ fn index_cmd_locked(
|
|
|
5995
6176
|
&pending_path,
|
|
5996
6177
|
)?;
|
|
5997
6178
|
if source_published {
|
|
6179
|
+
seal_source_snapshot(&data)?;
|
|
5998
6180
|
cache::remove_if_exists(&absence_path)?;
|
|
5999
6181
|
}
|
|
6000
6182
|
lap!("source-publish");
|
|
@@ -6150,6 +6332,7 @@ fn index_cmd_locked(
|
|
|
6150
6332
|
&pending_path,
|
|
6151
6333
|
)?;
|
|
6152
6334
|
if source_published {
|
|
6335
|
+
seal_source_snapshot(&data)?;
|
|
6153
6336
|
cache::remove_if_exists(&absence_path)?;
|
|
6154
6337
|
}
|
|
6155
6338
|
lap!("source-publish");
|
|
@@ -7801,6 +7984,77 @@ mod tests {
|
|
|
7801
7984
|
let _ = std::fs::remove_dir_all(&data);
|
|
7802
7985
|
}
|
|
7803
7986
|
|
|
7987
|
+
#[test]
|
|
7988
|
+
fn same_writer_adoption_remains_fenced_with_an_exact_busy_diagnosis() {
|
|
7989
|
+
let pid = std::process::id();
|
|
7990
|
+
let start = super::index_lock::current_process_start_identity().unwrap();
|
|
7991
|
+
let writer = agrep_core::ingest_cache::current_cache_writer_build_id();
|
|
7992
|
+
let token = "c".repeat(32);
|
|
7993
|
+
let data = std::env::temp_dir().join(format!(
|
|
7994
|
+
"agrep-exact-adoption-{pid}-{}",
|
|
7995
|
+
std::time::SystemTime::now()
|
|
7996
|
+
.duration_since(std::time::UNIX_EPOCH)
|
|
7997
|
+
.unwrap()
|
|
7998
|
+
.as_nanos()
|
|
7999
|
+
));
|
|
8000
|
+
std::fs::create_dir_all(&data).unwrap();
|
|
8001
|
+
let legacy = data.join(".indexd.lock");
|
|
8002
|
+
let current = data.join(".indexd.v2.lock");
|
|
8003
|
+
let raw = format!(
|
|
8004
|
+
"state=derived-adoption pid={pid} start={start} writer={writer} token={token}\n"
|
|
8005
|
+
);
|
|
8006
|
+
std::fs::write(&legacy, &raw).unwrap();
|
|
8007
|
+
std::fs::write(¤t, &raw).unwrap();
|
|
8008
|
+
assert_eq!(
|
|
8009
|
+
super::adoption_daemon_fence(&data),
|
|
8010
|
+
Some(super::AdoptionFence::LiveSameWriter(pid))
|
|
8011
|
+
);
|
|
8012
|
+
assert_eq!(std::fs::read_to_string(&legacy).unwrap(), raw);
|
|
8013
|
+
assert_eq!(std::fs::read_to_string(¤t).unwrap(), raw);
|
|
8014
|
+
std::fs::write(¤t, raw.replace(&token, &"d".repeat(32))).unwrap();
|
|
8015
|
+
assert!(matches!(
|
|
8016
|
+
super::adoption_daemon_fence(&data),
|
|
8017
|
+
Some(super::AdoptionFence::Refused(_))
|
|
8018
|
+
));
|
|
8019
|
+
|
|
8020
|
+
std::fs::remove_file(¤t).unwrap();
|
|
8021
|
+
for untrusted in [
|
|
8022
|
+
raw.replace(&format!("start={start}"), "start=unknown"),
|
|
8023
|
+
raw.replace(&format!("pid={pid}"), &format!("pid=+{pid}")),
|
|
8024
|
+
raw.replace("state=derived-adoption", "state=legacy"),
|
|
8025
|
+
raw.replace(&format!("token={token}"), "token=invalid"),
|
|
8026
|
+
raw.replace('\n', &format!(" pid={pid}\n")),
|
|
8027
|
+
raw.replace('\n', " extra=untrusted\n"),
|
|
8028
|
+
raw.replace('\n', "\n\n"),
|
|
8029
|
+
raw.replace('\n', "\0\n"),
|
|
8030
|
+
] {
|
|
8031
|
+
std::fs::write(&legacy, &untrusted).unwrap();
|
|
8032
|
+
let reason = super::adoption_daemon_fence(&data).unwrap();
|
|
8033
|
+
assert!(
|
|
8034
|
+
matches!(reason, super::AdoptionFence::Refused(_)),
|
|
8035
|
+
"{untrusted}: {reason}"
|
|
8036
|
+
);
|
|
8037
|
+
assert_eq!(std::fs::read_to_string(&legacy).unwrap(), untrusted);
|
|
8038
|
+
}
|
|
8039
|
+
std::fs::write(&legacy, &raw).unwrap();
|
|
8040
|
+
let unknown = data.join(".indexd.v3.lock");
|
|
8041
|
+
std::fs::write(&unknown, &raw).unwrap();
|
|
8042
|
+
assert!(super::adoption_daemon_fence(&data)
|
|
8043
|
+
.unwrap()
|
|
8044
|
+
.to_string()
|
|
8045
|
+
.contains("legacy or ambiguous"));
|
|
8046
|
+
std::fs::remove_file(unknown).unwrap();
|
|
8047
|
+
#[cfg(unix)]
|
|
8048
|
+
{
|
|
8049
|
+
std::os::unix::fs::symlink(&legacy, ¤t).unwrap();
|
|
8050
|
+
assert!(matches!(
|
|
8051
|
+
super::adoption_daemon_fence(&data),
|
|
8052
|
+
Some(super::AdoptionFence::Refused(_))
|
|
8053
|
+
));
|
|
8054
|
+
}
|
|
8055
|
+
std::fs::remove_dir_all(data).unwrap();
|
|
8056
|
+
}
|
|
8057
|
+
|
|
7804
8058
|
#[test]
|
|
7805
8059
|
fn live_adoption_claim_from_different_build_blocks_fencing() {
|
|
7806
8060
|
let data = std::env::temp_dir().join(format!(
|
|
@@ -1660,16 +1660,17 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
1660
1660
|
if not work_available("orphaned-tally join"):
|
|
1661
1661
|
validation_complete = False
|
|
1662
1662
|
break
|
|
1663
|
-
for _key, entry in keyed_entries
|
|
1664
|
-
agent = str(entry.get("agent") or "")
|
|
1665
|
-
if agent:
|
|
1666
|
-
book_agents.add(agent)
|
|
1663
|
+
agents_here = {str(entry.get("agent") or "") for _key, entry in keyed_entries} - {""}
|
|
1667
1664
|
if path in seen_paths:
|
|
1665
|
+
book_agents.update(agents_here)
|
|
1668
1666
|
continue
|
|
1669
1667
|
state, detail = _path_state(path)
|
|
1670
1668
|
if state == "missing":
|
|
1669
|
+
# A deleted store's tallies stay behind; they count here, not as that agent's evidence.
|
|
1671
1670
|
orphan_count += 1
|
|
1672
|
-
|
|
1671
|
+
continue
|
|
1672
|
+
book_agents.update(agents_here)
|
|
1673
|
+
if state == "unreadable":
|
|
1673
1674
|
problems.append(f"cannot stat tallied source {path}: {detail}")
|
|
1674
1675
|
else:
|
|
1675
1676
|
agents = sorted({str(entry.get("agent") or "")
|