1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
//! What the user gets told while a compaction walks the fallback chain.
//!
//! `complete_compaction_request` reported every step with `tracing` only:
//! a primary timing out after its full attempt budget, the walk moving to
//! the next provider, the chain running dry. After `/compact` the TUI showed
//! one "requested" line, then silence for as long as the chain took, then
//! an error (#1521). The chat path surfaces the same events as alerts.
//!
//! Manual compaction is the case where a person is watching, so it gets
//! every step. Automatic compaction stays quiet on the way and reports only
//! a failure, because a summariser that quietly succeeds is the point of
//! running it in the background.
use uuid::Uuid;
use super::types::{ProgressCallback, ProgressEvent};
/// One thing that happened inside the summariser walk.
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum CompactionStep {
/// A provider was asked and did not deliver: timeout, quota, refusal.
AttemptFailed { provider: String, reason: String },
/// The walk is about to ask this provider with this model.
TryingFallback { provider: String, model: String },
/// Nothing left to try, or nothing to walk to. `detail` is the ledger.
Failed { detail: String },
}
/// The line a step renders as. Pure, so the wording is testable on its own.
pub(crate) fn describe(step: &CompactionStep) -> String {
match step {
CompactionStep::AttemptFailed { provider, reason } => {
format!("Compaction: '{provider}' failed ({reason}), walking the fallback chain")
}
CompactionStep::TryingFallback { provider, model } => {
format!("Compaction: trying '{provider}' with model '{model}'")
}
CompactionStep::Failed { detail } => format!("Compaction failed: {detail}"),
}
}
/// Routes compaction steps to a session's progress callback.
#[derive(Clone)]
pub(crate) struct CompactionNotifier {
session_id: Uuid,
callback: ProgressCallback,
/// Report every step (manual compaction) or only a failure (automatic).
verbose: bool,
}
impl CompactionNotifier {
/// A person asked for this compaction and is waiting on it.
pub(crate) fn manual(session_id: Uuid, callback: ProgressCallback) -> Self {
Self {
session_id,
callback,
verbose: true,
}
}
/// The budget asked for this compaction; only a failure is news.
pub(crate) fn auto(session_id: Uuid, callback: ProgressCallback) -> Self {
Self {
session_id,
callback,
verbose: false,
}
}
/// Build from an optional callback, so call sites stay one line.
pub(crate) fn manual_from(
session_id: Uuid,
callback: Option<&ProgressCallback>,
) -> Option<Self> {
callback.map(|cb| Self::manual(session_id, cb.clone()))
}
pub(crate) fn auto_from(session_id: Uuid, callback: Option<&ProgressCallback>) -> Option<Self> {
callback.map(|cb| Self::auto(session_id, cb.clone()))
}
/// Whether this step reaches the user under this notifier's policy.
pub(crate) fn reports(&self, step: &CompactionStep) -> bool {
self.verbose || matches!(step, CompactionStep::Failed { .. })
}
pub(crate) fn step(&self, step: CompactionStep) {
if !self.reports(&step) {
return;
}
(self.callback)(
self.session_id,
ProgressEvent::SelfHealingAlert {
message: describe(&step),
},
);
}
}