use crate::embeddings::{
Embedder, EmbeddingBackendKind, EmbeddingModelKind, ExternalEmbedder, ExternalEmbeddingConfig,
FastEmbedder,
};
use crate::prediction_loss::{
ExternalPredictionLossBackend, ExternalPredictionLossConfig, PredictionLossBackend,
PredictionLossBackendKind, DEFAULT_LOSS_SCALE,
};
use crate::server::{
RankingConfig, DEFAULT_DECAY_FLOOR, DEFAULT_HALF_LIFE_DAYS, DEFAULT_OVERSAMPLE_FACTOR,
};
use crate::surprise::SurpriseWeights;
use crate::{server, storage, HippoError, VERSION};
use anyhow::Context;
use clap::{Args, Parser, Subcommand};
use std::path::PathBuf;
use std::sync::Arc;
use std::time::Duration;
#[derive(Args, Debug, Clone)]
struct EmbeddingFlags {
#[arg(long, env = "HIPPO_EMBEDDING_BACKEND")]
embedding_backend: Option<String>,
#[arg(long, env = "HIPPO_EMBEDDING_MODEL")]
embedding_model: Option<String>,
#[arg(long, env = "HIPPO_EXTERNAL_EMBEDDING_URL")]
external_embedding_url: Option<String>,
#[arg(long, env = "HIPPO_EXTERNAL_EMBEDDING_MODEL")]
external_embedding_model: Option<String>,
#[arg(long, env = "HIPPO_EXTERNAL_EMBEDDING_API_KEY_ENV")]
external_embedding_api_key_env: Option<String>,
#[arg(long, env = "HIPPO_EXTERNAL_EMBEDDING_TIMEOUT_MS")]
external_embedding_timeout_ms: Option<u64>,
#[arg(long, env = "HIPPO_EXTERNAL_EMBEDDING_BATCH_SIZE")]
external_embedding_batch_size: Option<usize>,
#[arg(long, env = "HIPPO_EXTERNAL_EMBEDDING_MAX_RETRIES")]
external_embedding_max_retries: Option<u32>,
}
impl EmbeddingFlags {
fn backend_kind(&self) -> anyhow::Result<EmbeddingBackendKind> {
match self.embedding_backend.as_deref() {
None => Ok(EmbeddingBackendKind::default()),
Some(s) => EmbeddingBackendKind::parse(s).map_err(|e| anyhow::anyhow!(e)),
}
}
}
#[derive(Args, Debug, Clone)]
struct PredictionLossFlags {
#[arg(long, env = "HIPPO_PREDICTION_LOSS_BACKEND")]
prediction_loss_backend: Option<String>,
#[arg(long, env = "HIPPO_PREDICTION_LOSS_URL")]
prediction_loss_url: Option<String>,
#[arg(long, env = "HIPPO_PREDICTION_LOSS_MODEL")]
prediction_loss_model: Option<String>,
#[arg(long, env = "HIPPO_PREDICTION_LOSS_API_KEY_ENV")]
prediction_loss_api_key_env: Option<String>,
#[arg(long, env = "HIPPO_PREDICTION_LOSS_TIMEOUT_MS")]
prediction_loss_timeout_ms: Option<u64>,
#[arg(long, env = "HIPPO_PREDICTION_LOSS_MAX_RETRIES")]
prediction_loss_max_retries: Option<u32>,
#[arg(long, env = "HIPPO_PREDICTION_LOSS_SCALE")]
prediction_loss_scale: Option<f32>,
#[arg(long, env = "HIPPO_CANDLE_MODEL_ID")]
candle_model_id: Option<String>,
#[arg(long, env = "HIPPO_CANDLE_CACHE_DIR")]
candle_cache_dir: Option<PathBuf>,
#[arg(long, env = "HIPPO_CANDLE_CPU")]
candle_cpu: bool,
}
impl PredictionLossFlags {
fn backend_kind(&self) -> anyhow::Result<PredictionLossBackendKind> {
match self.prediction_loss_backend.as_deref() {
None => Ok(PredictionLossBackendKind::default()),
Some(s) => PredictionLossBackendKind::parse(s).map_err(|e| anyhow::anyhow!(e)),
}
}
}
#[derive(Parser, Debug)]
#[command(name = "hippo", version = VERSION,
about = "Claude Code に海馬を足す MCP server (claude-hippo)",
long_about = None)]
struct Cli {
#[command(subcommand)]
command: Option<Cmd>,
}
#[derive(Subcommand, Debug)]
enum Cmd {
Serve {
#[arg(long, env = "HIPPO_DB_PATH")]
db: Option<PathBuf>,
#[arg(long, env = "HIPPO_MODEL_CACHE")]
model_cache: Option<PathBuf>,
#[arg(long, env = "HIPPO_SURPRISE_WEIGHTS")]
surprise_weights: Option<String>,
#[command(flatten)]
embed: EmbeddingFlags,
#[command(flatten)]
prediction: PredictionLossFlags,
#[arg(long, env = "HIPPO_HALF_LIFE_DAYS")]
half_life_days: Option<f32>,
#[arg(long, env = "HIPPO_DECAY_FLOOR")]
decay_floor: Option<f32>,
#[arg(long, env = "HIPPO_OVERSAMPLE_FACTOR")]
oversample_factor: Option<usize>,
#[arg(long, env = "HIPPO_NO_HEBBIAN_REINFORCE")]
no_hebbian_reinforce: bool,
#[arg(long, env = "HIPPO_CO_RECALL_ALPHA")]
co_recall_alpha: Option<f32>,
#[arg(long, env = "HIPPO_ANTHROPIC_MEMORY_TOOL")]
anthropic_memory_tool: bool,
#[arg(long, env = "HIPPO_SHODH_REST")]
shodh_rest: bool,
#[arg(long, env = "HIPPO_SHODH_REST_BIND")]
shodh_rest_bind: Option<String>,
},
Verify {
#[arg(long, env = "HIPPO_DB_PATH")]
db: Option<PathBuf>,
},
Embed {
text: String,
#[arg(long, env = "HIPPO_MODEL_CACHE")]
model_cache: Option<PathBuf>,
#[command(flatten)]
embed: EmbeddingFlags,
},
Bench {
#[arg(long, default_value_t = 100)]
n: usize,
#[arg(long)]
db: Option<PathBuf>,
#[arg(long, env = "HIPPO_MODEL_CACHE")]
model_cache: Option<PathBuf>,
#[arg(long, env = "HIPPO_SURPRISE_WEIGHTS")]
surprise_weights: Option<String>,
#[command(flatten)]
embed: EmbeddingFlags,
#[command(flatten)]
prediction: PredictionLossFlags,
#[arg(long, env = "HIPPO_HALF_LIFE_DAYS")]
half_life_days: Option<f32>,
#[arg(long, env = "HIPPO_DECAY_FLOOR")]
decay_floor: Option<f32>,
#[arg(long, env = "HIPPO_OVERSAMPLE_FACTOR")]
oversample_factor: Option<usize>,
#[arg(long, env = "HIPPO_NO_HEBBIAN_REINFORCE")]
no_hebbian_reinforce: bool,
#[arg(long, env = "HIPPO_CO_RECALL_ALPHA")]
co_recall_alpha: Option<f32>,
},
}
fn default_db_path() -> PathBuf {
dirs::data_local_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join("claude-hippo")
.join("memory.db")
}
fn ensure_parent_dir(p: &std::path::Path) -> std::io::Result<()> {
if let Some(parent) = p.parent() {
std::fs::create_dir_all(parent)?;
}
Ok(())
}
fn parse_model_kind(opt: Option<&str>) -> anyhow::Result<EmbeddingModelKind> {
match opt {
None => Ok(EmbeddingModelKind::default()),
Some(s) => EmbeddingModelKind::parse(s).map_err(|e| anyhow::anyhow!(e)),
}
}
fn build_external_config(flags: &EmbeddingFlags) -> anyhow::Result<ExternalEmbeddingConfig> {
let url = flags.external_embedding_url.clone().ok_or_else(|| {
anyhow::anyhow!(
"--external-embedding-url is required when --embedding-backend=external \
(or set HIPPO_EXTERNAL_EMBEDDING_URL)"
)
})?;
let model = flags.external_embedding_model.clone().ok_or_else(|| {
anyhow::anyhow!(
"--external-embedding-model is required when --embedding-backend=external \
(or set HIPPO_EXTERNAL_EMBEDDING_MODEL)"
)
})?;
let key_env = flags
.external_embedding_api_key_env
.clone()
.unwrap_or_else(|| "OPENAI_API_KEY".to_string());
let api_key = if key_env.eq_ignore_ascii_case("none") || key_env.is_empty() {
String::new()
} else {
std::env::var(&key_env).unwrap_or_else(|_| {
tracing::warn!(
env = key_env.as_str(),
"external embedding api key env not set; sending request without Authorization \
header (use `--external-embedding-api-key-env NONE` to silence)"
);
String::new()
})
};
Ok(ExternalEmbeddingConfig {
url,
model,
dim: crate::EMBEDDING_DIM,
api_key,
timeout: Duration::from_millis(flags.external_embedding_timeout_ms.unwrap_or(5_000)),
batch_size: flags.external_embedding_batch_size.unwrap_or(64),
max_retries: flags.external_embedding_max_retries.unwrap_or(3),
})
}
fn build_embedder_from_flags(
flags: &EmbeddingFlags,
model_cache: Option<PathBuf>,
) -> anyhow::Result<Arc<dyn Embedder>> {
match flags.backend_kind()? {
EmbeddingBackendKind::Local => {
let model = parse_model_kind(flags.embedding_model.as_deref())?;
build_embedder(model_cache, model)
}
EmbeddingBackendKind::External => {
let cfg = build_external_config(flags)?;
let e = ExternalEmbedder::new(cfg).map_err(|e: HippoError| anyhow::anyhow!(e))?;
Ok(Arc::new(e))
}
}
}
fn build_prediction_loss_backend(
flags: &PredictionLossFlags,
) -> anyhow::Result<Option<Arc<dyn PredictionLossBackend>>> {
match flags.backend_kind()? {
PredictionLossBackendKind::None => Ok(None),
PredictionLossBackendKind::OpenAiCompat => {
let url = flags
.prediction_loss_url
.clone()
.ok_or_else(|| anyhow::anyhow!(
"--prediction-loss-url is required when --prediction-loss-backend=openai-compat \
(or set HIPPO_PREDICTION_LOSS_URL)"
))?;
let model = flags
.prediction_loss_model
.clone()
.ok_or_else(|| anyhow::anyhow!(
"--prediction-loss-model is required when --prediction-loss-backend=openai-compat \
(or set HIPPO_PREDICTION_LOSS_MODEL)"
))?;
let key_env = flags
.prediction_loss_api_key_env
.clone()
.unwrap_or_else(|| "OPENAI_API_KEY".to_string());
let api_key = if key_env.eq_ignore_ascii_case("none") || key_env.is_empty() {
String::new()
} else {
std::env::var(&key_env).unwrap_or_default()
};
let cfg = ExternalPredictionLossConfig {
url,
model,
api_key,
timeout: Duration::from_millis(flags.prediction_loss_timeout_ms.unwrap_or(5_000)),
max_retries: flags.prediction_loss_max_retries.unwrap_or(3),
loss_scale: flags.prediction_loss_scale.unwrap_or(DEFAULT_LOSS_SCALE),
};
let backend = ExternalPredictionLossBackend::new(cfg)
.map_err(|e: HippoError| anyhow::anyhow!(e))?;
Ok(Some(Arc::new(backend)))
}
PredictionLossBackendKind::CandleLocal => build_candle_backend(flags),
}
}
#[cfg(feature = "candle")]
fn build_candle_backend(
flags: &PredictionLossFlags,
) -> anyhow::Result<Option<Arc<dyn PredictionLossBackend>>> {
use crate::prediction_loss::{
CandleLocalConfig, CandleLocalPredictionLoss, CANDLE_DEFAULT_LOSS_SCALE,
DEFAULT_CANDLE_MODEL_ID,
};
let cfg = CandleLocalConfig {
model_id: flags
.candle_model_id
.clone()
.unwrap_or_else(|| DEFAULT_CANDLE_MODEL_ID.to_string()),
cache_dir: flags.candle_cache_dir.clone(),
loss_scale: flags
.prediction_loss_scale
.unwrap_or(CANDLE_DEFAULT_LOSS_SCALE),
use_gpu: !flags.candle_cpu,
};
let backend = CandleLocalPredictionLoss::new(cfg).map_err(|e| anyhow::anyhow!(e))?;
Ok(Some(Arc::new(backend)))
}
#[cfg(not(feature = "candle"))]
fn build_candle_backend(
_flags: &PredictionLossFlags,
) -> anyhow::Result<Option<Arc<dyn PredictionLossBackend>>> {
anyhow::bail!(
"--prediction-loss-backend candle-local requires this binary to be built with \
`--features candle` (or `--features candle-cuda` for GPU). Reinstall with \
`cargo install claude-hippo --features candle`."
)
}
fn prediction_loss_label(flags: &PredictionLossFlags) -> String {
match flags.backend_kind().unwrap_or_default() {
PredictionLossBackendKind::None => "none".into(),
PredictionLossBackendKind::OpenAiCompat => format!(
"openai-compat:{}@{}",
flags
.prediction_loss_model
.as_deref()
.unwrap_or("(missing-model)"),
flags
.prediction_loss_url
.as_deref()
.unwrap_or("(missing-url)"),
),
PredictionLossBackendKind::CandleLocal => format!(
"candle-local:{}{}",
flags
.candle_model_id
.as_deref()
.unwrap_or("Qwen/Qwen2.5-0.5B"),
if flags.candle_cpu { " (cpu)" } else { "" },
),
}
}
fn embedding_backend_label(flags: &EmbeddingFlags) -> String {
match flags.backend_kind().unwrap_or_default() {
EmbeddingBackendKind::Local => {
let model = flags.embedding_model.as_deref().unwrap_or("minilm-l6-v2");
format!("local:{model}")
}
EmbeddingBackendKind::External => {
let url = flags
.external_embedding_url
.as_deref()
.unwrap_or("(missing-url)");
let model = flags
.external_embedding_model
.as_deref()
.unwrap_or("(missing-model)");
format!("external:{model}@{url}")
}
}
}
fn parse_weights(opt: Option<&str>) -> anyhow::Result<SurpriseWeights> {
match opt {
None => Ok(SurpriseWeights::default()),
Some(s) => SurpriseWeights::parse_csv(s).map_err(|e| anyhow::anyhow!(e)),
}
}
fn build_ranking_config(
half_life_days: Option<f32>,
decay_floor: Option<f32>,
oversample_factor: Option<usize>,
no_hebbian_reinforce: bool,
co_recall_alpha: Option<f32>,
) -> anyhow::Result<RankingConfig> {
let hl = half_life_days.unwrap_or(DEFAULT_HALF_LIFE_DAYS);
if hl < 0.0 {
anyhow::bail!("--half-life-days must be ≥ 0 (0 disables decay), got {hl}");
}
let floor = decay_floor.unwrap_or(DEFAULT_DECAY_FLOOR);
if !(0.0..=1.0).contains(&floor) {
anyhow::bail!("--decay-floor must be in 0.0..=1.0, got {floor}");
}
let factor = oversample_factor.unwrap_or(DEFAULT_OVERSAMPLE_FACTOR);
if factor == 0 {
anyhow::bail!("--oversample-factor must be ≥ 1, got 0");
}
let alpha = co_recall_alpha.unwrap_or(crate::server::DEFAULT_CO_RECALL_ALPHA);
if !(0.0..=1.0).contains(&alpha) {
anyhow::bail!("--co-recall-alpha must be in 0.0..=1.0, got {alpha}");
}
Ok(RankingConfig {
half_life_days: hl,
decay_floor: floor,
default_oversample_factor: factor,
reinforce_co_recall: !no_hebbian_reinforce,
co_recall_alpha: alpha,
})
}
fn build_embedder(
model_cache: Option<PathBuf>,
model: EmbeddingModelKind,
) -> anyhow::Result<Arc<dyn Embedder>> {
let cache = model_cache.unwrap_or_else(crate::embeddings::default_cache_dir);
let e = FastEmbedder::new_with_model(cache, model)?;
Ok(Arc::new(e))
}
pub async fn run() -> anyhow::Result<()> {
let _ = tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")),
)
.with_writer(std::io::stderr)
.try_init();
let cli = Cli::parse();
let cmd = cli.command.unwrap_or(Cmd::Serve {
db: None,
model_cache: None,
surprise_weights: None,
embed: EmbeddingFlags {
embedding_backend: None,
embedding_model: None,
external_embedding_url: None,
external_embedding_model: None,
external_embedding_api_key_env: None,
external_embedding_timeout_ms: None,
external_embedding_batch_size: None,
external_embedding_max_retries: None,
},
prediction: PredictionLossFlags {
prediction_loss_backend: None,
prediction_loss_url: None,
prediction_loss_model: None,
prediction_loss_api_key_env: None,
prediction_loss_timeout_ms: None,
prediction_loss_max_retries: None,
prediction_loss_scale: None,
candle_model_id: None,
candle_cache_dir: None,
candle_cpu: false,
},
half_life_days: None,
decay_floor: None,
oversample_factor: None,
no_hebbian_reinforce: false,
co_recall_alpha: None,
anthropic_memory_tool: false,
shodh_rest: false,
shodh_rest_bind: None,
});
storage::register_sqlite_vec();
match cmd {
Cmd::Serve {
db,
model_cache,
surprise_weights,
embed,
prediction,
half_life_days,
decay_floor,
oversample_factor,
no_hebbian_reinforce,
co_recall_alpha,
anthropic_memory_tool,
shodh_rest,
shodh_rest_bind,
} => {
let path = db.unwrap_or_else(default_db_path);
ensure_parent_dir(&path)?;
let weights = parse_weights(surprise_weights.as_deref())?;
let ranking = build_ranking_config(
half_life_days,
decay_floor,
oversample_factor,
no_hebbian_reinforce,
co_recall_alpha,
)?;
let backend_label = embedding_backend_label(&embed);
let pl_label = prediction_loss_label(&prediction);
let store = storage::Storage::open(&path)?;
let embedder = build_embedder_from_flags(&embed, model_cache)?;
let pl_backend = build_prediction_loss_backend(&prediction)?;
tracing::info!(
?path,
backend = backend_label.as_str(),
prediction_loss = pl_label.as_str(),
anthropic_memory_tool,
shodh_rest,
?weights,
?ranking,
"claude-hippo serve starting (rmcp stdio)"
);
run_serve_with_optional_rest(
store,
embedder,
pl_backend,
weights,
ranking,
anthropic_memory_tool,
shodh_rest,
shodh_rest_bind,
)
.await
}
Cmd::Verify { db } => {
let path = db.unwrap_or_else(default_db_path);
ensure_parent_dir(&path)?;
let store = storage::Storage::open(&path)?;
let alive = store.count_alive()?;
let total = store.count_total()?;
let vec_v = store.vec_version()?;
println!("hippo verify ✓");
println!(" db path : {}", path.display());
println!(" vec_version : {vec_v}");
println!(" alive : {alive}");
println!(" total : {total} (incl. soft-deleted)");
Ok(())
}
Cmd::Embed {
text,
model_cache,
embed,
} => {
let backend_label = embedding_backend_label(&embed);
let embedder = build_embedder_from_flags(&embed, model_cache)?;
let t0 = std::time::Instant::now();
let v = embedder.embed_one(&text)?;
let dt = t0.elapsed();
let norm: f32 = v.iter().map(|x| x * x).sum::<f32>().sqrt();
println!("hippo embed ✓");
println!(" text : {text:?}");
println!(" backend : {backend_label}");
println!(" total : {dt:?}");
println!(" dim : {}", v.len());
println!(" L2 norm : {norm:.6}");
println!(" first 5 : {:?}", &v[..5.min(v.len())]);
Ok(())
}
Cmd::Bench {
n,
db,
model_cache,
surprise_weights,
embed,
prediction,
half_life_days,
decay_floor,
oversample_factor,
no_hebbian_reinforce,
co_recall_alpha,
} => {
let weights = parse_weights(surprise_weights.as_deref())?;
let ranking = build_ranking_config(
half_life_days,
decay_floor,
oversample_factor,
no_hebbian_reinforce,
co_recall_alpha,
)?;
run_self_bench(n, db, model_cache, weights, ranking, embed, prediction).await
}
}
}
#[allow(clippy::too_many_arguments)]
async fn run_serve_with_optional_rest(
store: storage::Storage,
embedder: Arc<dyn Embedder>,
pl_backend: Option<Arc<dyn PredictionLossBackend>>,
weights: SurpriseWeights,
ranking: RankingConfig,
enable_memory_tool: bool,
shodh_rest: bool,
shodh_rest_bind: Option<String>,
) -> anyhow::Result<()> {
if !shodh_rest {
return server::run_stdio_full_with_memory_tool(
store,
embedder,
pl_backend,
weights,
ranking,
enable_memory_tool,
)
.await;
}
let bind: std::net::SocketAddr = shodh_rest_bind
.as_deref()
.unwrap_or("127.0.0.1:8765")
.parse()
.map_err(|e| anyhow::anyhow!("--shodh-rest-bind invalid socket addr: {e}"))?;
let shared_storage = Arc::new(tokio::sync::Mutex::new(store));
let rest_instance = Arc::new(server::MemoryServer::from_shared_storage(
shared_storage.clone(),
embedder.clone(),
pl_backend.clone(),
weights,
ranking,
enable_memory_tool,
));
let mcp_instance = server::MemoryServer::from_shared_storage(
shared_storage,
embedder,
pl_backend,
weights,
ranking,
enable_memory_tool,
);
tracing::info!(
?bind,
"claude-hippo serving stdio MCP + SHODH REST in the same process \
(shared SQLite via Arc<Mutex<Storage>>)"
);
let rest_handle = tokio::spawn(crate::shodh_rest::serve(rest_instance, bind));
let mcp_handle = tokio::spawn(async move {
use rmcp::ServiceExt;
let svc = mcp_instance
.serve(rmcp::transport::io::stdio())
.await
.map_err(|e| anyhow::anyhow!("rmcp serve init failed: {e}"))?;
svc.waiting().await.ok();
Ok::<(), anyhow::Error>(())
});
tokio::select! {
r = rest_handle => match r {
Ok(inner) => inner.context("SHODH REST exited")?,
Err(e) => anyhow::bail!("SHODH REST task join: {e}"),
},
r = mcp_handle => match r {
Ok(inner) => inner.context("MCP stdio exited")?,
Err(e) => anyhow::bail!("MCP stdio task join: {e}"),
},
}
Ok(())
}
async fn run_self_bench(
n: usize,
db: Option<PathBuf>,
model_cache: Option<PathBuf>,
weights: SurpriseWeights,
ranking: RankingConfig,
embed_flags: EmbeddingFlags,
pl_flags: PredictionLossFlags,
) -> anyhow::Result<()> {
use std::time::Instant;
let db_path = db.unwrap_or_else(|| {
let mut p = std::env::temp_dir();
p.push(format!("claude-hippo-bench-{}.db", std::process::id()));
p
});
ensure_parent_dir(&db_path)?;
let _ = std::fs::remove_file(&db_path);
let backend_label = embedding_backend_label(&embed_flags);
let pl_label = prediction_loss_label(&pl_flags);
let cold0 = Instant::now();
let store = storage::Storage::open(&db_path)?;
let embedder = build_embedder_from_flags(&embed_flags, model_cache)?;
let pl_backend = build_prediction_loss_backend(&pl_flags)?;
let _ = embedder.embed_one("warmup")?;
let cold = cold0.elapsed();
let server = server::MemoryServer::new_full(store, embedder, pl_backend, weights, ranking);
let t1 = Instant::now();
let mut store_lats = Vec::with_capacity(n);
for i in 0..n {
let st = Instant::now();
let _ = server
.do_remember(server::RememberParams {
content: format!("bench memory {i}: timing harness"),
tags: vec!["bench".into(), format!("i{}", i % 10)],
memory_type: Some("Observation".into()),
importance: Some(0.5),
metadata: None,
})
.await
.map_err(|e| anyhow::anyhow!("store err: {:?}", e))?;
store_lats.push(st.elapsed().as_secs_f64() * 1000.0);
}
let store_total = t1.elapsed();
let t2 = Instant::now();
let mut retrieve_lats = Vec::with_capacity(n);
for _ in 0..n {
let st = Instant::now();
let _ = server
.do_recall(server::RecallParams {
query: "timing harness memory".into(),
limit: 5,
no_surprise_boost: false,
oversample_factor: None,
mode: None,
seed_id: None,
})
.await
.map_err(|e| anyhow::anyhow!("retrieve err: {:?}", e))?;
retrieve_lats.push(st.elapsed().as_secs_f64() * 1000.0);
}
let retrieve_total = t2.elapsed();
fn pct(xs: &mut [f64], p: f64) -> f64 {
xs.sort_by(|a, b| a.partial_cmp(b).unwrap());
let k = ((xs.len() - 1) as f64) * p;
let f = k.floor() as usize;
let c = (f + 1).min(xs.len() - 1);
if f == c {
xs[f]
} else {
xs[f] + (xs[c] - xs[f]) * (k - f as f64)
}
}
let rss_kb = read_self_rss_kb().unwrap_or(0);
println!("claude-hippo self-bench ✓");
println!(" backend : {backend_label}");
println!(" prediction : {pl_label}");
println!(
" weights : outlier={:.2} engagement={:.2} explicit={:.2} prediction={:.2}",
server.weights().w_outlier,
server.weights().w_engagement,
server.weights().w_explicit,
server.weights().w_prediction,
);
let rc = server.ranking_config();
println!(
" ranking : half_life_days={:.1} decay_floor={:.2} oversample_factor={}",
rc.half_life_days, rc.decay_floor, rc.default_oversample_factor,
);
println!(" cold-start (db open + embed warmup) : {cold:?}");
println!(
" store x{n}: total={store_total:?} p50={:.1}ms p95={:.1}ms",
pct(&mut store_lats.clone(), 0.5),
pct(&mut store_lats.clone(), 0.95),
);
println!(
" retrieve x{n}: total={retrieve_total:?} p50={:.1}ms p95={:.1}ms",
pct(&mut retrieve_lats.clone(), 0.5),
pct(&mut retrieve_lats.clone(), 0.95),
);
println!(" peak RSS (self) : {:.1} MB", rss_kb as f64 / 1024.0);
Ok(())
}
fn read_self_rss_kb() -> Option<u64> {
let s = std::fs::read_to_string("/proc/self/status").ok()?;
for line in s.lines() {
if let Some(rest) = line.strip_prefix("VmHWM:") {
return rest
.split_whitespace()
.next()
.and_then(|n| n.parse::<u64>().ok());
}
}
None
}