Skip to content

Commit 2447958

Browse files
committed
feat(memory): phase 2 smartness - semantic dedup, intent confidence, consolidation, index fingerprint, stats API
- semantic dedup on add: after the exact-match tier, atoms whose top-1 cosine similarity to a live atom exceeds semantic_dedup_threshold (default 0.92, 0 disables) refresh the existing atom instead of duplicating; in-batch duplicates handled via direct embedding - predicted-intent confidence now weights recall scores; intents below 0.3 confidence are skipped entirely - extractor confidence default is now 0.7 (was 1.0) so unconfident extractions no longer get maximal retention and eviction resistance - new ConsolidateAtoms: greedy similarity clustering (default 0.9) + one LLM merge call per group; originals kept on any failure; merged atoms keep highest confidence and union of associations; live store only, quarantine untouched - vector index persists a corpus fingerprint (count + FNV-1a of sorted atom IDs) in vectors_meta.json; a mismatch on load forces a rebuild instead of silently serving a stale corpus (legacy files rebuild once) - new Stats() API: live/quarantined atoms, quarantine reason breakdown, index vectors/dirty, store size, and recall timeout/failure counters for degradation observability
1 parent 18c21d6 commit 2447958

16 files changed

Lines changed: 1407 additions & 72 deletions

docs/EXTENDED_MEMORY.md

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,13 +99,13 @@ The current extraction prompt and tool schema accept all nine primary types. Unk
9999
│ └── <atom-id>.md
100100
├── vectors.gob # persisted go-vector store
101101
├── vectors.gob.emb # persisted embedder state (RP vocabulary / HTTP fingerprint)
102-
├── vectors_meta.json # embedding-space fingerprint for invalidation
102+
├── vectors_meta.json # embedding-space + corpus fingerprints for invalidation/staleness
103103
├── quarantine.json # tainted atoms awaiting promotion
104104
├── user_model.json # persisted inferred user state + pending review queue
105105
└── associations.json # bidirectional atom association links
106106
```
107107

108-
All files are written atomically and use `0600` permissions. The vector store and metadata are rebuilt from the chunk files if they are ever corrupted. The user model and associations are loaded at startup and persisted on every change.
108+
All files are written atomically and use `0600` permissions. The vector store and metadata are rebuilt from the chunk files if they are ever corrupted. `vectors_meta.json` records both the embedding-space fingerprint (which embedding backend produced the vectors) and a corpus fingerprint (atom count plus a hash of the sorted atom IDs); a mismatch on either — including a missing corpus fingerprint in files written before it was tracked — marks the persisted index stale so it is rebuilt once instead of silently serving an outdated corpus. The user model and associations are loaded at startup and persisted on every change.
109109

110110
## Dedicated Memory LLM
111111

@@ -163,6 +163,23 @@ Extracted atoms are immediately:
163163
4. Embedded and written to the atom store.
164164
5. Checked against the 100 MB size cap; low-retention atoms are evicted if needed.
165165

166+
Explicit LLM-provided `confidence` values in (0, 1] are kept. A missing or out-of-range confidence defaults to **0.7** for extraction-origin atoms, so unqualified extracted memories do not inflate their retention score.
167+
168+
### Write-Path Deduplication
169+
170+
Atoms are deduplicated in two tiers at persistence time:
171+
172+
1. **Exact match**: an atom whose normalized text (lowercased, whitespace-collapsed) equals an existing live atom refreshes it — new `CreatedAt`, the higher confidence wins, the original ID is kept — instead of appending a duplicate.
173+
2. **Semantic match**: if no exact match exists and `semantic_dedup_threshold` is non-zero, the incoming atom is compared against the live corpus via the vector index (top-1 similarity); atoms stored earlier in the same batch, which the index does not cover yet, are compared by embedding both texts directly. A match at or above the threshold is refreshed the same way. The dedup search uses the pre-batch index state and never triggers extra index rebuilds: the single index invalidation after the batch is preserved.
174+
175+
### Atom Consolidation
176+
177+
`ExtendedMemory.ConsolidateAtoms(ctx)` (no CLI yet) merges groups of live atoms that are near-duplicates (pairwise cosine similarity at or above `consolidate_similarity_threshold`). For each group it asks the memory LLM — one call per group — to merge the texts into a single concise atom, stores the merged atom through the normal add path (scan and size cap apply) keeping the group's highest confidence, a refreshed `CreatedAt`, and the union of the group's outward associations, then removes the originals. Quarantined atoms are never consolidated. On any failure for a group (LLM error, empty response, scan rejection) the originals are kept untouched.
178+
179+
### Observability
180+
181+
`ExtendedMemory.Stats()` returns a `Stats` snapshot for operators and monitoring: live/quarantined atom counts, quarantine entries grouped by reason prefix (`tainted`, `scan_rejected`), index vector count and dirty flag, on-disk store size, and recall error counters (`RecallTimeouts` for context-deadline-exceeded errors, `RecallFailures` for all other recall errors).
182+
166183
## Semantic Search
167184

168185
Extended Memory replaces episode-based recall with semantic search over atom vectors.
@@ -200,6 +217,8 @@ The final recall result is also bounded by `memory_budget_chars`. Tainted atoms
200217

201218
When `follow_up_anticipation_enabled` is true (default), the memory LLM receives the current user message, the last several user messages, and the current user-state model. It returns a JSON array of up to `predictive_intents` likely follow-up intents. Each intent is embedded and searched, and the union of literal-query matches and predicted-intent matches is injected into the main agent's context. For each predicted intent, the system also performs a type-targeted recall for `convention`, `file`, and `error` atoms so the agent can pre-load relevant conventions, references, and known failure modes.
202219

220+
Intents carry a `confidence` (0.0-1.0). Intents below 0.3 are skipped entirely; otherwise the composite score of every atom found via a predicted intent is multiplied by that intent's confidence, so a speculative intent cannot outrank literal-query matches.
221+
203222
Example:
204223

205224
- User: "Refactor the auth package to remove JWT."
@@ -355,6 +374,8 @@ Extended Memory is configured under the `memory.extended` section.
355374
"user_state_max_pending": 20,
356375
"associations_enabled": true,
357376
"association_semantic_top_k": 3,
377+
"semantic_dedup_threshold": 0.92,
378+
"consolidate_similarity_threshold": 0.9,
358379
"proactive_return_after_break": true,
359380
"style_mirroring_enabled": true,
360381
"anaphora_resolution_enabled": true,
@@ -401,6 +422,8 @@ Extended Memory is configured under the `memory.extended` section.
401422
| `user_state_max_pending` | `20` | Maximum pending-review entries kept in the user model. |
402423
| `associations_enabled` | `true` | Enable bidirectional atom associations (temporal, task, semantic). |
403424
| `association_semantic_top_k` | `3` | Number of semantic neighbours linked when building associations. |
425+
| `semantic_dedup_threshold` | `0.92` | Cosine similarity at or above which an incoming atom is treated as a paraphrase of an existing live atom and refreshes it instead of appending. `0` disables the semantic tier (exact-match dedup always runs). |
426+
| `consolidate_similarity_threshold` | `0.9` | Cosine similarity at or above which live atoms are grouped as near-duplicates for `ConsolidateAtoms` merging. |
404427
| `proactive_return_after_break` | `true` | On session resume, inject a "where you left off" summary. |
405428
| `style_mirroring_enabled` | `true` | Inject a style-guidance directive based on the inferred user model. |
406429
| `anaphora_resolution_enabled` | `true` | Resolve the first pronoun in a user message against recent trusted atoms when the top atom's score is high enough. |
@@ -455,6 +478,8 @@ Additional CLI commands:
455478
odek memory extended forget <atom-id>
456479
odek memory extended quarantine
457480
odek memory extended compact
481+
odek memory extended stats
482+
odek memory extended consolidate
458483
odek memory extended promote <atom-id>
459484
odek memory extended pin <atom-id>
460485
odek memory extended pending
@@ -467,6 +492,8 @@ odek memory extended reject <pending-id>
467492
| `forget` | Removes an atom from the live store or quarantine. |
468493
| `quarantine` | Lists tainted atoms awaiting promotion. |
469494
| `compact` | Triggers a background rebuild of the vector index to reclaim space. |
495+
| `stats` | Shows store/index sizes, quarantine reason breakdown, and recall degradation counters (timeouts/failures). |
496+
| `consolidate` | Merges near-duplicate live atoms via the LLM (requires a configured backend). |
470497
| `promote` | Moves a quarantined atom to the live store as `user_approved`. |
471498
| `pin` | Pins a live atom so it is never evicted. |
472499
| `pending` | Lists pending user-model inferences. |

internal/memory/extended/config.go

Lines changed: 59 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -16,30 +16,32 @@ import (
1616

1717
// Config controls the Extended Memory subsystem.
1818
type Config struct {
19-
Enabled *bool `json:"enabled,omitempty"`
20-
MaxSizeMB int `json:"max_size_mb,omitempty"`
21-
SemanticSearchTopK int `json:"semantic_search_top_k,omitempty"`
22-
SemanticSearchOverfetch int `json:"semantic_search_overfetch,omitempty"`
23-
SemanticSearchMinScore float32 `json:"semantic_search_min_score,omitempty"`
24-
SemanticSearchRerank *bool `json:"semantic_search_rerank,omitempty"`
25-
AtomMaxChars int `json:"atom_max_chars,omitempty"`
26-
MemoryBudgetChars int `json:"memory_budget_chars,omitempty"`
27-
DecayHalfLifeDays int `json:"decay_half_life_days,omitempty"`
28-
QuarantineTTLDays int `json:"quarantine_ttl_days,omitempty"`
29-
EvictionPolicy string `json:"eviction_policy,omitempty"`
30-
PredictiveIntents int `json:"predictive_intents,omitempty"`
31-
AutoExtractPerTurn *bool `json:"auto_extract_per_turn,omitempty"`
32-
InferUserState *bool `json:"infer_user_state,omitempty"`
33-
UserStateTurnInterval int `json:"user_state_turn_interval,omitempty"`
34-
UserStateMaxPending int `json:"user_state_max_pending,omitempty"`
35-
AssociationsEnabled *bool `json:"associations_enabled,omitempty"`
36-
AssociationSemanticTopK int `json:"association_semantic_top_k,omitempty"`
37-
ProactiveReturnAfterBreak *bool `json:"proactive_return_after_break,omitempty"`
38-
StyleMirroringEnabled *bool `json:"style_mirroring_enabled,omitempty"`
39-
AnaphoraResolutionEnabled *bool `json:"anaphora_resolution_enabled,omitempty"`
40-
FollowUpAnticipationEnabled *bool `json:"follow_up_anticipation_enabled,omitempty"`
41-
LLM *LLMConfig `json:"llm,omitempty"`
42-
Embedding *embedding.Config `json:"embedding,omitempty"`
19+
Enabled *bool `json:"enabled,omitempty"`
20+
MaxSizeMB int `json:"max_size_mb,omitempty"`
21+
SemanticSearchTopK int `json:"semantic_search_top_k,omitempty"`
22+
SemanticSearchOverfetch int `json:"semantic_search_overfetch,omitempty"`
23+
SemanticSearchMinScore float32 `json:"semantic_search_min_score,omitempty"`
24+
SemanticSearchRerank *bool `json:"semantic_search_rerank,omitempty"`
25+
AtomMaxChars int `json:"atom_max_chars,omitempty"`
26+
MemoryBudgetChars int `json:"memory_budget_chars,omitempty"`
27+
DecayHalfLifeDays int `json:"decay_half_life_days,omitempty"`
28+
QuarantineTTLDays int `json:"quarantine_ttl_days,omitempty"`
29+
EvictionPolicy string `json:"eviction_policy,omitempty"`
30+
PredictiveIntents int `json:"predictive_intents,omitempty"`
31+
AutoExtractPerTurn *bool `json:"auto_extract_per_turn,omitempty"`
32+
InferUserState *bool `json:"infer_user_state,omitempty"`
33+
UserStateTurnInterval int `json:"user_state_turn_interval,omitempty"`
34+
UserStateMaxPending int `json:"user_state_max_pending,omitempty"`
35+
AssociationsEnabled *bool `json:"associations_enabled,omitempty"`
36+
AssociationSemanticTopK int `json:"association_semantic_top_k,omitempty"`
37+
SemanticDedupThreshold *float32 `json:"semantic_dedup_threshold,omitempty"`
38+
ConsolidateSimilarityThreshold float32 `json:"consolidate_similarity_threshold,omitempty"`
39+
ProactiveReturnAfterBreak *bool `json:"proactive_return_after_break,omitempty"`
40+
StyleMirroringEnabled *bool `json:"style_mirroring_enabled,omitempty"`
41+
AnaphoraResolutionEnabled *bool `json:"anaphora_resolution_enabled,omitempty"`
42+
FollowUpAnticipationEnabled *bool `json:"follow_up_anticipation_enabled,omitempty"`
43+
LLM *LLMConfig `json:"llm,omitempty"`
44+
Embedding *embedding.Config `json:"embedding,omitempty"`
4345
}
4446

4547
// LLMConfig selects a dedicated LLM for Extended Memory extraction and
@@ -57,32 +59,37 @@ type LLMConfig struct {
5759
// boolPtr returns a pointer to b.
5860
func boolPtr(b bool) *bool { return &b }
5961

62+
// floatPtr returns a pointer to f.
63+
func floatPtr(f float32) *float32 { return &f }
64+
6065
// DefaultConfig returns the default Extended Memory configuration.
6166
// Extended Memory is opt-in: Enabled defaults to false.
6267
func DefaultConfig() Config {
6368
return Config{
64-
Enabled: boolPtr(false),
65-
MaxSizeMB: 100,
66-
SemanticSearchTopK: 10,
67-
SemanticSearchOverfetch: 4,
68-
SemanticSearchMinScore: 0.55,
69-
SemanticSearchRerank: boolPtr(true),
70-
AtomMaxChars: 300,
71-
MemoryBudgetChars: 2000,
72-
DecayHalfLifeDays: 30,
73-
QuarantineTTLDays: 7,
74-
EvictionPolicy: "retention_decay",
75-
PredictiveIntents: 3,
76-
AutoExtractPerTurn: boolPtr(true),
77-
InferUserState: boolPtr(true),
78-
UserStateTurnInterval: 5,
79-
UserStateMaxPending: 20,
80-
AssociationsEnabled: boolPtr(true),
81-
AssociationSemanticTopK: 3,
82-
ProactiveReturnAfterBreak: boolPtr(true),
83-
StyleMirroringEnabled: boolPtr(true),
84-
AnaphoraResolutionEnabled: boolPtr(true),
85-
FollowUpAnticipationEnabled: boolPtr(true),
69+
Enabled: boolPtr(false),
70+
MaxSizeMB: 100,
71+
SemanticSearchTopK: 10,
72+
SemanticSearchOverfetch: 4,
73+
SemanticSearchMinScore: 0.55,
74+
SemanticSearchRerank: boolPtr(true),
75+
AtomMaxChars: 300,
76+
MemoryBudgetChars: 2000,
77+
DecayHalfLifeDays: 30,
78+
QuarantineTTLDays: 7,
79+
EvictionPolicy: "retention_decay",
80+
PredictiveIntents: 3,
81+
AutoExtractPerTurn: boolPtr(true),
82+
InferUserState: boolPtr(true),
83+
UserStateTurnInterval: 5,
84+
UserStateMaxPending: 20,
85+
AssociationsEnabled: boolPtr(true),
86+
AssociationSemanticTopK: 3,
87+
SemanticDedupThreshold: floatPtr(0.92),
88+
ConsolidateSimilarityThreshold: 0.9,
89+
ProactiveReturnAfterBreak: boolPtr(true),
90+
StyleMirroringEnabled: boolPtr(true),
91+
AnaphoraResolutionEnabled: boolPtr(true),
92+
FollowUpAnticipationEnabled: boolPtr(true),
8693
}
8794
}
8895

@@ -143,6 +150,12 @@ func Resolve(cfg Config) Config {
143150
if cfg.AssociationSemanticTopK > 0 {
144151
def.AssociationSemanticTopK = cfg.AssociationSemanticTopK
145152
}
153+
if cfg.SemanticDedupThreshold != nil {
154+
def.SemanticDedupThreshold = cfg.SemanticDedupThreshold
155+
}
156+
if cfg.ConsolidateSimilarityThreshold > 0 {
157+
def.ConsolidateSimilarityThreshold = cfg.ConsolidateSimilarityThreshold
158+
}
146159
if cfg.ProactiveReturnAfterBreak != nil {
147160
def.ProactiveReturnAfterBreak = cfg.ProactiveReturnAfterBreak
148161
}

0 commit comments

Comments
 (0)