memoryd: recall's local items on one indented line, as Honcho's

Co-Authored-By: ornith-1.5-35b-a3b <agent@wntrmute.dev>
This commit is contained in:
2026-09-29 09:17:05 -07:00
co-authored by ornith-1.5-35b-a3b
parent d8c396e691
commit a73d49c609
5 changed files with 279 additions and 40 deletions
+8 -21
View File
@@ -4,12 +4,11 @@
//! Pure: no I/O.
use bxcore::DataClass;
use bxcore::sanitize::neutralise;
use roncho::{Conclusion, Message};
use crate::honcho::read::Label;
use crate::index::class_name;
use crate::recall::{SNIPPET, when};
use crate::recall::{item, snippet, when};
/// One Honcho hit as `recall` shows it.
#[derive(Debug, Clone, PartialEq, Eq)]
@@ -29,13 +28,13 @@ impl Item {
/// {class}`, then its text on one line indented by two spaces, so that no text can begin a
/// line the way a header does.
pub fn render(&self, n: usize) -> String {
format!(
"[{n}] {} · {} · {}\n {}",
let header = format!(
"[{n}] {} · {} · {}",
self.tag,
self.from,
class_name(self.class),
self.text
)
class_name(self.class)
);
item(&header, &self.text)
}
}
@@ -53,21 +52,9 @@ pub fn relevant(query: &[String], text: &str) -> bool {
words(text).iter().any(|w| query.contains(w))
}
/// Text from Honcho as an item holds it: neutralised, every control character (a line break
/// among them) and every Unicode line or paragraph separator a space, then cut to its first
/// `SNIPPET` characters, as a local snippet is cut.
/// Text from Honcho as an item holds it: as a local hit's text is (`recall::snippet`).
pub fn clean(text: &str) -> String {
neutralise(text)
.chars()
.map(|c| {
if c.is_control() || c == '\u{2028}' || c == '\u{2029}' {
' '
} else {
c
}
})
.take(SNIPPET)
.collect()
snippet(text)
}
/// `{name}: {peer} said, {when}` for a message.
+63 -17
View File
@@ -3,6 +3,7 @@
use serde::Serialize;
use bxcore::sanitize::neutralise;
use bxcore::{DataClass, RecallArgs, Timestamp};
use rusqlite::params;
@@ -20,6 +21,38 @@ pub struct Recalled {
pub untrusted: bool,
}
/// The local index's hits, each rendered as an item, numbered from 1, and the label they give
/// the answer: `Private` and trusted when there are none.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Local {
pub items: Vec<String>,
pub class: DataClass,
pub untrusted: bool,
}
/// A hit's text as an item holds it, from the local index or from Honcho: neutralised, every
/// control character (a line break among them) and every Unicode line or paragraph separator a
/// space, then cut to its first `SNIPPET` characters, with no mark.
pub fn snippet(text: &str) -> String {
neutralise(text)
.chars()
.map(|c| {
if c.is_control() || c == '\u{2028}' || c == '\u{2029}' {
' '
} else {
c
}
})
.take(SNIPPET)
.collect()
}
/// An item as the model reads it: its header, then its text on one line indented by two spaces,
/// so that no text can begin a line the way a header does (M5c spec, section 8; tip T37).
pub fn item(header: &str, text: &str) -> String {
format!("{header}\n {text}")
}
/// The FTS5 query for `query`: every run of letters and digits, double-quoted, joined by `OR`.
/// `None` when there is no such run.
pub fn terms(query: &str) -> Option<String> {
@@ -140,18 +173,39 @@ pub fn recall(
session: &str,
now: Timestamp,
) -> Result<Recalled, String> {
let found = local(index, store, args, session, now)?;
if found.items.is_empty() {
return Ok(Recalled {
text: format!("(no matches for \"{}\")", args.query),
class: DataClass::Private,
untrusted: false,
});
}
Ok(Recalled {
text: found.items.join("\n"),
class: found.class,
untrusted: found.untrusted,
})
}
/// Search the local index for `args` on behalf of `session`, log the recall, and give its hits
/// as items with their label. `recall` answers with them alone; with Honcho, its hits follow.
pub fn local(
index: &Index,
store: &Store,
args: &RecallArgs,
session: &str,
now: Timestamp,
) -> Result<Local, String> {
// 1. `args.check()?`.
// 2. hits: `terms(&args.query)` None -> none; Some(fts) -> `search(index, &fts, args.k)?`.
// 3. Log it: a Logged { time: now.to_rfc3339(), session, query, hits: a LoggedHit (kind,
// session, epoch, record) per hit } as one JSON line appended to
// format!("recalls/{month}.jsonl") (month: the first 7 characters of now.to_rfc3339()),
// after `create_dir_all(store.path("recalls"))`.
// 4. No hits: Ok(Recalled { text: format!("(no matches for \"{}\")", args.query), class
// Private, untrusted false }).
// 5. Otherwise, for each hit with i from 0: class = class.max(its class) starting at Private;
// untrusted |= its tag is "memory-tainted"; its text cut to its first SNIPPET characters;
// the item is format!("{}\n{text}", hit.header(i + 1)). Ok(Recalled { the items joined by
// newlines, class, untrusted }).
// 4. For each hit with i from 0: class = class.max(its class) starting at Private;
// untrusted |= its tag is "memory-tainted"; the item is `item(&hit.header(i + 1),
// &snippet(&hit.text))`. Ok(Local { items, class, untrusted }).
args.check()?;
let hits: Vec<Hit> = match terms(&args.query) {
None => Vec::new(),
@@ -176,24 +230,16 @@ pub fn recall(
};
let line = serde_json::to_string(&logged).map_err(|e| format!("recall: {e}"))?;
store.append(&format!("recalls/{month}.jsonl"), &format!("{line}\n"))?;
if hits.is_empty() {
return Ok(Recalled {
text: format!("(no matches for \"{}\")", args.query),
class: DataClass::Private,
untrusted: false,
});
}
let mut class = DataClass::Private;
let mut untrusted = false;
let mut items: Vec<String> = Vec::with_capacity(hits.len());
for (i, hit) in hits.iter().enumerate() {
class = class.max(hit.class);
untrusted |= hit.tag() == "memory-tainted";
let text: String = hit.text.chars().take(SNIPPET).collect();
items.push(format!("{}\n{text}", hit.header(i + 1)));
items.push(item(&hit.header(i + 1), &snippet(&hit.text)));
}
Ok(Recalled {
text: items.join("\n"),
Ok(Local {
items,
class,
untrusted,
})