pub(crate) mod watch;
use anda_core::BoxError;
use anda_kip::{KipError, KipErrorCode, Request, Response};
use serde_json::Value as Json;
use std::future::Future;
use crate::{
kip,
types::{
ArmedWatch, CommitmentSettlement, Dependent, RevisedRoot, SkillSettlement, WatchSettlement,
},
};
pub(crate) const SETTLEMENT_BATCH_LIMIT: usize = 500;
pub(crate) const PINNED_RETENTION_CLASS: &str = "pinned";
pub(crate) const STANDARD_RETENTION_CLASS: &str = "standard";
pub(crate) trait RunKip: Send + Sync {
fn attention_sweep(&self) -> impl Future<Output = Option<WatchSettlement>> + Send {
async { None }
}
fn space_id(&self) -> &str;
fn run_kip(
&self,
request: Request,
readonly: bool,
) -> impl Future<Output = Result<Response, BoxError>> + Send;
fn advance_watch(
&self,
_id: &str,
_version: u64,
_generation: u64,
) -> impl Future<Output = Result<Json, KipError>> + Send {
async {
Err(KipError::unsupported_capability(
"Watch runtime is unavailable",
))
}
}
}
async fn read(port: &impl RunKip, request: Request) -> Result<Response, String> {
match port.run_kip(request, true).await {
Ok(response) if kip::succeeded(&response) => Ok(response),
Ok(response) => Err(kip::error_message(&response)),
Err(err) => Err(err.to_string()),
}
}
#[derive(Debug, Clone)]
pub(crate) struct SupersededRow {
pub assertion: String,
pub space_seq: u64,
pub actor: Option<String>,
pub proposition: Option<String>,
pub superseded_by: Vec<String>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub(crate) struct CorrectionCursor {
pub seq: u64,
#[serde(default)]
pub after_id: String,
}
impl From<u64> for CorrectionCursor {
fn from(seq: u64) -> Self {
Self {
seq,
after_id: String::new(),
}
}
}
#[derive(Debug, Default)]
pub(crate) struct CorrectionScan {
pub rows: Vec<SupersededRow>,
pub cursor: CorrectionCursor,
pub watermark: u64,
pub incomplete: bool,
pub error: Option<String>,
}
pub(crate) async fn scan_corrections(
port: &impl RunKip,
after: CorrectionCursor,
) -> CorrectionScan {
let through = if after.after_id.is_empty() {
after.seq
} else {
after.seq.saturating_sub(1)
};
let mut scan = CorrectionScan {
cursor: after.clone(),
watermark: through,
..Default::default()
};
let boundary = if after.after_id.is_empty() {
"FILTER(?a._system.space_seq > :after)"
} else {
"FILTER(?a._system.space_seq > :after || (?a._system.space_seq == :after && ?a.id > :after_id))"
};
let request = kip::request_with(
format!(
"FIND(?a.id, ?a._system.space_seq, ?a.asserted_by, ?a.proposition, ?a.lifecycle.superseded_by) WHERE {{ ?a ASSERTION {{}} FILTER(?a.lifecycle.status == \"superseded\") {boundary} }} ORDER BY ?a._system.space_seq, ?a.id LIMIT {SETTLEMENT_BATCH_LIMIT}"
),
serde_json::Map::from_iter([
("after".into(), Json::from(after.seq)),
("after_id".into(), Json::from(after.after_id.clone())),
]),
);
let response = match read(port, request).await {
Ok(response) => response,
Err(error) => {
scan.incomplete = true;
scan.error = Some(error);
return scan;
}
};
let Some(rows) = kip::ok_result(&response).and_then(Json::as_array) else {
scan.incomplete = true;
scan.error = Some("correction scan returned no row array".into());
return scan;
};
scan.incomplete = rows.len() >= SETTLEMENT_BATCH_LIMIT
|| response
.results
.first()
.is_some_and(|r| r.next_cursor.is_some());
let mut parsed = superseded_rows(&Json::Array(rows.clone()));
if parsed.len() != rows.len() {
scan.incomplete = true;
scan.error = Some("correction scan returned an unreadable row; cursor retained".into());
return scan;
}
parsed.retain(|row| {
row.space_seq > after.seq
|| row.space_seq == after.seq
&& !after.after_id.is_empty()
&& row.assertion > after.after_id
});
parsed.sort_by(|a, b| {
a.space_seq
.cmp(&b.space_seq)
.then(a.assertion.cmp(&b.assertion))
});
if let Some(last) = parsed.last() {
scan.cursor = CorrectionCursor {
seq: last.space_seq,
after_id: if scan.incomplete {
last.assertion.clone()
} else {
String::new()
},
};
scan.watermark = if scan.incomplete {
last.space_seq.saturating_sub(1)
} else {
last.space_seq
};
} else if !scan.incomplete {
scan.cursor = after.seq.into();
scan.watermark = after.seq;
}
scan.watermark = scan.watermark.max(through);
scan.rows = parsed;
scan
}
pub(crate) async fn sweep_watches(port: &impl RunKip) -> WatchSettlement {
if let Some(report) = port.attention_sweep().await {
return report;
}
let mut report = WatchSettlement::default();
let response = match read(port, watch::watches_request("armed")).await {
Ok(response) => response,
Err(error) => {
report.error = Some(error);
return report;
}
};
let rows = kip::ok_result(&response)
.map(watch::read_watch_rows)
.unwrap_or_default();
let full = rows.len() >= watch::WATCH_SWEEP_LIMIT;
let mut runnable = Vec::new();
for row in rows {
let Some(_) = row.generation else {
report.deferred += 1;
report.error = Some("armed Watch has no WatchState; review its observation gap and re-arm it through the host".into());
continue;
};
if !watch::is_structured(&row.condition) {
report.deferred += 1;
continue;
}
runnable.push(row);
}
if full {
match read(port, watch::runnable_watches_request()).await {
Ok(response) => {
runnable = kip::ok_result(&response)
.map(watch::read_watch_rows)
.unwrap_or_default()
}
Err(error) => {
report.error = Some(error);
return report;
}
}
}
for row in runnable {
let Some(generation) = row.generation else {
continue;
};
if !watch::is_structured(&row.condition) {
continue;
}
match port.advance_watch(&row.id, row.version, generation).await {
Ok(result) => match result["status"].as_str() {
Some("fired") => report.fired += 1,
Some("expired" | "disarmed") => report.disarmed += 1,
_ => report.deferred += 1,
},
Err(error) if error.code == KipErrorCode::VersionConflict => {
report.conflicted += 1;
report.error = Some(error.to_string());
}
Err(error) => {
report.deferred += 1;
report.error = Some(error.to_string());
}
}
}
report
}
pub(crate) const COMMITMENT_REVIEW_LIMIT: usize = 50;
const DUE_COMMITMENTS: &str = r#"FIND(?c.id, ?c.attributes.due_at) WHERE {
?c CONCEPT {type: "Commitment"}
FILTER((?c.attributes.status == "pending" || ?c.attributes.status == "blocked") && ?c.attributes.due_at <= :now)
NOT { ?w CONCEPT {type: "Watch"} STRUCTURAL (?w, "watches", ?c) }
} ORDER BY ?c.attributes.due_at ASC LIMIT :limit"#;
const RAISE_COMMITMENT: &str = r#"CREATE ACTIVITY ?review {
CLIENT KEY :key
SET FIELDS { activity_class: "commitment_review", status: "completed", started_at: :now, ended_at: :now }
SET STRUCTURAL { ("inputs", :commitment) }
}"#;
pub(crate) async fn raise_due_commitments(port: &impl RunKip, now_ms: u64) -> CommitmentSettlement {
let now = kip::timestamp(now_ms);
let mut report = CommitmentSettlement::default();
let response = match read(
port,
kip::request_with(
DUE_COMMITMENTS,
serde_json::Map::from_iter([
("now".to_string(), Json::from(now.as_str())),
("limit".to_string(), Json::from(COMMITMENT_REVIEW_LIMIT)),
]),
),
)
.await
{
Ok(response) => response,
Err(error) => {
report.error = Some(error);
return report;
}
};
let rows = kip::ok_result(&response)
.and_then(Json::as_array)
.cloned()
.unwrap_or_default();
for row in rows {
let (Some(id), Some(due_at)) = (
row.get(0).and_then(Json::as_str),
row.get(1).and_then(Json::as_str),
) else {
continue;
};
if kip::time_ms(due_at).is_none_or(|due| due > now_ms) {
continue;
}
report.due += 1;
let request = kip::request_with(
RAISE_COMMITMENT,
serde_json::Map::from_iter([
(
"key".to_string(),
Json::from(format!("commitment_review:{id}:{due_at}")),
),
("commitment".to_string(), Json::from(id)),
("now".to_string(), Json::from(now.as_str())),
]),
);
match port.run_kip(request, false).await {
Ok(response) if kip::succeeded(&response) => {
if kip::changed(&response, "create") > 0 {
report.raised += 1;
}
}
Ok(response) => report.error = Some(kip::error_message(&response)),
Err(error) => report.error = Some(error.to_string()),
}
}
report
}
pub(crate) async fn watches_in_status(port: &impl RunKip, status: &str) -> Vec<ArmedWatch> {
match read(port, watch::watches_request(status)).await {
Ok(response) => kip::ok_result(&response)
.map(watch::read_watch_rows)
.unwrap_or_default()
.into_iter()
.map(|row| row.watch)
.collect(),
Err(error) => {
log::warn!(
target: "brain",
space_id = port.space_id(),
status;
"reading Watches for the maintenance assessment failed: {error}"
);
Vec::new()
}
}
}
pub(crate) const REVISED_ROOTS_LIMIT: usize = 20;
pub(crate) const DEPENDENTS_DEPTH: u64 = 2;
pub(crate) const DEPENDENTS_LIMIT: usize = 20;
pub(crate) async fn revised_roots(port: &impl RunKip, rows: &[SupersededRow]) -> Vec<RevisedRoot> {
let mut roots = Vec::with_capacity(rows.len().min(REVISED_ROOTS_LIMIT));
for row in rows.iter().take(REVISED_ROOTS_LIMIT) {
let request = kip::request_with(
format!("LIST DEPENDENTS :root DEPTH {DEPENDENTS_DEPTH} LIMIT {DEPENDENTS_LIMIT}"),
kip::param("root", row.assertion.as_str()),
);
let (dependents, truncated) = match read(port, request).await {
Ok(response) => (dependents_of(&response), dependents_truncated(&response)),
Err(error) => {
log::warn!(
target: "brain",
space_id = port.space_id(),
assertion = row.assertion;
"walking the dependents of a revised root failed: {error}"
);
(Vec::new(), true)
}
};
roots.push(RevisedRoot {
assertion: row.assertion.clone(),
proposition: row.proposition.clone(),
actor: row.actor.clone(),
superseded_by: row.superseded_by.clone(),
space_seq: row.space_seq,
dependents,
truncated,
});
}
roots
}
fn dependents_of(response: &Response) -> Vec<Dependent> {
kip::ok_result(response)
.and_then(Json::as_array)
.map(|rows| {
rows.iter()
.filter_map(|row| {
Some(Dependent {
id: row.get("id")?.as_str()?.to_string(),
kind: row
.get("kind")
.and_then(Json::as_str)
.unwrap_or_default()
.to_string(),
distance: row.get("distance").and_then(Json::as_u64).unwrap_or(1),
via: row
.get("via")
.and_then(|via| via.get("activity"))
.and_then(Json::as_str)
.map(str::to_string),
})
})
.collect()
})
.unwrap_or_default()
}
fn dependents_truncated(response: &Response) -> bool {
let Some(result) = response.results.first() else {
return false;
};
result.next_cursor.is_some()
|| result.warnings.iter().any(|warning| {
matches!(warning, anda_kip::Warning::Coded { code, .. } if code == "truncated")
})
}
pub(crate) fn skill_settlement() -> SkillSettlement {
SkillSettlement {
unsupported_reason: Some("memory_learning requires configured independent observers, frozen trials and replayable evaluations".into()),
..Default::default()
}
}
fn superseded_rows(result: &Json) -> Vec<SupersededRow> {
let element_id = |value: &Json| -> Option<String> {
match value {
Json::String(id) => Some(id.clone()),
Json::Object(map) => map.get("id").and_then(Json::as_str).map(str::to_string),
_ => None,
}
};
result
.as_array()
.map(|rows| {
rows.iter()
.filter_map(|row| {
let columns = row.as_array()?;
let id = columns.first().and_then(Json::as_str)?;
Some(SupersededRow {
assertion: id.to_string(),
space_seq: columns.get(1).and_then(Json::as_u64)?,
actor: columns.get(2).and_then(element_id),
proposition: columns.get(3).and_then(element_id),
superseded_by: columns
.get(4)
.and_then(Json::as_array)
.map(|ids| ids.iter().filter_map(element_id).collect())
.unwrap_or_default(),
})
})
.collect()
})
.unwrap_or_default()
}
#[cfg(test)]
mod tests {
use super::*;
use anda_kip::{KipError, KipErrorCode};
use parking_lot::Mutex;
use serde_json::json;
use std::collections::VecDeque;
#[derive(Default)]
struct FakeKip {
replies: Mutex<Vec<(&'static str, VecDeque<Response>)>>,
seen: Mutex<Vec<(String, bool)>>,
}
impl FakeKip {
fn new(replies: impl IntoIterator<Item = (&'static str, Vec<Response>)>) -> Self {
Self {
replies: Mutex::new(
replies
.into_iter()
.map(|(key, queue)| (key, VecDeque::from(queue)))
.collect(),
),
seen: Mutex::new(Vec::new()),
}
}
fn seen(&self) -> Vec<(String, bool)> {
self.seen.lock().clone()
}
fn wrote(&self) -> Vec<String> {
self.seen()
.into_iter()
.filter(|(_, readonly)| !readonly)
.map(|(command, _)| command)
.collect()
}
}
impl RunKip for FakeKip {
fn space_id(&self) -> &str {
"fake_space"
}
async fn run_kip(&self, request: Request, readonly: bool) -> Result<Response, BoxError> {
let command = request
.operations
.first()
.and_then(|op| op.command.clone())
.unwrap_or_default();
self.seen.lock().push((command.clone(), readonly));
let mut replies = self.replies.lock();
for (key, queue) in replies.iter_mut() {
if command.contains(*key)
&& let Some(response) = queue.pop_front()
{
return Ok(response);
}
}
Ok(Response::ok(json!([])))
}
}
fn failed(message: &str) -> Response {
Response::failed(KipError::new(KipErrorCode::InternalError, message))
}
#[tokio::test]
async fn the_correction_cursor_never_steps_over_a_half_read_coordinate() {
let scan =
scan_corrections(&superseded(&[(7, "actor_a"), (9, "actor_b")]), 3u64.into()).await;
assert_eq!(scan.watermark, 9);
assert_eq!(scan.rows.len(), 2);
assert_eq!(scan.rows[0].actor.as_deref(), Some("actor_a"));
assert!(!scan.incomplete);
assert_eq!(
scan_corrections(&superseded(&[]), 3u64.into())
.await
.watermark,
3
);
let mut page: Vec<(u64, &str)> = vec![(7, "a"), (8, "a")];
page.extend((0..SETTLEMENT_BATCH_LIMIT - 2).map(|_| (9u64, "a")));
let full = scan_corrections(&superseded(&page), 3u64.into()).await;
assert_eq!(full.watermark, 8);
assert!(full.incomplete);
assert!(!full.cursor.after_id.is_empty());
let ahead = scan_corrections(&superseded(&page), 8u64.into()).await;
assert_eq!(ahead.watermark, 8);
let further = scan_corrections(&superseded(&page), 42u64.into()).await;
assert_eq!(further.watermark, 42);
let one: Vec<(u64, &str)> = (0..SETTLEMENT_BATCH_LIMIT).map(|_| (9u64, "a")).collect();
let stuck = scan_corrections(&superseded(&one), 3u64.into()).await;
assert_eq!(stuck.watermark, 8);
assert_eq!(stuck.cursor.seq, 9);
assert!(!stuck.cursor.after_id.is_empty());
assert!(stuck.incomplete);
}
#[tokio::test]
async fn a_failed_correction_scan_leaves_the_cursor_where_it_was() {
let port = FakeKip::new([("superseded", vec![failed("full-scan cap")])]);
let scan = scan_corrections(&port, 42u64.into()).await;
assert_eq!(scan.watermark, 42);
assert!(scan.rows.is_empty());
assert!(scan.error.unwrap().contains("full-scan cap"));
assert!(port.wrote().is_empty());
}
#[tokio::test]
async fn a_transaction_larger_than_one_page_is_fully_discovered() {
let rows: Vec<Json> = (0..SETTLEMENT_BATCH_LIMIT)
.map(|i| {
serde_json::json!([
format!("A-{}", 1000+i), 9, {"id":"C-1"}, {"id":"P-1"}, [{"id":"A-9999"}]
])
})
.collect();
let port = FakeKip::new([(
"superseded",
vec![
Response::ok(Json::Array(rows)),
Response::ok(
serde_json::json!([["A-2000",9,{"id":"C-1"},{"id":"P-1"},[{"id":"A-9999"}]]]),
),
],
)]);
let first = scan_corrections(&port, 0u64.into()).await;
assert_eq!(first.rows.len(), SETTLEMENT_BATCH_LIMIT);
assert!(first.incomplete);
assert_eq!(first.watermark, 8);
let second = scan_corrections(&port, first.cursor).await;
assert_eq!(second.rows.len(), 1);
assert_eq!(second.rows[0].assertion, "A-2000");
assert!(!second.incomplete);
assert_eq!(second.watermark, 9);
assert!(second.cursor.after_id.is_empty());
}
fn superseded(rows: &[(u64, &str)]) -> FakeKip {
let result: Vec<_> = rows
.iter()
.enumerate()
.map(|(n, (seq, actor))| json!([format!("A-{n}"), seq, actor, "P-1", ["A-99"]]))
.collect();
FakeKip::new([("superseded", vec![Response::ok(json!(result))])])
}
#[tokio::test]
async fn revised_roots_carry_what_each_superseded_claim_fed() {
let rows = vec![
SupersededRow {
assertion: "A-3".into(),
space_seq: 40,
actor: Some("C-7".into()),
proposition: Some("P-11".into()),
superseded_by: vec!["A-9".into()],
},
SupersededRow {
assertion: "A-4".into(),
space_seq: 41,
actor: None,
proposition: None,
superseded_by: vec![],
},
];
let port = FakeKip::new([(
"LIST DEPENDENTS",
vec![
Response::ok(json!([
{"id": "C-30", "kind": "concept", "distance": 1, "via": {"activity": "ACT-5"}},
{"id": "C-31", "kind": "concept", "distance": 2, "via": {"activity": "ACT-6"}}
])),
failed("engine busy"),
],
)]);
let roots = revised_roots(&port, &rows).await;
assert_eq!(roots.len(), 2);
assert_eq!(roots[0].assertion, "A-3");
assert_eq!(roots[0].superseded_by, vec!["A-9".to_string()]);
assert_eq!(roots[0].dependents.len(), 2);
assert_eq!(roots[0].dependents[0].id, "C-30");
assert_eq!(roots[0].dependents[0].via.as_deref(), Some("ACT-5"));
assert!(!roots[0].truncated);
assert!(roots[1].dependents.is_empty());
assert!(roots[1].truncated);
assert!(port.wrote().is_empty());
}
#[test]
fn learning_is_explicitly_unavailable_without_a_pipeline() {
let report = skill_settlement();
assert_eq!(report.graded, 0);
assert_eq!(report.transitions, 0);
assert!(report.unsupported_reason.is_some());
}
}