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 CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
- feat(config): `[memory] structured_summaries = false` config field enables opt-in structured compaction summaries (issue #1607)
- feat(tools): dynamic tool schema filtering — sends only relevant tool definitions to the LLM per turn, selected by embedding similarity between user query and tool descriptions; configurable via `[agent.tool_filter]` with `enabled`, `top_k`, `always_on`, and `min_description_words`; disabled by default (#2020)
- enh(tools): `/status` reports `tool_filter` state when enabled — shows Filter line with top_k, always_on count, and embedding count; silent when filter is disabled (#2028)
- enh(tui): compaction probe metrics visible in Memory panel — shows rate distribution (`P N% S N% H N% E N%`) and last verdict with color coding (Pass=green, SoftFail=yellow, HardFail=red, Error=gray); lines hidden until first probe runs; `ProbeVerdict::Error` variant added for transport/timeout failures that produce no quality score (#2049)
- enh(init): `--init` wizard exposes compaction probe configuration in the context-compression step; adds "Enable compaction probe?" confirm and, when enabled, prompts for model, pass threshold (0.0, 1.0], and hard-fail threshold [0.0, 1.0) with cross-field validation loop; `config/default.toml` gains a commented `[memory.compression.probe]` block so `--migrate-config` surfaces the section; `Config::validate()` enforces probe threshold invariants (`hard_fail_threshold < threshold`, range checks, `max_questions >= 1`, `timeout_secs >= 1`) when `probe.enabled = true` (#2048)
- feat(channels): register Discord slash commands (`/reset`, `/skills`, `/agent`) at startup via fire-and-forget background task; idempotent via `PUT /applications/{id}/commands` (CHAN-05, epic #1978)
- feat(channels): extract shared `CONFIRM_TIMEOUT` constant (30s) to `zeph-channels` crate; Telegram, Discord, and Slack `confirm()` all reference it (CHAN-02, epic #1978)
Expand Down
29 changes: 25 additions & 4 deletions crates/zeph-core/src/agent/context/summarization.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1253,7 +1253,11 @@ impl<C: Channel> Agent<C> {
Ok(result) => result,
Err(e) => {
tracing::warn!("compaction probe error (non-blocking): {e:#}");
self.update_metrics(|m| m.compaction_probe_errors += 1);
self.update_metrics(|m| {
m.compaction_probe_errors += 1;
m.last_probe_verdict = Some(zeph_memory::ProbeVerdict::Error);
m.last_probe_score = None;
});
None
}
};
Expand All @@ -1270,7 +1274,11 @@ impl<C: Channel> Agent<C> {
threshold = result.hard_fail_threshold,
"compaction probe HARD FAIL — keeping original messages"
);
self.update_metrics(|m| m.compaction_probe_failures += 1);
self.update_metrics(|m| {
m.compaction_probe_failures += 1;
m.last_probe_verdict = Some(zeph_memory::ProbeVerdict::HardFail);
m.last_probe_score = Some(result.score);
});
return Ok(CompactionOutcome::ProbeRejected);
}
zeph_memory::ProbeVerdict::SoftFail => {
Expand All @@ -1279,11 +1287,24 @@ impl<C: Channel> Agent<C> {
threshold = result.threshold,
"compaction probe SOFT FAIL — proceeding with warning"
);
self.update_metrics(|m| m.compaction_probe_soft_failures += 1);
self.update_metrics(|m| {
m.compaction_probe_soft_failures += 1;
m.last_probe_verdict = Some(zeph_memory::ProbeVerdict::SoftFail);
m.last_probe_score = Some(result.score);
});
}
zeph_memory::ProbeVerdict::Pass => {
tracing::info!(score = result.score, "compaction probe passed");
self.update_metrics(|m| m.compaction_probe_passes += 1);
self.update_metrics(|m| {
m.compaction_probe_passes += 1;
m.last_probe_verdict = Some(zeph_memory::ProbeVerdict::Pass);
m.last_probe_score = Some(result.score);
});
}
zeph_memory::ProbeVerdict::Error => {
// Unreachable: validate_compaction returns Err on errors, not Ok(Error).
// If this fires, the error-handling path in validate_compaction changed.
debug_assert!(false, "ProbeVerdict::Error reached inside Ok path");
}
}
}
Expand Down
7 changes: 7 additions & 0 deletions crates/zeph-core/src/metrics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ use std::collections::VecDeque;

use tokio::sync::watch;

pub use zeph_memory::ProbeVerdict;

/// Category of a security event for TUI display.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SecurityEventCategory {
Expand Down Expand Up @@ -197,6 +199,11 @@ pub struct MetricsSnapshot {
pub compaction_probe_failures: u64,
/// Compaction probe errors (LLM/timeout — non-blocking, compaction proceeded).
pub compaction_probe_errors: u64,
/// Last compaction probe verdict. `None` before the first probe completes.
pub last_probe_verdict: Option<zeph_memory::ProbeVerdict>,
/// Last compaction probe score in [0.0, 1.0]. `None` before the first probe
/// completes or after an Error verdict (errors produce no score).
pub last_probe_score: Option<f32>,
pub cache_read_tokens: u64,
pub cache_creation_tokens: u64,
pub cost_spent_cents: f64,
Expand Down
2 changes: 2 additions & 0 deletions crates/zeph-memory/src/compaction_probe.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ pub enum ProbeVerdict {
SoftFail,
/// Score < `hard_fail_threshold`: summary lost critical facts. Block compaction.
HardFail,
/// Transport/timeout failure — no quality score produced.
Error,
}

/// Full result of a compaction probe run.
Expand Down
4 changes: 2 additions & 2 deletions crates/zeph-tui/src/metrics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@
// SPDX-License-Identifier: MIT OR Apache-2.0

pub use zeph_core::metrics::{
MetricsCollector, MetricsSnapshot, SecurityEvent, SecurityEventCategory, SkillConfidence,
TaskGraphSnapshot, TaskSnapshotRow,
MetricsCollector, MetricsSnapshot, ProbeVerdict, SecurityEvent, SecurityEventCategory,
SkillConfidence, TaskGraphSnapshot, TaskSnapshotRow,
};
97 changes: 94 additions & 3 deletions crates/zeph-tui/src/widgets/memory.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@

use ratatui::Frame;
use ratatui::layout::Rect;
use ratatui::text::Line;
use ratatui::style::{Color, Style};
use ratatui::text::{Line, Span};
use ratatui::widgets::{Block, Borders, Paragraph};

use crate::metrics::MetricsSnapshot;
use crate::metrics::{MetricsSnapshot, ProbeVerdict};
use crate::theme::Theme;

pub fn render(metrics: &MetricsSnapshot, frame: &mut Frame, area: Rect) {
Expand Down Expand Up @@ -49,6 +50,41 @@ pub fn render(metrics: &MetricsSnapshot, frame: &mut Frame, area: Rect) {
metrics.graph_extraction_count, metrics.graph_extraction_failures,
)));
}
let total_probes = metrics.compaction_probe_passes
+ metrics.compaction_probe_soft_failures
+ metrics.compaction_probe_failures
+ metrics.compaction_probe_errors;

#[allow(
clippy::cast_precision_loss,
clippy::cast_possible_truncation,
clippy::cast_sign_loss
)]
if total_probes > 0 {
let pct = |n: u64| -> u64 { (n as f64 / total_probes as f64 * 100.0).round() as u64 };
let p = pct(metrics.compaction_probe_passes);
let s = pct(metrics.compaction_probe_soft_failures);
let h = pct(metrics.compaction_probe_failures);
let e = pct(metrics.compaction_probe_errors);
mem_lines.push(Line::from(format!(" Probe: P {p}% S {s}% H {h}% E {e}%")));

if let Some(verdict) = &metrics.last_probe_verdict {
let (label, color) = match verdict {
ProbeVerdict::Pass => ("Pass", Color::Green),
ProbeVerdict::SoftFail => ("SoftFail", Color::Yellow),
ProbeVerdict::HardFail => ("HardFail", Color::Red),
ProbeVerdict::Error => ("Error", Color::Gray),
};
let score_str = metrics
.last_probe_score
.map_or_else(String::new, |sc| format!(" ({sc:.2})"));
mem_lines.push(Line::from(vec![
Span::raw(" Last: "),
Span::styled(format!("{label}{score_str}"), Style::default().fg(color)),
]));
}
}

if metrics.guidelines_version > 0 {
mem_lines.push(Line::from(format!(
" Guidelines: v{} ({})",
Expand All @@ -68,7 +104,7 @@ pub fn render(metrics: &MetricsSnapshot, frame: &mut Frame, area: Rect) {
mod tests {
use insta::assert_snapshot;

use crate::metrics::MetricsSnapshot;
use crate::metrics::{MetricsSnapshot, ProbeVerdict};
use crate::test_utils::render_to_string;

#[test]
Expand Down Expand Up @@ -102,4 +138,59 @@ mod tests {
});
assert_snapshot!(output);
}

#[test]
fn probe_lines_visible_when_probes_ran() {
let metrics = MetricsSnapshot {
sqlite_message_count: 5,
compaction_probe_passes: 87,
compaction_probe_soft_failures: 10,
compaction_probe_failures: 2,
compaction_probe_errors: 1,
last_probe_verdict: Some(ProbeVerdict::Pass),
last_probe_score: Some(0.91),
..MetricsSnapshot::default()
};

let output = render_to_string(50, 12, |frame, area| {
super::render(&metrics, frame, area);
});
assert_snapshot!(output);
}

#[test]
fn probe_lines_hidden_when_no_probes() {
let metrics = MetricsSnapshot {
sqlite_message_count: 5,
compaction_probe_passes: 0,
compaction_probe_soft_failures: 0,
compaction_probe_failures: 0,
compaction_probe_errors: 0,
last_probe_verdict: None,
last_probe_score: None,
..MetricsSnapshot::default()
};

let output = render_to_string(50, 10, |frame, area| {
super::render(&metrics, frame, area);
});
assert_snapshot!(output);
}

#[test]
fn probe_error_verdict_shows_no_score() {
let metrics = MetricsSnapshot {
sqlite_message_count: 5,
compaction_probe_passes: 1,
compaction_probe_errors: 1,
last_probe_verdict: Some(ProbeVerdict::Error),
last_probe_score: None,
..MetricsSnapshot::default()
};

let output = render_to_string(50, 12, |frame, area| {
super::render(&metrics, frame, area);
});
assert_snapshot!(output);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
source: crates/zeph-tui/src/widgets/memory.rs
expression: output
---
┌ Memory ────────────────────────────────────────┐
│ SQLite: 5 msgs │
│ Conv ID: --- │
│ Embeddings: 0 │
│ Graph: 0 entities, 0 edges, 0 communities │
│ Graph extractions: 0 ok, 0 failed │
│ Probe: P 50% S 0% H 0% E 50% │
│ Last: Error │
│ │
│ │
│ │
└────────────────────────────────────────────────┘
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
source: crates/zeph-tui/src/widgets/memory.rs
expression: output
---
┌ Memory ────────────────────────────────────────┐
│ SQLite: 5 msgs │
│ Conv ID: --- │
│ Embeddings: 0 │
│ Graph: 0 entities, 0 edges, 0 communities │
│ Graph extractions: 0 ok, 0 failed │
│ │
│ │
│ │
└────────────────────────────────────────────────┘
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
source: crates/zeph-tui/src/widgets/memory.rs
expression: output
---
┌ Memory ────────────────────────────────────────┐
│ SQLite: 5 msgs │
│ Conv ID: --- │
│ Embeddings: 0 │
│ Graph: 0 entities, 0 edges, 0 communities │
│ Graph extractions: 0 ok, 0 failed │
│ Probe: P 87% S 10% H 2% E 1% │
│ Last: Pass (0.91) │
│ │
│ │
│ │
└────────────────────────────────────────────────┘
Loading