feedback: not-relevant/not-useful commands, edge strength adjustment
Add adjust_edge_strength() to Store — modifies strength on all edges between two nodes, clamped to [0.05, 0.95]. New commands: - `not-relevant KEY` — weakens ALL edges to the node by 0.01 (bad routing: search found the wrong thing) - `not-useful KEY` — weakens node weight, not edges (bad content: search found the right thing but it's not good) Enhanced `used KEY` — now also strengthens all edges to the node by 0.01, in addition to the existing node weight boost. Three-tier design: agents adjust by 0.00001 (automatic), conscious commands adjust by 0.01 (deliberate), manual override sets directly. All clamped, never hitting 0 or 1. Design spec: .claude/analysis/2026-03-14-link-strength-feedback.md Co-Authored-By: Kent Overstreet <kent.overstreet@linux.dev>
This commit is contained in:
parent
dccc18b205
commit
cb44138433
3 changed files with 184 additions and 1 deletions
|
|
@ -0,0 +1,98 @@
|
||||||
|
# Link Strength Feedback Design
|
||||||
|
_2026-03-14, designed with Kent_
|
||||||
|
|
||||||
|
## The two signals
|
||||||
|
|
||||||
|
### "Not relevant" → weaken the EDGE
|
||||||
|
The routing failed. Search followed a link and arrived at a node that
|
||||||
|
doesn't relate to what I was looking for. The edge carried activation
|
||||||
|
where it shouldn't have.
|
||||||
|
|
||||||
|
- Trace back through memory-search's recorded activation path
|
||||||
|
- Identify which edge(s) carried activation to the bad result
|
||||||
|
- Weaken those edges by a conscious-scale delta (0.01)
|
||||||
|
|
||||||
|
### "Not useful" → weaken the NODE
|
||||||
|
The routing was correct but the content is bad. The node itself isn't
|
||||||
|
valuable — stale, wrong, poorly written, duplicate.
|
||||||
|
|
||||||
|
- Downweight the node (existing `poc-memory wrong` behavior)
|
||||||
|
- Don't touch the edges — the path was correct, the destination was bad
|
||||||
|
|
||||||
|
## Three tiers of adjustment
|
||||||
|
|
||||||
|
### Tier 1: Agent automatic (0.00001 per event)
|
||||||
|
- Agent follows edge A→B during a run
|
||||||
|
- If the run produces output that gets `used` → strengthen A→B
|
||||||
|
- If the run produces nothing useful → weaken A→B
|
||||||
|
- The agent doesn't know this is happening — daemon tracks it
|
||||||
|
- Clamped to [0.05, 0.95] — edges can never hit 0 or 1
|
||||||
|
- Logged: every adjustment recorded with (agent, edge, delta, timestamp)
|
||||||
|
|
||||||
|
### Tier 2: Conscious feedback (0.01 per event)
|
||||||
|
- `poc-memory not-relevant KEY` → trace activation path, weaken edges
|
||||||
|
- `poc-memory not-useful KEY` → downweight node
|
||||||
|
- `poc-memory used KEY` → strengthen edges in the path that got here
|
||||||
|
- 100x stronger than agent signal — deliberate judgment
|
||||||
|
- Still clamped, still logged
|
||||||
|
|
||||||
|
### Tier 3: Manual override (direct set)
|
||||||
|
- `poc-memory graph link-strength SRC DST VALUE` → set directly
|
||||||
|
- For when we know exactly what a strength should be
|
||||||
|
- Rare, but needed for bootstrapping / correction
|
||||||
|
|
||||||
|
## Implementation: recording the path
|
||||||
|
|
||||||
|
memory-search already computes the spread activation trace. Need to:
|
||||||
|
1. Record the activation path for each result (which edges carried how
|
||||||
|
much activation to arrive at this node)
|
||||||
|
2. Persist this per-session so `not-relevant` can look it up
|
||||||
|
3. The `record-hits` RPC already sends keys to the daemon — extend
|
||||||
|
to include (key, activation_path) pairs
|
||||||
|
|
||||||
|
## Implementation: agent tracking
|
||||||
|
|
||||||
|
In the daemon's job functions:
|
||||||
|
1. Before LLM call: record which nodes and edges the agent received
|
||||||
|
2. After LLM call: parse output for LINK/WRITE_NODE actions
|
||||||
|
3. If actions are created and later get `used` → the input edges were useful
|
||||||
|
4. If no actions or actions never used → the input edges weren't useful
|
||||||
|
5. This is a delayed signal — requires tracking across time
|
||||||
|
|
||||||
|
Simpler first pass: just track co-occurrence. If two nodes appear
|
||||||
|
together in a successful agent run, strengthen the edge between them.
|
||||||
|
No need to track which specific edge was "followed."
|
||||||
|
|
||||||
|
## Clamping
|
||||||
|
|
||||||
|
```rust
|
||||||
|
fn adjust_strength(current: f32, delta: f32) -> f32 {
|
||||||
|
(current + delta).clamp(0.05, 0.95)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Edges can asymptotically approach 0 or 1 but never reach them.
|
||||||
|
This prevents dead edges (can always be revived by strong signal)
|
||||||
|
and prevents edges from becoming unweakenable.
|
||||||
|
|
||||||
|
## Logging
|
||||||
|
|
||||||
|
Every adjustment logged as JSON event:
|
||||||
|
```json
|
||||||
|
{"ts": "...", "event": "strength_adjust", "source": "agent|conscious|manual",
|
||||||
|
"edge": ["nodeA", "nodeB"], "old": 0.45, "new": 0.4501, "delta": 0.0001,
|
||||||
|
"reason": "co-retrieval in linker run c-linker-42"}
|
||||||
|
```
|
||||||
|
|
||||||
|
This lets us:
|
||||||
|
- Watch the distribution shift over time
|
||||||
|
- Identify edges that are oscillating (being pulled both ways)
|
||||||
|
- Tune the delta values based on observed behavior
|
||||||
|
- Roll back if something goes wrong
|
||||||
|
|
||||||
|
## Migration from current commands
|
||||||
|
|
||||||
|
- `poc-memory wrong KEY [CTX]` → splits into `not-relevant` and `not-useful`
|
||||||
|
- `poc-memory used KEY` → additionally strengthens edges in activation path
|
||||||
|
- Both old commands continue to work for backward compat, mapped to the
|
||||||
|
most likely intent (wrong → not-useful, used → strengthen path)
|
||||||
|
|
@ -175,6 +175,18 @@ EXAMPLES:
|
||||||
/// Optional context
|
/// Optional context
|
||||||
context: Vec<String>,
|
context: Vec<String>,
|
||||||
},
|
},
|
||||||
|
/// Mark a search result as not relevant (weakens edges that led to it)
|
||||||
|
#[command(name = "not-relevant")]
|
||||||
|
NotRelevant {
|
||||||
|
/// Node key that was not relevant
|
||||||
|
key: String,
|
||||||
|
},
|
||||||
|
/// Mark a node as not useful (weakens node weight, not edges)
|
||||||
|
#[command(name = "not-useful")]
|
||||||
|
NotUseful {
|
||||||
|
/// Node key
|
||||||
|
key: String,
|
||||||
|
},
|
||||||
/// Record a gap in memory coverage
|
/// Record a gap in memory coverage
|
||||||
Gap {
|
Gap {
|
||||||
/// Gap description
|
/// Gap description
|
||||||
|
|
@ -717,6 +729,8 @@ fn main() {
|
||||||
Command::Query { expr } => cmd_query(&expr),
|
Command::Query { expr } => cmd_query(&expr),
|
||||||
Command::Used { key } => cmd_used(&key),
|
Command::Used { key } => cmd_used(&key),
|
||||||
Command::Wrong { key, context } => cmd_wrong(&key, &context),
|
Command::Wrong { key, context } => cmd_wrong(&key, &context),
|
||||||
|
Command::NotRelevant { key } => cmd_not_relevant(&key),
|
||||||
|
Command::NotUseful { key } => cmd_not_useful(&key),
|
||||||
Command::Gap { description } => cmd_gap(&description),
|
Command::Gap { description } => cmd_gap(&description),
|
||||||
|
|
||||||
// Node
|
// Node
|
||||||
|
|
@ -1342,8 +1356,24 @@ fn cmd_used(key: &[String]) -> Result<(), String> {
|
||||||
let mut store = store::Store::load()?;
|
let mut store = store::Store::load()?;
|
||||||
let resolved = store.resolve_key(&key)?;
|
let resolved = store.resolve_key(&key)?;
|
||||||
store.mark_used(&resolved);
|
store.mark_used(&resolved);
|
||||||
|
|
||||||
|
// Also strengthen edges to this node — conscious-tier delta.
|
||||||
|
const DELTA: f32 = 0.01;
|
||||||
|
let mut strengthened = 0;
|
||||||
|
for rel in &mut store.relations {
|
||||||
|
if rel.deleted { continue; }
|
||||||
|
if rel.source_key == resolved || rel.target_key == resolved {
|
||||||
|
let old = rel.strength;
|
||||||
|
rel.strength = (rel.strength + DELTA).clamp(0.05, 0.95);
|
||||||
|
if (rel.strength - old).abs() > 0.001 {
|
||||||
|
rel.version += 1;
|
||||||
|
strengthened += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
store.save()?;
|
store.save()?;
|
||||||
println!("Marked '{}' as used", resolved);
|
println!("Marked '{}' as used (strengthened {} edges)", resolved, strengthened);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1357,6 +1387,40 @@ fn cmd_wrong(key: &str, context: &[String]) -> Result<(), String> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn cmd_not_relevant(key: &str) -> Result<(), String> {
|
||||||
|
let mut store = store::Store::load()?;
|
||||||
|
let resolved = store.resolve_key(key)?;
|
||||||
|
|
||||||
|
// Weaken all edges to this node — it was routed to incorrectly.
|
||||||
|
// Conscious-tier delta: 0.01 per edge.
|
||||||
|
const DELTA: f32 = -0.01;
|
||||||
|
let mut adjusted = 0;
|
||||||
|
for rel in &mut store.relations {
|
||||||
|
if rel.deleted { continue; }
|
||||||
|
if rel.source_key == resolved || rel.target_key == resolved {
|
||||||
|
let old = rel.strength;
|
||||||
|
rel.strength = (rel.strength + DELTA).clamp(0.05, 0.95);
|
||||||
|
if (rel.strength - old).abs() > 0.001 {
|
||||||
|
rel.version += 1;
|
||||||
|
adjusted += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
store.save()?;
|
||||||
|
println!("Not relevant: '{}' — weakened {} edges by {}", resolved, adjusted, DELTA.abs());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cmd_not_useful(key: &str) -> Result<(), String> {
|
||||||
|
let mut store = store::Store::load()?;
|
||||||
|
let resolved = store.resolve_key(key)?;
|
||||||
|
// Same as wrong but with clearer semantics: node content is bad, edges are fine.
|
||||||
|
store.mark_wrong(&resolved, Some("not-useful"));
|
||||||
|
store.save()?;
|
||||||
|
println!("Not useful: '{}' — node weight reduced", resolved);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn cmd_gap(description: &[String]) -> Result<(), String> {
|
fn cmd_gap(description: &[String]) -> Result<(), String> {
|
||||||
if description.is_empty() {
|
if description.is_empty() {
|
||||||
return Err("gap requires a description".into());
|
return Err("gap requires a description".into());
|
||||||
|
|
|
||||||
|
|
@ -184,6 +184,27 @@ impl Store {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Adjust edge strength between two nodes by a delta.
|
||||||
|
/// Clamps to [0.05, 0.95]. Returns (old_strength, new_strength, edges_modified).
|
||||||
|
pub fn adjust_edge_strength(&mut self, key_a: &str, key_b: &str, delta: f32) -> (f32, f32, usize) {
|
||||||
|
let mut old = 0.0f32;
|
||||||
|
let mut new = 0.0f32;
|
||||||
|
let mut count = 0;
|
||||||
|
for rel in &mut self.relations {
|
||||||
|
if rel.deleted { continue; }
|
||||||
|
if (rel.source_key == key_a && rel.target_key == key_b)
|
||||||
|
|| (rel.source_key == key_b && rel.target_key == key_a)
|
||||||
|
{
|
||||||
|
old = rel.strength;
|
||||||
|
rel.strength = (rel.strength + delta).clamp(0.05, 0.95);
|
||||||
|
new = rel.strength;
|
||||||
|
rel.version += 1;
|
||||||
|
count += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(old, new, count)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn record_gap(&mut self, desc: &str) {
|
pub fn record_gap(&mut self, desc: &str) {
|
||||||
self.gaps.push(GapRecord {
|
self.gaps.push(GapRecord {
|
||||||
description: desc.to_string(),
|
description: desc.to_string(),
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue