Skip to content

Commit 160dcd8

Browse files
committed
fix: include document content in situation report, not just titles
The local model needs actual content to evaluate HEARTBEAT.md tasks meaningfully. Previously it only saw titles like "Deadline reminder" with no way to know if it's urgent. Now recalls content per namespace (up to 500 chars each, max 10 namespaces) via client.recall_namespace(). The model sees actual email text and page content alongside the task checklist.
1 parent 8287d3a commit 160dcd8

1 file changed

Lines changed: 56 additions & 10 deletions

File tree

src/openhuman/subconscious/situation_report.rs

Lines changed: 56 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,11 @@ async fn build_tasks_section(workspace_dir: &Path) -> String {
9797
section
9898
}
9999

100+
/// Max chars of content to include per document in the report.
101+
const DOC_CONTENT_SNIPPET_CHARS: usize = 500;
102+
/// Max number of new docs to include full content for.
103+
const MAX_DOCS_WITH_CONTENT: usize = 10;
104+
100105
async fn build_memory_docs_section(client: &MemoryClient, last_tick_at: f64) -> String {
101106
let docs = match client.list_documents(None).await {
102107
Ok(raw) => raw,
@@ -105,7 +110,6 @@ async fn build_memory_docs_section(client: &MemoryClient, last_tick_at: f64) ->
105110
}
106111
};
107112

108-
// Parse the raw serde_json::Value into document summaries
109113
let doc_array = docs
110114
.as_array()
111115
.or_else(|| docs.get("documents").and_then(|v| v.as_array()));
@@ -114,9 +118,10 @@ async fn build_memory_docs_section(client: &MemoryClient, last_tick_at: f64) ->
114118
return "## Memory Documents\n\nNo documents found.\n".to_string();
115119
};
116120

117-
// Filter to docs updated since last tick (or all on cold start)
118121
let is_cold_start = last_tick_at <= 0.0;
119-
let mut new_docs: Vec<(&str, &str, f64)> = Vec::new();
122+
123+
// Collect new/updated doc metadata
124+
let mut new_doc_keys: Vec<(String, String, String)> = Vec::new(); // (namespace, key, title)
120125
for doc in doc_array {
121126
let updated_at = doc
122127
.get("updated_at")
@@ -132,13 +137,23 @@ async fn build_memory_docs_section(client: &MemoryClient, last_tick_at: f64) ->
132137
.get("title")
133138
.or_else(|| doc.get("key"))
134139
.and_then(|v| v.as_str())
135-
.unwrap_or("untitled");
136-
let namespace = doc.get("namespace").and_then(|v| v.as_str()).unwrap_or("?");
140+
.unwrap_or("untitled")
141+
.to_string();
142+
let namespace = doc
143+
.get("namespace")
144+
.and_then(|v| v.as_str())
145+
.unwrap_or("?")
146+
.to_string();
147+
let key = doc
148+
.get("key")
149+
.and_then(|v| v.as_str())
150+
.unwrap_or("")
151+
.to_string();
137152

138-
new_docs.push((namespace, title, updated_at));
153+
new_doc_keys.push((namespace, key, title));
139154
}
140155

141-
if new_docs.is_empty() {
156+
if new_doc_keys.is_empty() {
142157
return format!(
143158
"## Memory Documents\n\n{} total documents. No changes since last tick.\n",
144159
doc_array.len()
@@ -148,14 +163,45 @@ async fn build_memory_docs_section(client: &MemoryClient, last_tick_at: f64) ->
148163
let mut section = format!(
149164
"## Memory Documents\n\n{} total, {} new/updated since last tick:\n\n",
150165
doc_array.len(),
151-
new_docs.len()
166+
new_doc_keys.len()
152167
);
153-
for (namespace, title, _) in &new_docs {
154-
let _ = writeln!(section, "- [{namespace}] {title}");
168+
169+
// Recall content per namespace for new docs
170+
let mut recalled_namespaces = std::collections::HashSet::new();
171+
let mut docs_with_content = 0;
172+
173+
for (namespace, _key, title) in &new_doc_keys {
174+
let _ = writeln!(section, "### [{namespace}] {title}\n");
175+
176+
// Only recall each namespace once (may have multiple docs per namespace)
177+
if docs_with_content < MAX_DOCS_WITH_CONTENT
178+
&& recalled_namespaces.insert(namespace.clone())
179+
{
180+
if let Ok(Some(context)) = client.recall_namespace(namespace, 3).await {
181+
let snippet = truncate_at_char_boundary(&context, DOC_CONTENT_SNIPPET_CHARS);
182+
if !snippet.trim().is_empty() {
183+
let _ = writeln!(section, "{snippet}\n");
184+
}
185+
}
186+
docs_with_content += 1;
187+
}
155188
}
156189
section
157190
}
158191

192+
fn truncate_at_char_boundary(text: &str, max_chars: usize) -> String {
193+
if text.len() <= max_chars {
194+
return text.to_string();
195+
}
196+
let truncate_at = text
197+
.char_indices()
198+
.take_while(|(i, _)| *i < max_chars)
199+
.last()
200+
.map(|(i, ch)| i + ch.len_utf8())
201+
.unwrap_or(0);
202+
format!("{}...", &text[..truncate_at])
203+
}
204+
159205
async fn build_graph_section(client: &MemoryClient, last_tick_at: f64) -> String {
160206
let relations = match client.graph_query(None, None, None).await {
161207
Ok(rows) => rows,

0 commit comments

Comments
 (0)