omgbase 0.3.0

omgbase: a versioned, addressable graph layer over authored Markdown — the `omgbase` binary (`omgbase mcp` serves the tool catalog over MCP stdio, with the filesystem watcher and the background embed drain)
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
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
//! The background embed drainer (`spec/search` §2.6; the reference's
//! `EmbedDrainer`). Mutations only ever *queue* embeddable blocks; a
//! long-lived host closes the gap by scheduling a drain after every write
//! and every watcher checkpoint. `schedule()` is a channel send — cheap,
//! never blocking the tool's response path. One thread owns the work: it
//! debounces a burst of schedules into one drain (500 ms), is single-flight
//! by construction, re-runs once more when a schedule arrived mid-drain (so
//! the last edit is never left unembedded), and swallows drain errors onto
//! stderr (a provider hiccup must not take the host down; the next schedule
//! retries). `flush` runs a final drain synchronously (shutdown).
//!
//! The thread builds its own store connection and its own provider
//! instance: the main thread's provider serves `semantic()` queries and stays
//! single-owner, and neither a `Store` nor a boxed provider is `Send`.

use std::sync::mpsc::{Receiver, RecvTimeoutError, Sender, TryRecvError, channel};
use std::thread::JoinHandle;
use std::time::{Duration, Instant};

/// The debounce the reference uses.
pub const DEBOUNCE: Duration = Duration::from_millis(500);

/// What one drain did (the reference's `onDrain` payload).
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct DrainReport {
    /// Block vectors plus whole-document vectors computed.
    pub embedded: usize,
    /// Document vectors pooled from cached block vectors (no provider call).
    pub pooled: usize,
}

impl DrainReport {
    /// Whether the drain produced anything worth logging.
    #[must_use]
    pub fn did_work(&self) -> bool {
        self.embedded > 0 || self.pooled > 0
    }
}

/// The messages a drainer thread receives.
pub enum Msg {
    /// Mark dirty and (re)arm the debounce.
    Schedule,
    /// Drain now (dirty or not — the reference's `flush` always marks
    /// dirty), then acknowledge.
    Flush(Sender<()>),
    /// Stop after the current drain, if any.
    Close,
}

/// A cheap, cloneable scheduling handle.
#[derive(Clone)]
pub struct DrainHandle(Sender<Msg>);

impl DrainHandle {
    /// Mark the repo dirty; the thread drains after the debounce. Never
    /// blocks, never fails (a closed drainer ignores it).
    pub fn schedule(&self) {
        let _ = self.0.send(Msg::Schedule);
    }
}

/// One drain: `Ok(report)`, or the error to log.
pub type DrainFn = Box<dyn FnMut() -> Result<DrainReport, String>>;

/// The thread and its channel.
pub struct Drainer {
    tx: Sender<Msg>,
    thread: Option<JoinHandle<()>>,
}

impl Drainer {
    /// Start the thread. `init` runs *on the thread* and builds the drain
    /// closure (opening the store connection and the provider there); when
    /// it fails the thread logs once and idles, acknowledging flushes, so a
    /// broken embedder is never fatal.
    pub fn spawn<I>(debounce: Duration, init: I) -> Self
    where
        I: FnOnce() -> Result<DrainFn, String> + Send + 'static,
    {
        let (tx, rx) = channel();
        let thread = std::thread::Builder::new()
            .name("omgbase-embed-drain".to_owned())
            .spawn(move || {
                let mut log = |line: String| eprintln!("{line}");
                match init() {
                    Ok(mut drain) => run_loop(&rx, debounce, &mut *drain, &mut log),
                    Err(e) => {
                        log(format!("[mcp] embed drain disabled: {e}"));
                        idle(&rx);
                    }
                }
            })
            .expect("spawn the embed drain thread");
        Self {
            tx,
            thread: Some(thread),
        }
    }

    #[must_use]
    pub fn handle(&self) -> DrainHandle {
        DrainHandle(self.tx.clone())
    }

    /// Drain now and wait for it (and for any re-run a mid-drain schedule
    /// triggered) to finish.
    pub fn flush(&self) {
        let (ack_tx, ack_rx) = channel();
        if self.tx.send(Msg::Flush(ack_tx)).is_ok() {
            let _ = ack_rx.recv();
        }
    }

    /// Stop the thread (no drains start after this) and join it.
    pub fn close(mut self) {
        let _ = self.tx.send(Msg::Close);
        if let Some(t) = self.thread.take() {
            let _ = t.join();
        }
    }
}

impl Drop for Drainer {
    fn drop(&mut self) {
        let _ = self.tx.send(Msg::Close);
        if let Some(t) = self.thread.take() {
            let _ = t.join();
        }
    }
}

/// A disabled drainer's thread: acknowledge flushes until closed.
fn idle(rx: &Receiver<Msg>) {
    while let Ok(msg) = rx.recv() {
        match msg {
            Msg::Schedule => {}
            Msg::Flush(ack) => {
                let _ = ack.send(());
            }
            Msg::Close => return,
        }
    }
}

/// The debounce / single-flight loop, separated from the thread for tests:
/// runs until `Close` or the channel closes.
pub fn run_loop(
    rx: &Receiver<Msg>,
    debounce: Duration,
    drain: &mut dyn FnMut() -> Result<DrainReport, String>,
    log: &mut dyn FnMut(String),
) {
    let mut dirty = false;
    let mut deadline: Option<Instant> = None;
    loop {
        let msg = match deadline {
            Some(dl) => match rx.recv_timeout(dl.saturating_duration_since(Instant::now())) {
                Ok(m) => Some(m),
                Err(RecvTimeoutError::Timeout) => None,
                Err(RecvTimeoutError::Disconnected) => return,
            },
            None => match rx.recv() {
                Ok(m) => Some(m),
                Err(_) => return,
            },
        };
        match msg {
            Some(Msg::Schedule) => {
                dirty = true;
                deadline = Some(Instant::now() + debounce);
            }
            Some(Msg::Flush(ack)) => {
                deadline = None;
                dirty = true;
                let closed = run_until_clean(rx, &mut dirty, drain, log, vec![ack]);
                if closed {
                    return;
                }
            }
            Some(Msg::Close) => return,
            None => {
                // The debounce elapsed.
                deadline = None;
                if run_until_clean(rx, &mut dirty, drain, log, Vec::new()) {
                    return;
                }
            }
        }
    }
}

/// Drain while dirty: each pass clears the flag, drains, then absorbs the
/// messages that arrived meanwhile (a `Schedule` re-dirties; a `Flush` is
/// acknowledged once the loop settles; a `Close` ends the thread after
/// this pass). On a drain error the pass stops — the dirt that arrived
/// during it stays queued for the next debounce (the reference swallows
/// and waits for the next `schedule()`). Returns whether `Close` arrived.
fn run_until_clean(
    rx: &Receiver<Msg>,
    dirty: &mut bool,
    drain: &mut dyn FnMut() -> Result<DrainReport, String>,
    log: &mut dyn FnMut(String),
    mut acks: Vec<Sender<()>>,
) -> bool {
    let mut closed = false;
    while *dirty && !closed {
        *dirty = false;
        match drain() {
            Ok(report) => {
                if report.did_work() {
                    log(format!("[mcp] embedded {} block(s)", report.embedded));
                }
            }
            Err(e) => {
                log(format!("[mcp] embed drain failed: {e}"));
                break;
            }
        }
        loop {
            match rx.try_recv() {
                Ok(Msg::Schedule) => *dirty = true,
                Ok(Msg::Flush(ack)) => {
                    *dirty = true;
                    acks.push(ack);
                }
                Ok(Msg::Close) => closed = true,
                Err(TryRecvError::Empty | TryRecvError::Disconnected) => break,
            }
        }
    }
    for ack in acks {
        let _ = ack.send(());
    }
    closed
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::sync::atomic::{AtomicUsize, Ordering};
    use std::sync::{Arc, Mutex};

    const FAST: Duration = Duration::from_millis(40);

    struct Harness {
        tx: Sender<Msg>,
        calls: Arc<AtomicUsize>,
        log: Arc<Mutex<Vec<String>>>,
        thread: Option<JoinHandle<()>>,
    }

    impl Harness {
        /// A loop over a fake drain: `on_call(n)` decides the nth call's
        /// outcome and may schedule mid-drain through the cloned sender.
        fn start<F>(on_call: F) -> Self
        where
            F: Fn(usize, &Sender<Msg>) -> Result<DrainReport, String> + Send + 'static,
        {
            let (tx, rx) = channel();
            let calls = Arc::new(AtomicUsize::new(0));
            let log = Arc::new(Mutex::new(Vec::new()));
            let (c, l, t) = (Arc::clone(&calls), Arc::clone(&log), tx.clone());
            let thread = std::thread::spawn(move || {
                let mut drain = move || {
                    let n = c.fetch_add(1, Ordering::SeqCst);
                    // Simulate provider latency so a mid-drain schedule can land.
                    std::thread::sleep(Duration::from_millis(15));
                    on_call(n, &t)
                };
                let mut log = move |line: String| l.lock().unwrap().push(line);
                run_loop(&rx, FAST, &mut drain, &mut log);
            });
            Self {
                tx,
                calls,
                log,
                thread: Some(thread),
            }
        }

        fn schedule(&self) {
            self.tx.send(Msg::Schedule).unwrap();
        }

        fn flush(&self) {
            let (ack, rx) = channel();
            self.tx.send(Msg::Flush(ack)).unwrap();
            rx.recv().unwrap();
        }

        fn calls(&self) -> usize {
            self.calls.load(Ordering::SeqCst)
        }

        fn close(mut self) -> Vec<String> {
            self.tx.send(Msg::Close).unwrap();
            self.thread.take().unwrap().join().unwrap();
            self.log.lock().unwrap().clone()
        }
    }

    fn ok(embedded: usize) -> Result<DrainReport, String> {
        Ok(DrainReport {
            embedded,
            pooled: 0,
        })
    }

    #[test]
    fn a_burst_of_schedules_is_one_debounced_drain() {
        let h = Harness::start(|_, _| ok(3));
        for _ in 0..5 {
            h.schedule();
            std::thread::sleep(Duration::from_millis(5));
        }
        assert_eq!(h.calls(), 0, "nothing runs before the debounce elapses");
        std::thread::sleep(FAST * 3);
        assert_eq!(h.calls(), 1);
        let log = h.close();
        assert_eq!(log, ["[mcp] embedded 3 block(s)"]);
    }

    #[test]
    fn a_schedule_during_a_drain_reruns_once_without_debounce() {
        let h = Harness::start(|n, tx| {
            if n == 0 {
                // A mutation lands while the first drain is in flight.
                tx.send(Msg::Schedule).unwrap();
            }
            ok(if n == 0 { 1 } else { 0 })
        });
        h.schedule();
        std::thread::sleep(FAST + Duration::from_millis(60));
        assert_eq!(h.calls(), 2, "one re-run, then clean");
        std::thread::sleep(FAST * 2);
        assert_eq!(h.calls(), 2, "no third pass without a new schedule");
        let log = h.close();
        assert_eq!(
            log,
            ["[mcp] embedded 1 block(s)"],
            "a clean pass logs nothing"
        );
    }

    #[test]
    fn flush_drains_now_and_waits() {
        let h = Harness::start(|_, _| ok(0));
        h.flush();
        assert_eq!(h.calls(), 1, "flush always drains, dirty or not");
        h.schedule();
        h.flush();
        assert_eq!(
            h.calls(),
            2,
            "the pending debounce collapses into the flush"
        );
        std::thread::sleep(FAST * 2);
        assert_eq!(h.calls(), 2);
        assert!(h.close().is_empty());
    }

    #[test]
    fn a_failing_drain_is_logged_and_retried_on_the_next_schedule() {
        let h = Harness::start(|n, _| {
            if n == 0 {
                Err("provider down".into())
            } else {
                ok(2)
            }
        });
        h.schedule();
        std::thread::sleep(FAST * 3);
        assert_eq!(h.calls(), 1);
        h.schedule();
        std::thread::sleep(FAST * 3);
        assert_eq!(h.calls(), 2);
        let log = h.close();
        assert_eq!(
            log,
            [
                "[mcp] embed drain failed: provider down",
                "[mcp] embedded 2 block(s)"
            ]
        );
    }

    #[test]
    fn close_stops_before_a_pending_debounce_fires() {
        let h = Harness::start(|_, _| ok(1));
        h.schedule();
        let log = h.close();
        assert!(log.is_empty());
    }

    #[test]
    fn drainer_spawn_runs_init_on_the_thread_and_disables_on_failure() {
        let calls = Arc::new(AtomicUsize::new(0));
        let c = Arc::clone(&calls);
        let d = Drainer::spawn(FAST, move || {
            let c = Arc::clone(&c);
            Ok(Box::new(move || {
                c.fetch_add(1, Ordering::SeqCst);
                ok(0)
            }) as DrainFn)
        });
        let handle = d.handle();
        handle.schedule();
        d.flush();
        assert_eq!(calls.load(Ordering::SeqCst), 1);
        d.close();
        handle.schedule(); // ignored: the thread is gone

        let broken = Drainer::spawn(FAST, || Err("no embedder".to_owned()));
        broken.handle().schedule();
        broken.flush(); // acknowledged even though nothing drains
        broken.close();
    }
}