Skip to main content

oxicode_agent/advisor/
emission_guard.rs

1//! Per-session policy gate for advisor `advise()` calls — ported from omp
2//! `emission-guard.ts`.
3//!
4//! The advisor system prompt tells the watcher model "at most one `advise`
5//! per update" and "NEVER repeat advice you already gave." Real advisor models
6//! violate this. omp issue #3520 captured a session that recorded 309 `advise`
7//! calls covering 92 unique notes — 114× "Stop.", 52× "No issue; continue." —
8//! flooding the primary transcript with `<advisory severity="blocker">Stop.
9//! </advisory>` after the task was already complete.
10//!
11//! The fix is to make the rules load-bearing in code, not prose: silently drop
12//! duplicates, content-free self-talk, and over-budget calls at the `advise`
13//! boundary so the primary stays clean even when the advisor misbehaves. The
14//! gate is invisible to the advisor model — `AdviseTool` still returns
15//! `Recorded.` for a suppressed call.
16//!
17//! # Attribution
18//!
19//! Translated to Rust from omp (oh-my-pi), MIT licensed.
20
21use std::collections::{HashSet, VecDeque};
22
23use parking_lot::Mutex;
24
25/// Case-insensitive, punctuation-folded normalization. Lowercases, applies
26/// NFKC, collapses every run of non-letter / non-digit characters into a single
27/// space, and trims — so `"Stop."`, `"*Stop*"`, and `"  stop  "` all key to
28/// `stop`, while `"No issue; continue."` keys to `no issue continue`.
29/// omp `normalizeAdvisorNote`.
30#[must_use]
31pub fn normalize_advisor_note(note: &str) -> String {
32    use unicode_normalization::UnicodeNormalization;
33
34    let mut out = String::with_capacity(note.len());
35    let mut prev_space = true; // suppress a leading space
36    for c in note.to_lowercase().nfkc() {
37        if c.is_alphanumeric() {
38            out.push(c);
39            prev_space = false;
40        } else if !prev_space {
41            out.push(' ');
42            prev_space = true;
43        }
44    }
45    out.trim().to_string()
46}
47
48/// Normalized phrases the advisor occasionally emits that carry no concrete
49/// actionable content. Each must be the output of [`normalize_advisor_note`]
50/// so a single membership check covers every punctuation/casing variant
51/// (`"Stop."`, `"stop"`, `"STOP!"`). Ported verbatim from omp
52/// `SUPPRESSED_NORMALIZED_PHRASES` — do not edit casually; it is the curated
53/// set observed driving primary-transcript pollution (#3520). A genuine
54/// `blocker` like `"Stop: 'await' missing on writeStream.end()..."` does not
55/// match.
56const SUPPRESSED_NORMALIZED_PHRASES: &[&str] = &[
57    // Self-stop noise — telling the agent to "stop" without a reason is useless.
58    "stop",
59    "stop here",
60    "stop now",
61    "halt",
62    "abort",
63    // Completion self-talk — the agent already finished the task.
64    "done",
65    "task done",
66    "task complete",
67    "complete",
68    "finished",
69    "ok",
70    "okay",
71    "ok done",
72    // "Nothing to flag" — silence is the correct expression of "no concerns".
73    "no issue",
74    "no issues",
75    "no issue continue",
76    "no concerns",
77    "no concern",
78    "nothing to add",
79    "nothing to flag",
80    "nothing to report",
81    "no notes",
82    "no further input",
83    "no further input needed",
84    "no further input required",
85    "no further watcher input",
86    "no further watcher input needed",
87    "no further advice",
88    "no further advice needed",
89    // Endorsements — equivalent to silence.
90    "lgtm",
91    "looks good",
92    "all good",
93    "agent is on track",
94    "agent on track",
95    "on track",
96    "continue",
97    "carry on",
98];
99
100/// Bounds the dedupe history. omp `DEFAULT_HISTORY_CAPACITY`.
101const DEFAULT_HISTORY_CAPACITY: usize = 4096;
102
103#[derive(Default)]
104struct State {
105    /// Normalized notes already delivered, for dedupe.
106    seen: HashSet<String>,
107    /// Insertion-order log to drive FIFO eviction without an extra Map.
108    seen_order: VecDeque<String>,
109    /// Per-update budget: at most one accepted `advise` per update.
110    consumed_this_update: bool,
111}
112
113/// Decides whether an advisor `advise()` call should reach the primary agent.
114/// omp `AdvisorEmissionGuard`.
115///
116/// Thread-safe (`&self` + internal lock) because `accept` is driven from the
117/// advisor agent's tool execution while `begin_update`/`reset` are driven from
118/// the host's turn boundaries — they can overlap.
119pub struct AdvisorEmissionGuard {
120    state: Mutex<State>,
121    capacity: usize,
122}
123
124impl AdvisorEmissionGuard {
125    /// Construct with the default dedupe history capacity (4096).
126    #[must_use]
127    pub fn new() -> Self {
128        Self::with_capacity(DEFAULT_HISTORY_CAPACITY)
129    }
130
131    /// Construct with an explicit dedupe history capacity.
132    #[must_use]
133    pub fn with_capacity(capacity: usize) -> Self {
134        Self {
135            state: Mutex::new(State::default()),
136            capacity,
137        }
138    }
139
140    /// Drop all dedupe and per-update state. Called whenever the advisor
141    /// runtime is reset (compaction, session switch, `/new`) so a re-primed
142    /// advisor can re-raise old issues — the primary transcript was rewritten.
143    /// omp `reset()`.
144    pub fn reset(&self) {
145        let mut s = self.state.lock();
146        s.seen.clear();
147        s.seen_order.clear();
148        s.consumed_this_update = false;
149    }
150
151    /// Clear the per-update rate-limit gate. Called right before each advisor
152    /// `prompt(batch)` cycle so the next advisor model cycle starts with a
153    /// fresh budget of one advise. omp `beginUpdate()`.
154    pub fn begin_update(&self) {
155        self.state.lock().consumed_this_update = false;
156    }
157
158    /// Whether the proposed note should reach the primary. On `true` the gate
159    /// has already recorded the note (consumed the per-update budget and added
160    /// it to the dedupe history) — the caller delivers the note. On `false` the
161    /// caller drops it. omp `accept()`.
162    ///
163    /// Empty/whitespace-only notes are suppressed (defense-in-depth; the
164    /// tool-args contract requires a non-empty string). Content-free filler
165    /// (per `SUPPRESSED_NORMALIZED_PHRASES`) and exact normalized duplicates
166    /// are suppressed. Over-budget calls (a second accept in the same update)
167    /// are suppressed.
168    pub fn accept(&self, note: &str) -> bool {
169        let key = normalize_advisor_note(note);
170        if key.is_empty() {
171            return false;
172        }
173        if SUPPRESSED_NORMALIZED_PHRASES.contains(&key.as_str()) {
174            return false;
175        }
176        let mut s = self.state.lock();
177        if s.seen.contains(&key) {
178            return false;
179        }
180        if s.consumed_this_update {
181            return false;
182        }
183        s.consumed_this_update = true;
184        s.seen.insert(key.clone());
185        s.seen_order.push_back(key);
186        while s.seen_order.len() > self.capacity {
187            if let Some(stale) = s.seen_order.pop_front() {
188                s.seen.remove(&stale);
189            }
190        }
191        true
192    }
193}
194
195impl Default for AdvisorEmissionGuard {
196    fn default() -> Self {
197        Self::new()
198    }
199}
200
201#[cfg(test)]
202mod tests {
203    #![allow(clippy::unwrap_used)]
204    use super::*;
205
206    fn guard() -> AdvisorEmissionGuard {
207        AdvisorEmissionGuard::with_capacity(4)
208    }
209
210    #[test]
211    fn normalize_folds_punctuation_and_case() {
212        assert_eq!(normalize_advisor_note("Stop."), "stop");
213        assert_eq!(normalize_advisor_note("*Stop*"), "stop");
214        assert_eq!(normalize_advisor_note("  stop  "), "stop");
215        assert_eq!(
216            normalize_advisor_note("No issue; continue."),
217            "no issue continue"
218        );
219        assert_eq!(normalize_advisor_note(""), "");
220        assert_eq!(normalize_advisor_note("   "), "");
221    }
222
223    #[test]
224    fn accept_suppresses_content_free_phrases() {
225        let g = guard();
226        for phrase in [
227            "Stop.",
228            "STOP!",
229            "done",
230            "No issues.",
231            "looks good",
232            "continue",
233        ] {
234            assert!(!g.accept(phrase), "should suppress {phrase:?}");
235        }
236    }
237
238    #[test]
239    fn accept_suppresses_empty() {
240        let g = guard();
241        assert!(!g.accept(""));
242        assert!(!g.accept("   "));
243    }
244
245    #[test]
246    fn accept_one_per_update_then_blocks_second() {
247        let g = guard();
248        assert!(g.accept("Use saturating_add to avoid overflow on the counter"));
249        // same update -> blocked even if novel
250        assert!(!g.accept("Different, valid note about naming"));
251        // new update -> fresh budget, but the first note is now deduped
252        g.begin_update();
253        assert!(!g.accept("Use saturating_add to avoid overflow on the counter"));
254        assert!(g.accept("Different, valid note about naming"));
255    }
256
257    #[test]
258    fn accept_dedupes_normalized_variants_across_updates() {
259        let g = guard();
260        assert!(g.accept("Use saturating_add."));
261        g.begin_update();
262        // punctuation/case variant normalizes to the same key -> deduped
263        assert!(!g.accept("use saturating add!"));
264    }
265
266    #[test]
267    fn fifo_evicts_at_capacity_then_readmits() {
268        let g = guard();
269        // capacity 4
270        for i in 0..4 {
271            assert!(g.accept(&format!("note number {i}")));
272            g.begin_update();
273        }
274        // 5th evicts the oldest ("note number 0")
275        assert!(g.accept("note number 4"));
276        g.begin_update();
277        // evicted note is readmitted
278        assert!(g.accept("note number 0"));
279    }
280
281    #[test]
282    fn reset_clears_everything() {
283        let g = guard();
284        g.accept("some real advice");
285        g.reset();
286        // same note readmitted after reset
287        assert!(g.accept("some real advice"));
288    }
289
290    #[test]
291    fn genuine_blocker_is_not_suppressed() {
292        let g = guard();
293        assert!(g.accept("Stop: 'await' missing on writeStream.end() will lose buffered writes."));
294    }
295}