Skip to content

Commit c2b9319

Browse files
Merge pull request #68 from YellowSnnowmann/feat/a4-typed-errors-retry-health
§A4 typed errors, read retries, deep health, honest min_score
2 parents b4a46f0 + 5dc3b99 commit c2b9319

25 files changed

Lines changed: 986 additions & 202 deletions

File tree

adapters/remote/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ anyhow = "1"
2020
# cap rather than buffered whole, because the endpoint is operator-supplied
2121
# and a broken or hostile one must not be able to OOM the host.
2222
reqwest = { version = "0.12", default-features = false, features = ["json", "multipart", "rustls-tls", "stream"] }
23+
# Only the timer, for the read-retry backoff (issue #18 §U5) — reqwest already
24+
# requires a tokio runtime, so this adds no new runtime assumption.
25+
tokio = { version = "1", default-features = false, features = ["time"] }
2326
# Remote records are translated through a private, lossless envelope.
2427
serde = { version = "1", features = ["derive"] }
2528
# Streaming a capped body needs a Stream combinator.

adapters/remote/src/cognee.rs

Lines changed: 48 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ use tinymemory_api::recall::RecallOpts;
88
use tinymemory_api::traits::Memory;
99
use tinymemory_api::types::MemoryTaint;
1010

11-
use crate::common::{stable_id, Dialect, HttpClient, RemoteMemory, StoredEntry};
11+
use crate::common::{stable_id, Attempts, Dialect, HttpClient, RemoteMemory, StoredEntry};
1212

1313
/// Stable driver id used by configuration and status output.
1414
pub use tinymemory_api::drivers::COGNEE_DRIVER_ID;
@@ -20,6 +20,26 @@ pub struct CogneeMemory {
2020
}
2121

2222
impl CogneeMemory {
23+
/// Rebuilds the HTTP transport with a different per-request deadline
24+
/// (issue #18 follow-up U5). The default is 60s with a 10s connect
25+
/// deadline — right for interactive calls; a bulk migration or a tight
26+
/// liveness probe may want its own budget.
27+
///
28+
/// # Errors
29+
///
30+
/// Fails only if the underlying HTTP client cannot be rebuilt — a
31+
/// configuration-time failure, before any request is made.
32+
pub fn with_request_timeout(mut self, timeout: std::time::Duration) -> anyhow::Result<Self> {
33+
let client = self
34+
.inner
35+
.dialect_mut()
36+
.client
37+
.clone()
38+
.with_timeout(timeout)?;
39+
self.inner.dialect_mut().client = client;
40+
Ok(self)
41+
}
42+
2343
/// Connect to a self-hosted Cognee server.
2444
///
2545
/// `access_token` is sent as a bearer token. Local deployments with
@@ -152,6 +172,13 @@ impl Memory for CogneeMemory {
152172
async fn health_check(&self) -> bool {
153173
self.inner.health_check().await
154174
}
175+
/// Forwarded explicitly: this wrapper delegates method-by-method, so the
176+
/// defaulted `None` would otherwise shadow `RemoteMemory`'s typed probe —
177+
/// which is exactly what the first cut shipped, making §U4's deep health
178+
/// unreachable through every public type (the #68 review's Major 1).
179+
async fn health_probe(&self) -> Option<tinymemory_api::health::MemoryHealth> {
180+
self.inner.health_probe().await
181+
}
155182
}
156183

157184
#[derive(Debug)]
@@ -181,7 +208,12 @@ impl CogneeDialect {
181208
async fn datasets(&self) -> anyhow::Result<Vec<Dataset>> {
182209
let response: Value = self
183210
.client
184-
.json(Method::GET, "api/v1/datasets/", None)
211+
.json(
212+
Method::GET,
213+
"api/v1/datasets/",
214+
None,
215+
Attempts::RetryTransient,
216+
)
185217
.await?;
186218
Ok(response
187219
.as_array()
@@ -205,6 +237,7 @@ impl CogneeDialect {
205237
Method::GET,
206238
&format!("api/v1/datasets/{}/data", dataset.id),
207239
None,
240+
Attempts::RetryTransient,
208241
)
209242
.await?;
210243
let mut entries = Vec::new();
@@ -225,6 +258,7 @@ impl CogneeDialect {
225258
.text(
226259
Method::GET,
227260
&format!("api/v1/datasets/{}/data/{id}/raw", dataset.id),
261+
Attempts::RetryTransient,
228262
)
229263
.await?;
230264
let mut entry: StoredEntry =
@@ -356,6 +390,7 @@ impl Dialect for CogneeDialect {
356390
"only_context": true,
357391
"session_id": opts.session_id
358392
})),
393+
Attempts::RetryTransient,
359394
)
360395
.await?;
361396
let mut entries = Vec::new();
@@ -402,9 +437,17 @@ impl Dialect for CogneeDialect {
402437
Ok(true)
403438
}
404439

405-
/// Checks Cognee's aggregate health endpoint.
406-
async fn health(&self) -> bool {
407-
self.client.healthy("health").await
440+
/// Probes Cognee's aggregate health endpoint, typed.
441+
async fn health(&self) -> anyhow::Result<()> {
442+
self.client.probe("health").await
443+
}
444+
445+
/// Context-only recall carries no score field — see the trait doc for
446+
/// what this means for `min_score` (documented-inert, not everything-
447+
/// dropping; the first cut's over-fetch pulled 3x the data and discarded
448+
/// all of it).
449+
fn scores_recall(&self) -> bool {
450+
false
408451
}
409452
}
410453

adapters/remote/src/cognee_test.rs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,27 @@ async fn native_cognee_round_trips_the_tinymemory_contract() {
207207
.len(),
208208
1
209209
);
210+
// #68 review Major 2: Cognee's context-only recall is scoreless, so the
211+
// strict filter would have dropped 100% of every thresholded result.
212+
// The dialect declares scores_recall() = false and min_score is
213+
// documented-inert: the hit survives.
214+
assert_eq!(
215+
driver
216+
.recall(
217+
"graph",
218+
3,
219+
&OwnedRecallOpts {
220+
namespace: Some("project".into()),
221+
min_score: Some(0.5),
222+
..OwnedRecallOpts::default()
223+
},
224+
None
225+
)
226+
.await
227+
.expect("recall with a threshold the backend cannot score")
228+
.len(),
229+
1
230+
);
210231
assert!(driver.forget("project", "key").await.expect("forget"));
211232
assert!(!driver.forget("project", "key").await.expect("forget again"));
212233
assert!(driver.health().await.is_usable());

0 commit comments

Comments
 (0)