exocortex-ingest 0.4.0

Exocortex ingestion service: HMAC-authenticated batches through fingerprint, ceiling, and triple validation, with server-side entity extraction and embeddings.
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use std::time::{Duration, Instant};

use exocortex_ingest::{Embedder, IngestServer};
use exocortex_pack_dev_v1::pack_def;
use exocortex_storage::InMemoryStorage;
use exocortex_wire::ingest::v1::{
    ingest_service_server::IngestService, IngestBatch, MemoryDraft, ProducerIdentity,
};
use tonic::Request;

const HMAC_KEY: [u8; 32] = [0x79; 32];

struct BlockingBatchEmbedder {
    single_calls: AtomicUsize,
    batch_calls: AtomicUsize,
    last_batch_len: AtomicUsize,
}

impl BlockingBatchEmbedder {
    fn new() -> Self {
        Self {
            single_calls: AtomicUsize::new(0),
            batch_calls: AtomicUsize::new(0),
            last_batch_len: AtomicUsize::new(0),
        }
    }
}

impl Embedder for BlockingBatchEmbedder {
    fn embed(&self, _text: &str) -> Result<Vec<f32>, String> {
        if self.single_calls.fetch_add(1, Ordering::SeqCst) == 0 {
            std::thread::sleep(Duration::from_millis(400));
        }
        Ok(vec![1.0, 0.0, 0.0, 0.0])
    }

    fn embed_batch(&self, texts: &[String]) -> Result<Vec<Vec<f32>>, String> {
        self.batch_calls.fetch_add(1, Ordering::SeqCst);
        self.last_batch_len.store(texts.len(), Ordering::SeqCst);
        std::thread::sleep(Duration::from_millis(400));
        Ok(vec![vec![1.0, 0.0, 0.0, 0.0]; texts.len()])
    }

    fn model_id(&self) -> &'static str {
        "blocking-test"
    }

    fn model_version(&self) -> &'static str {
        "v1"
    }

    fn dim(&self) -> usize {
        4
    }
}

fn draft(index: usize) -> MemoryDraft {
    MemoryDraft {
        rights: None,
        draft_key: format!("row-{index}"),
        id: String::new(),
        memory_type: "Solution".into(),
        title: format!("blocking embedding {index}"),
        content: "the synchronous model must not occupy a Tokio worker".into(),
        tags: vec![],
        visibility: 3,
        valid_from: None,
        valid_until: None,
        external_key: None,
    }
}

#[tokio::test(flavor = "current_thread")]
async fn max_batch_embedding_is_one_blocking_invocation_without_worker_starvation() {
    let ontology = Arc::new(
        exocortex_kernel::Ontology::from_packs(vec![pack_def()]).expect("ontology assembles"),
    );
    let storage = Arc::new(InMemoryStorage::new(ontology.clone()));
    let embedder = Arc::new(BlockingBatchEmbedder::new());
    let server = IngestServer::new(storage, ontology, HMAC_KEY).with_embedder(embedder.clone());
    server
        .register_source(Request::new(exocortex_wire::signing::registration(
            &HMAC_KEY,
            "org",
            "custom://blocking",
            "blocking-test",
            3,
            "custom",
            "test-node",
            exocortex_wire::ingest::v1::ProducerKind::CodingAgent,
        )))
        .await
        .unwrap();
    let mut batch = IngestBatch {
        org_id: "org".into(),
        source_uri: "custom://blocking".into(),
        producer_id: "blocking-test".into(),
        batch_id: "max-embedding-batch".into(),
        mapping_version: "1".into(),
        ontology_fingerprint: server.ontology.fingerprint.0.to_vec(),
        ceiling: 3,
        checksum: String::new(),
        observed_at: None,
        recorded_at: None,
        snapshot: None,
        memories: (0..exocortex_wire::limits::MAX_MEMORIES_PER_BATCH)
            .map(draft)
            .collect(),
        relationships: vec![],
        producer: Some(ProducerIdentity {
            node_id: "test-node".into(),
            agent_id: String::new(),
            adapter_id: String::new(),
            hmac_signature: vec![],
            client_metadata: None,
        }),
    };
    exocortex_wire::signing::prepare_batch(&HMAC_KEY, &mut batch);

    let started = Instant::now();
    let submit = tokio::spawn(async move { server.submit(Request::new(batch)).await });
    tokio::task::yield_now().await;
    tokio::time::sleep(Duration::from_millis(10)).await;
    assert!(
        started.elapsed() < Duration::from_millis(250),
        "Tokio heartbeat was starved by synchronous inference"
    );

    let ack = submit.await.unwrap().unwrap().into_inner();
    assert_eq!(
        ack.accepted as usize,
        exocortex_wire::limits::MAX_MEMORIES_PER_BATCH
    );
    assert_eq!(embedder.batch_calls.load(Ordering::SeqCst), 1);
    assert_eq!(embedder.single_calls.load(Ordering::SeqCst), 0);
    assert_eq!(
        embedder.last_batch_len.load(Ordering::SeqCst),
        exocortex_wire::limits::MAX_MEMORIES_PER_BATCH
    );
}