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
//! Surface relevant memory without the model having to ask (#799).
//!
//! MEMORY.md was written constantly and read almost never. #800 made reading
//! cheap, but a cheap read still has to be chosen, and the case that hurts most
//! is the one where the model does not know there is anything to look up. It
//! cannot decide to recall a correction it has forgotten exists.
//!
//! So recall runs on the ENTRY path instead: the user's message is matched
//! against MEMORY.md at turn start and anything relevant rides along with it.
//! This mirrors `hints.rs`, which already proved the shape works, except that
//! fires on the error path after a tool call has already missed.
//!
//! Deliberately conservative. This is paid on every single turn and competes
//! with the actual task for attention, so it stays silent unless the match is
//! good, and it never grows past a couple of short sections.
use cratebrain_sections;
/// At most this many sections ride along with a user message.
const RECALL_MAX_SECTIONS: usize = 2;
/// Total character budget for injected recall.
const RECALL_MAX_CHARS: usize = 1200;
/// A section must match at least this many DISTINCT terms from the message.
///
/// One shared word is noise: on a long message almost every section would
/// match something. Two terms co-occurring is a signal. This is the main guard
/// against recall becoming per-turn bloat, which is the failure mode that would
/// make it worse than the problem it solves.
const RECALL_MIN_HITS: usize = 2;
/// Recall relevant to `user_message`, formatted for context, or `None`.
///
/// Pure: takes the file content, so the decision is testable without a home
/// directory or a populated MEMORY.md.
/// Read MEMORY.md and recall against it. `None` when the file is absent,
/// unreadable, or nothing matched well enough.