Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

48 changes: 47 additions & 1 deletion crates/tinymemory-api/src/provider/records.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -18,7 +18,9 @@ use async_trait::async_trait;

use crate::error::MemoryError;
use crate::goals::GoalsDoc;
use crate::provider::types::{IngestOutcome, MaintenanceReport, SourceItem};
use crate::provider::types::{
IngestOutcome, MaintenanceReport, QueueFailure, QueueStats, SourceItem, StoreStats,
};
use crate::tool_memory::ToolMemoryRule;
use crate::types::MemoryTaint;

Expand DownExpand Up@@ -164,4 +166,48 @@ pub trait MemoryMaintenance: Send + Sync {
/// Backend failures only. A *finding* is not an error: a store with
/// problems still returns `Ok` with the problems listed.
async fn doctor(&self) -> Result<MaintenanceReport, MemoryError>;

/// Aggregate counts over what this driver has stored.
///
/// Defaulted to an empty [`StoreStats`] rather than `Unsupported`, and the
/// difference matters: this is a diagnostic, and a caller asking "how much
/// is stored" can do something sensible with "nothing reported" while
/// having nothing to do with an error. A driver that can answer should.
///
/// # Errors
///
/// Backend failures only.
async fn store_stats(&self) -> Result<StoreStats, MemoryError> {
Ok(StoreStats::default())
}

/// The ingest and re-embed queue's state.
///
/// `kind` narrows to one job kind (the driver's own identifier); `None`
/// counts every kind. A driver with no queue answers all-zero, which is
/// true of it rather than a refusal — and so does a kind this driver does
/// not have, since "no jobs of a kind I never enqueue" is the honest
/// count. A caller that does not know the driver's vocabulary passes
/// `None`; that is what the `Option` is for.
///
/// # Errors
///
/// Backend failures only.
async fn queue_stats(&self, kind: Option<&str>) -> Result<QueueStats, MemoryError> {
let _ = kind;
Ok(QueueStats::default())
}

/// The most recent terminal queue failure, if the driver records one.
///
/// `Ok(None)` means "nothing has failed", which is why this is not an
/// error: a healthy queue and a driver that keeps no failure history give
/// the same answer, and neither is a fault the caller can act on.
///
/// # Errors
///
/// Backend failures only.
async fn latest_queue_failure(&self) -> Result<Option<QueueFailure>, MemoryError> {
Ok(None)
}
}
2 changes: 1 addition & 1 deletion crates/tinymemory-bus/src/lib.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,7 +2,7 @@
//! the members that carry them.
//!
//! TinyMemory ships as a loadable `TinyBus` module: `crates/tinymemory-module`
//! exports one object with 89 members on it, built as a `cdylib`. A host that
//! exports one object with 92 members on it, built as a `cdylib`. A host that
//! loads it — OpenHuman — can call into it but cannot `use` anything out of it,
//! so the payload vocabulary has to be published as an ordinary library. This
//! is that library.
Expand Down
11 changes: 10 additions & 1 deletion crates/tinymemory-bus/src/names.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -150,6 +150,12 @@ pub mod methods {
pub const CONSOLIDATE: &str = "Consolidate";
/// `Doctor` — doctor.
pub const DOCTOR: &str = "Doctor";
/// `StoreStats` — aggregate counts over what the driver has stored.
pub const STORE_STATS: &str = "StoreStats";
/// `QueueStats` — the ingest and re-embed queue's state.
pub const QUEUE_STATS: &str = "QueueStats";
/// `LatestQueueFailure` — the most recent terminal queue failure.
pub const LATEST_QUEUE_FAILURE: &str = "LatestQueueFailure";

// The people store: ranking, handles, scores and interactions.
/// `ListPeople` — list people.
Expand DownExpand Up@@ -239,7 +245,7 @@ pub mod methods {
/// The order matters: `tinybus`'s `Interface::members()` returns declaration
/// order, and the module compares the two sequences directly rather than as
/// sets, so a reordering is caught alongside an addition or a removal.
pub const METHODS: [&str; 89] = [
pub const METHODS: [&str; 92] = [
methods::DRIVER_ID,
methods::CAPABILITIES,
methods::HEALTH,
Expand DownExpand Up@@ -291,6 +297,9 @@ pub const METHODS: [&str; 89] = [
methods::COMPACT,
methods::CONSOLIDATE,
methods::DOCTOR,
methods::STORE_STATS,
methods::QUEUE_STATS,
methods::LATEST_QUEUE_FAILURE,
methods::LIST_PEOPLE,
methods::GET_PERSON,
methods::RESOLVE_HANDLE,
Expand Down
101 changes: 101 additions & 0 deletions crates/tinymemory-bus/src/provider/types.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -388,6 +388,107 @@ pub struct MaintenanceReport {
pub findings: Vec<String>,
}

/// Aggregate counts over what the driver has stored.
///
/// Separate from [`MaintenanceReport`] because the caller does something
/// different with it: a report is read by an operator, these are read by code.
/// `findings: Vec<String>` cannot answer "how far behind is the pipeline"
/// without parsing prose back into numbers.
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct StoreStats {
/// Chunks the driver holds.
pub chunks: u64,
/// Of those chunks, how many the driver has extracted structure from.
///
/// A count rather than the ratio a caller displays, because the ratio is
/// only meaningful against the denominator it was measured with. Read
/// separately, the two can be sampled either side of a write and produce a
/// coverage above 1.0; read together they cannot.
///
/// A driver that does not extract structure leaves this at zero, which
/// reads as "nothing extracted" — correct for it, and the reason a caller
/// should show the pair rather than the ratio alone.
pub chunks_with_structure: u64,
/// Timestamp of the most recently stored chunk, if any.
///
/// `None` for an empty store — distinct from `Some(0)`, which would be a
/// chunk stamped at the epoch.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub most_recent_chunk_ms: Option<i64>,
}

/// The ingest and re-embed queue's state, as counts rather than rows.
///
/// Every field answers a question an operator or a health probe asks about
/// throughput. A driver with no queue answers all-zero rather than refusing:
/// "nothing is backed up" is true of a driver that cannot back up.
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct QueueStats {
/// Jobs waiting, whatever their scheduled time.
pub ready: u64,
/// Jobs a worker currently holds.
pub running: u64,
/// Jobs that finished successfully.
pub done: u64,
/// Jobs that ended in a terminal failure.
pub failed: u64,
/// Of those failures, how many the driver will not retry on its own.
///
/// The distinction is what separates an alert from a shrug: transient
/// failures self-heal on the next attempt, and a caller that escalates on
/// [`Self::failed`] alone pages someone for a queue that is already
/// recovering. Counted with `failed` rather than beside it, because two
/// reads can land either side of a retry and report more unrecoverable
/// failures than there are failures.
pub failed_unrecoverable: u64,
/// Ready jobs whose scheduled time has already passed.
///
/// The difference between this and [`Self::ready`] is deferred work, and
/// conflating them reads a healthy backlog of future jobs as a stall.
pub eligible_now: u64,
/// When the queue last settled a job.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub last_completed_ms: Option<i64>,
/// The scheduled time of the oldest job eligible to run now.
///
/// With [`Self::last_completed_ms`] this is what an idle-time calculation
/// needs: how long something runnable has been waiting.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub oldest_eligible_ms: Option<i64>,
}

/// The most recent terminal queue failure.
///
/// Carries the driver's own words rather than a class this contract invents:
/// the caller shows it to an operator, and a re-classification here would lose
/// what the engine actually said.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct QueueFailure {
/// The failure's own message. Must carry no memory content.
pub reason: String,
/// The driver's classification, when it has one.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub class: Option<String>,
/// When the failing job settled.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub completed_at_ms: Option<i64>,
/// When the queue last completed a job *successfully*, read together with
/// the failure above rather than in a second call.
///
/// A caller deciding whether to show this failure asks whether anything
/// has succeeded since it — a success after the failure means the queue
/// recovered and the failure is stale. Answering that from two separate
/// calls lets a job settle in between and flip the decision, so the two
/// values are read as one observation.
///
/// This is not [`QueueStats::last_completed_ms`]: that one counts a
/// failure as progress, because a fast-failing queue is not a stalled
/// one. Supersession needs the opposite reading — only a success clears a
/// failure — so it takes the newest *successful* completion.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub last_success_ms: Option<i64>,
}

#[cfg(test)]
#[path = "types_tests.rs"]
mod tests;
1 change: 1 addition & 0 deletions crates/tinymemory-module/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions crates/tinymemory-module/src/lib.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -307,6 +307,9 @@ mod exports {
"Compact",
"Consolidate",
"Doctor",
"StoreStats",
"QueueStats",
"LatestQueueFailure",
],
signals = [],
// The host's embedder is deliberately NOT declared as `requires`. That
Expand Down
23 changes: 22 additions & 1 deletion crates/tinymemory-module/src/service/mod.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -121,7 +121,7 @@ use tinymemory_api::goals::GoalsDoc;
use tinymemory_api::health::MemoryHealth;
use tinymemory_api::provider::types::{
DiffReport, EntityHit, ExportPage, ExportRecord, ImportOutcome, IngestItem, IngestOutcome,
MaintenanceReport, SnapshotRef, SourceItem, SourceScope,
MaintenanceReport, QueueFailure, QueueStats, SnapshotRef, SourceItem, SourceScope, StoreStats,
};
// `MemoryCore`, `MemoryRecall` and `MemoryPortability` are deliberately not
// imported: they are supertraits of `MemoryProvider`, so their methods are
Expand DownExpand Up@@ -887,6 +887,27 @@ impl MemoryService {
.map_err(|error| into_bus_error(&error))
}

async fn store_stats(&self) -> BusResult<StoreStats> {
require_family!(self, as_maintenance, Capability::Maintenance)
.store_stats()
.await
.map_err(|error| into_bus_error(&error))
}

async fn queue_stats(&self, kind: Option<String>) -> BusResult<QueueStats> {
require_family!(self, as_maintenance, Capability::Maintenance)
.queue_stats(kind.as_deref())
.await
.map_err(|error| into_bus_error(&error))
}

async fn latest_queue_failure(&self) -> BusResult<Option<QueueFailure>> {
require_family!(self, as_maintenance, Capability::Maintenance)
.latest_queue_failure()
.await
.map_err(|error| into_bus_error(&error))
}

// ── People ──────────────────────────────────────────────────────────────

/// Known people, ranked by closeness.
Expand Down
2 changes: 1 addition & 1 deletion crates/tinymemory-module/src/service/test.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -544,7 +544,7 @@ fn the_served_members_are_exactly_the_published_contract() {
.map(|member| (*member).to_string())
.collect();

// Reported as differences rather than as a 89-element inequality, so the
// Reported as differences rather than as a 92-element inequality, so the
// failure names the method that moved instead of printing both lists.
let missing: Vec<&String> = served.iter().filter(|m| !published.contains(m)).collect();
assert!(
Expand Down
3 changes: 3 additions & 0 deletions crates/tinymemory-module/tests/module_e2e.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -671,6 +671,9 @@ const EXPECTED_METHODS: &[&str] = &[
"Compact",
"Consolidate",
"Doctor",
"StoreStats",
"QueueStats",
"LatestQueueFailure",
];

#[tokio::test]
Expand Down
9 changes: 9 additions & 0 deletions crates/tinymemory-tinycortex/Cargo.toml
Original file line numberDiff line numberDiff line change
Expand Up@@ -14,6 +14,15 @@ repository = "https://github.com/tinyhumansai/tinymemory"
[dependencies]
# The contract this adapter targets.
tinymemory-api = { path = "../tinymemory-api" }
# The diagnostic reads in `MemoryMaintenance` query TinyCortex's own tables
# directly, which is this crate's job — it exists to adapt that schema to the
# contract, and the schema is not something the contract can describe.
#
# Pinned to the exact version `tinymemory-core` uses, deliberately. `rusqlite`
# carries a `links` key through `libsqlite3-sys`, so two different versions in
# one graph is a hard cargo error rather than a silent duplicate; matching the
# pin makes cargo unify them onto the one bundled copy already present.
rusqlite = { version = "=0.40.2", features = ["bundled"] }
# The engine being adapted. A version requirement rather than a path, so a host
# that already pins its own TinyCortex checkout unifies both onto one copy
# through its `[patch.crates-io]`; the workspace root patches it to the nested
Expand Down
Loading
Loading