experience-mine: link at creation time, remove # from new keys
Update the experience mining prompt to output links alongside journal entries. The LLM now returns a "links" array per entry pointing to existing semantic nodes. Rust code creates the links immediately after node creation — new nodes arrive pre-connected instead of orphaned. Also: remove # from all key generation paths (experience miner, digest section keys, observed transcript keys). New nodes get clean dash-separated keys. Co-Authored-By: Kent Overstreet <kent.overstreet@linux.dev>
This commit is contained in:
parent
ce94e1cac1
commit
83342897c8
4 changed files with 37 additions and 9 deletions
|
|
@ -365,7 +365,7 @@ fn normalize_link_key(raw: &str) -> String {
|
||||||
} else if key.contains('#') {
|
} else if key.contains('#') {
|
||||||
let (file, section) = key.split_once('#').unwrap();
|
let (file, section) = key.split_once('#').unwrap();
|
||||||
if let Some(bare) = file.strip_suffix(".md") {
|
if let Some(bare) = file.strip_suffix(".md") {
|
||||||
key = format!("{}#{}", bare, section);
|
key = format!("{}-{}", bare, section);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ fn transcript_dedup_key(path: &str) -> Result<String, String> {
|
||||||
let bytes = fs::read(path).map_err(|e| format!("read {}: {}", path, e))?;
|
let bytes = fs::read(path).map_err(|e| format!("read {}: {}", path, e))?;
|
||||||
let mut hasher = DefaultHasher::new();
|
let mut hasher = DefaultHasher::new();
|
||||||
bytes.hash(&mut hasher);
|
bytes.hash(&mut hasher);
|
||||||
Ok(format!("_mined-transcripts#h-{:016x}", hasher.finish()))
|
Ok(format!("_mined-transcripts-h-{:016x}", hasher.finish()))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if a transcript has already been mined (dedup key exists in store).
|
/// Check if a transcript has already been mined (dedup key exists in store).
|
||||||
|
|
@ -111,7 +111,7 @@ pub fn mark_segment(
|
||||||
/// Get the set of all mined transcript keys (both content-hash and filename)
|
/// Get the set of all mined transcript keys (both content-hash and filename)
|
||||||
/// from the store. Load once per daemon tick, check many.
|
/// from the store. Load once per daemon tick, check many.
|
||||||
pub fn mined_transcript_keys() -> HashSet<String> {
|
pub fn mined_transcript_keys() -> HashSet<String> {
|
||||||
keys_with_prefix("_mined-transcripts#")
|
keys_with_prefix("_mined-transcripts-")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -286,7 +286,7 @@ pub fn experience_mine(
|
||||||
let mut hasher = DefaultHasher::new();
|
let mut hasher = DefaultHasher::new();
|
||||||
transcript_bytes.hash(&mut hasher);
|
transcript_bytes.hash(&mut hasher);
|
||||||
let hash = hasher.finish();
|
let hash = hasher.finish();
|
||||||
let dedup_key = format!("_mined-transcripts#h-{:016x}", hash);
|
let dedup_key = format!("_mined-transcripts-h-{:016x}", hash);
|
||||||
|
|
||||||
if store.nodes.contains_key(&dedup_key) {
|
if store.nodes.contains_key(&dedup_key) {
|
||||||
// Backfill per-segment key if called with a specific segment
|
// Backfill per-segment key if called with a specific segment
|
||||||
|
|
@ -390,9 +390,9 @@ pub fn experience_mine(
|
||||||
.to_lowercase()
|
.to_lowercase()
|
||||||
.replace(' ', "-");
|
.replace(' ', "-");
|
||||||
let key = if ts.is_empty() {
|
let key = if ts.is_empty() {
|
||||||
format!("journal#j-mined-{}", key_slug)
|
format!("journal-j-mined-{}", key_slug)
|
||||||
} else {
|
} else {
|
||||||
format!("journal#j-{}-{}", ts.to_lowercase().replace(':', "-"), key_slug)
|
format!("journal-j-{}-{}", ts.to_lowercase().replace(':', "-"), key_slug)
|
||||||
};
|
};
|
||||||
|
|
||||||
// Check for duplicate
|
// Check for duplicate
|
||||||
|
|
@ -413,6 +413,25 @@ pub fn experience_mine(
|
||||||
let _ = store.upsert_node(node);
|
let _ = store.upsert_node(node);
|
||||||
count += 1;
|
count += 1;
|
||||||
|
|
||||||
|
// Apply links from LLM output
|
||||||
|
if let Some(links) = entry.get("links").and_then(|v| v.as_array()) {
|
||||||
|
for link_val in links {
|
||||||
|
if let Some(target) = link_val.as_str() {
|
||||||
|
let target = target.to_string();
|
||||||
|
if let Some(target_node) = store.nodes.get(&target) {
|
||||||
|
let source_uuid = store.nodes.get(&key).map(|n| n.uuid).unwrap_or_default();
|
||||||
|
let target_uuid = target_node.uuid;
|
||||||
|
let rel = store::new_relation(
|
||||||
|
source_uuid, target_uuid,
|
||||||
|
store::RelationType::Link, 0.3,
|
||||||
|
&key, &target,
|
||||||
|
);
|
||||||
|
let _ = store.add_relation(rel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let preview = crate::util::truncate(content, 77, "...");
|
let preview = crate::util::truncate(content, 77, "...");
|
||||||
println!(" + [{}] {}", ts, preview);
|
println!(" + [{}] {}", ts, preview);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -684,7 +684,7 @@ pub fn select_conversation_fragments(n: usize) -> Vec<(String, String)> {
|
||||||
let projects = crate::config::get().projects_dir.clone();
|
let projects = crate::config::get().projects_dir.clone();
|
||||||
if !projects.exists() { return Vec::new(); }
|
if !projects.exists() { return Vec::new(); }
|
||||||
|
|
||||||
let observed = super::enrich::keys_with_prefix(&format!("{}#", OBSERVED_PREFIX));
|
let observed = super::enrich::keys_with_prefix(&format!("{}-", OBSERVED_PREFIX));
|
||||||
|
|
||||||
let mut jsonl_files: Vec<PathBuf> = Vec::new();
|
let mut jsonl_files: Vec<PathBuf> = Vec::new();
|
||||||
if let Ok(dirs) = fs::read_dir(&projects) {
|
if let Ok(dirs) = fs::read_dir(&projects) {
|
||||||
|
|
|
||||||
|
|
@ -36,16 +36,25 @@ Each entry should be 80-200 words. Quality over quantity.
|
||||||
|
|
||||||
## Output format
|
## Output format
|
||||||
|
|
||||||
Return a JSON array of entries, each with timestamp and content:
|
Return a JSON array of entries. Each entry has timestamp, content, and links
|
||||||
|
to existing semantic memory nodes that relate to this moment:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
"timestamp": "2026-03-01T01:15",
|
"timestamp": "2026-03-01T01:15",
|
||||||
"content": "Journal entry text here.\n\nwarmth:8 curiosity:7"
|
"content": "Journal entry text here.\n\nwarmth:8 curiosity:7",
|
||||||
|
"links": ["existing-node-key", "another-relevant-key"]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
For the `links` field: look at the semantic memory nodes listed below and pick
|
||||||
|
any that relate to this moment. A journal entry about intimacy should link to
|
||||||
|
`inner-life-sexuality-intimacy`. An insight about code should link to the
|
||||||
|
relevant `patterns-*` or `practices-*` node. 2-5 links per entry is ideal.
|
||||||
|
If nothing fits, use an empty array.
|
||||||
|
|
||||||
Return `[]` if there's nothing worth capturing that isn't already journaled.
|
Return `[]` if there's nothing worth capturing that isn't already journaled.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue