fix(memory): clear structure-degraded latch on any completed extraction (#3365) (#3631)

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Steven Enamakel <enamakel@tinyhumans.ai>
This commit is contained in:
sanil-23
2026-06-15 13:28:23 -07:00
committed by GitHub
co-authored by Claude Steven Enamakel
parent c5087d83da
commit e4666f9be4
2 changed files with 57 additions and 9 deletions
+14 -9
View File
@@ -150,15 +150,20 @@ impl EntityExtractor for LlmEntityExtractor {
for attempt in 0..MAX_ATTEMPTS {
match self.try_extract(text).await {
Some(extracted) => {
// #002 (T013): a completed extraction that yielded
// structure means the extraction model is working —
// clear any prior "structure degraded" flag so the
// status/doctor surface recovers. (An empty-but-valid
// result, e.g. genuinely entity-free text, is left
// alone — it isn't evidence the model is broken.)
if !extracted.entities.is_empty() || !extracted.topics.is_empty() {
crate::openhuman::memory_tree::health::clear_structure_degraded();
}
// #3365: the structure-degraded latch means "the extraction
// model is timing out / unreachable" — set ONLY after
// MAX_ATTEMPTS transport failures (below). A *completed* call
// disproves that: `try_extract` returns `Some` whenever the
// provider responded, including the valid-but-empty and
// malformed-JSON-as-empty cases. So a completed extraction
// clears the latch regardless of whether this particular text
// yielded entities — liveness, not content, is what it tracks.
//
// (Clearing only on a non-empty result left the latch stuck
// after the model recovered whenever later texts were
// entity-light, so the surface kept warning "extraction model
// is timing out" long after it had stopped — #3365.)
crate::openhuman::memory_tree::health::clear_structure_degraded();
return Ok(extracted);
}
None => {
@@ -291,6 +291,49 @@ async fn extract_returns_empty_on_malformed_provider_response() {
assert!(out.llm_importance.is_none());
}
#[tokio::test]
async fn extract_clears_structure_latch_on_completed_empty_extraction() {
// #3365: the structure-degraded latch is a LIVENESS signal ("the extraction
// model is timing out"), set only after transport-failure retries are
// exhausted. A *completed* call — even one that yields no entities (a
// valid-but-empty result for entity-free text) — proves the model is
// responding, so it must clear the latch. Previously only a NON-empty result
// cleared it, so after the model recovered the latch stayed stuck whenever
// later texts were entity-light.
let _health_guard = crate::openhuman::memory_tree::health::test_guard();
use crate::openhuman::memory::chat::{ChatPrompt, ChatProvider};
use async_trait::async_trait;
use std::sync::Arc;
struct EmptyButOkProvider;
#[async_trait]
impl ChatProvider for EmptyButOkProvider {
fn name(&self) -> &str {
"test:empty-ok"
}
async fn chat_for_json(&self, _p: &ChatPrompt) -> anyhow::Result<String> {
Ok(r#"{"entities": [], "topics": []}"#.to_string())
}
}
crate::openhuman::memory_tree::health::mark_structure_degraded(
crate::openhuman::memory_tree::health::FailureCode::ExtractionTimeout,
);
assert!(
crate::openhuman::memory_tree::health::current_degraded_state().structure,
"precondition: structure-degraded latch is set"
);
let ex = LlmEntityExtractor::new(LlmExtractorConfig::default(), Arc::new(EmptyButOkProvider));
let out = ex.extract("entity-free text").await.unwrap();
assert!(out.entities.is_empty(), "no entities in the response");
assert!(
!crate::openhuman::memory_tree::health::current_degraded_state().structure,
"a completed extraction must clear the structure-degraded latch even when empty"
);
}
#[test]
fn into_extracted_entities_drops_hallucinations() {
let out = LlmExtractionOutput {