htm 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.architecture/decisions/adrs/001-use-postgresql-timescaledb-storage.md +227 -0
- data/.architecture/decisions/adrs/002-two-tier-memory-architecture.md +322 -0
- data/.architecture/decisions/adrs/003-ollama-default-embedding-provider.md +339 -0
- data/.architecture/decisions/adrs/004-multi-robot-shared-memory-hive-mind.md +374 -0
- data/.architecture/decisions/adrs/005-rag-based-retrieval-with-hybrid-search.md +443 -0
- data/.architecture/decisions/adrs/006-context-assembly-strategies.md +444 -0
- data/.architecture/decisions/adrs/007-working-memory-eviction-strategy.md +461 -0
- data/.architecture/decisions/adrs/008-robot-identification-system.md +550 -0
- data/.architecture/decisions/adrs/009-never-forget-explicit-deletion-only.md +570 -0
- data/.architecture/decisions/adrs/010-redis-working-memory-rejected.md +323 -0
- data/.architecture/decisions/adrs/011-database-side-embedding-generation-with-pgai.md +585 -0
- data/.architecture/decisions/adrs/012-llm-driven-ontology-topic-extraction.md +583 -0
- data/.architecture/decisions/adrs/013-activerecord-orm-and-many-to-many-tagging.md +299 -0
- data/.architecture/decisions/adrs/014-client-side-embedding-generation-workflow.md +569 -0
- data/.architecture/decisions/adrs/015-hierarchical-tag-ontology-and-llm-extraction.md +701 -0
- data/.architecture/decisions/adrs/016-async-embedding-and-tag-generation.md +694 -0
- data/.architecture/members.yml +144 -0
- data/.architecture/reviews/2025-10-29-llm-configuration-and-async-processing-review.md +1137 -0
- data/.architecture/reviews/initial-system-analysis.md +330 -0
- data/.envrc +32 -0
- data/.irbrc +145 -0
- data/CHANGELOG.md +150 -0
- data/COMMITS.md +196 -0
- data/LICENSE +21 -0
- data/README.md +1347 -0
- data/Rakefile +51 -0
- data/SETUP.md +268 -0
- data/config/database.yml +67 -0
- data/db/migrate/20250101000001_enable_extensions.rb +14 -0
- data/db/migrate/20250101000002_create_robots.rb +14 -0
- data/db/migrate/20250101000003_create_nodes.rb +42 -0
- data/db/migrate/20250101000005_create_tags.rb +38 -0
- data/db/migrate/20250101000007_add_node_vector_indexes.rb +30 -0
- data/db/schema.sql +473 -0
- data/db/seed_data/README.md +100 -0
- data/db/seed_data/presidents.md +136 -0
- data/db/seed_data/states.md +151 -0
- data/db/seeds.rb +208 -0
- data/dbdoc/README.md +173 -0
- data/dbdoc/public.node_stats.md +48 -0
- data/dbdoc/public.node_stats.svg +41 -0
- data/dbdoc/public.node_tags.md +40 -0
- data/dbdoc/public.node_tags.svg +112 -0
- data/dbdoc/public.nodes.md +54 -0
- data/dbdoc/public.nodes.svg +118 -0
- data/dbdoc/public.nodes_tags.md +39 -0
- data/dbdoc/public.nodes_tags.svg +112 -0
- data/dbdoc/public.ontology_structure.md +48 -0
- data/dbdoc/public.ontology_structure.svg +38 -0
- data/dbdoc/public.operations_log.md +42 -0
- data/dbdoc/public.operations_log.svg +130 -0
- data/dbdoc/public.relationships.md +39 -0
- data/dbdoc/public.relationships.svg +41 -0
- data/dbdoc/public.robot_activity.md +46 -0
- data/dbdoc/public.robot_activity.svg +35 -0
- data/dbdoc/public.robots.md +35 -0
- data/dbdoc/public.robots.svg +90 -0
- data/dbdoc/public.schema_migrations.md +29 -0
- data/dbdoc/public.schema_migrations.svg +26 -0
- data/dbdoc/public.tags.md +35 -0
- data/dbdoc/public.tags.svg +60 -0
- data/dbdoc/public.topic_relationships.md +45 -0
- data/dbdoc/public.topic_relationships.svg +32 -0
- data/dbdoc/schema.json +1437 -0
- data/dbdoc/schema.svg +154 -0
- data/docs/api/database.md +806 -0
- data/docs/api/embedding-service.md +532 -0
- data/docs/api/htm.md +797 -0
- data/docs/api/index.md +259 -0
- data/docs/api/long-term-memory.md +1096 -0
- data/docs/api/working-memory.md +665 -0
- data/docs/architecture/adrs/001-postgresql-timescaledb.md +314 -0
- data/docs/architecture/adrs/002-two-tier-memory.md +411 -0
- data/docs/architecture/adrs/003-ollama-embeddings.md +421 -0
- data/docs/architecture/adrs/004-hive-mind.md +437 -0
- data/docs/architecture/adrs/005-rag-retrieval.md +531 -0
- data/docs/architecture/adrs/006-context-assembly.md +496 -0
- data/docs/architecture/adrs/007-eviction-strategy.md +645 -0
- data/docs/architecture/adrs/008-robot-identification.md +625 -0
- data/docs/architecture/adrs/009-never-forget.md +648 -0
- data/docs/architecture/adrs/010-redis-working-memory-rejected.md +323 -0
- data/docs/architecture/adrs/011-pgai-integration.md +494 -0
- data/docs/architecture/adrs/index.md +215 -0
- data/docs/architecture/hive-mind.md +736 -0
- data/docs/architecture/index.md +351 -0
- data/docs/architecture/overview.md +538 -0
- data/docs/architecture/two-tier-memory.md +873 -0
- data/docs/assets/css/custom.css +83 -0
- data/docs/assets/images/htm-core-components.svg +63 -0
- data/docs/assets/images/htm-database-schema.svg +93 -0
- data/docs/assets/images/htm-hive-mind-architecture.svg +125 -0
- data/docs/assets/images/htm-importance-scoring-framework.svg +83 -0
- data/docs/assets/images/htm-layered-architecture.svg +71 -0
- data/docs/assets/images/htm-long-term-memory-architecture.svg +115 -0
- data/docs/assets/images/htm-working-memory-architecture.svg +120 -0
- data/docs/assets/images/htm.jpg +0 -0
- data/docs/assets/images/htm_demo.gif +0 -0
- data/docs/assets/js/mathjax.js +18 -0
- data/docs/assets/videos/htm_video.mp4 +0 -0
- data/docs/database_rake_tasks.md +322 -0
- data/docs/development/contributing.md +787 -0
- data/docs/development/index.md +336 -0
- data/docs/development/schema.md +596 -0
- data/docs/development/setup.md +719 -0
- data/docs/development/testing.md +819 -0
- data/docs/guides/adding-memories.md +824 -0
- data/docs/guides/context-assembly.md +1009 -0
- data/docs/guides/getting-started.md +577 -0
- data/docs/guides/index.md +118 -0
- data/docs/guides/long-term-memory.md +941 -0
- data/docs/guides/multi-robot.md +866 -0
- data/docs/guides/recalling-memories.md +927 -0
- data/docs/guides/search-strategies.md +953 -0
- data/docs/guides/working-memory.md +717 -0
- data/docs/index.md +214 -0
- data/docs/installation.md +477 -0
- data/docs/multi_framework_support.md +519 -0
- data/docs/quick-start.md +655 -0
- data/docs/setup_local_database.md +302 -0
- data/docs/using_rake_tasks_in_your_app.md +383 -0
- data/examples/basic_usage.rb +93 -0
- data/examples/cli_app/README.md +317 -0
- data/examples/cli_app/htm_cli.rb +270 -0
- data/examples/custom_llm_configuration.rb +183 -0
- data/examples/example_app/Rakefile +71 -0
- data/examples/example_app/app.rb +206 -0
- data/examples/sinatra_app/Gemfile +21 -0
- data/examples/sinatra_app/app.rb +335 -0
- data/lib/htm/active_record_config.rb +113 -0
- data/lib/htm/configuration.rb +342 -0
- data/lib/htm/database.rb +594 -0
- data/lib/htm/embedding_service.rb +115 -0
- data/lib/htm/errors.rb +34 -0
- data/lib/htm/job_adapter.rb +154 -0
- data/lib/htm/jobs/generate_embedding_job.rb +65 -0
- data/lib/htm/jobs/generate_tags_job.rb +82 -0
- data/lib/htm/long_term_memory.rb +965 -0
- data/lib/htm/models/node.rb +109 -0
- data/lib/htm/models/node_tag.rb +33 -0
- data/lib/htm/models/robot.rb +52 -0
- data/lib/htm/models/tag.rb +76 -0
- data/lib/htm/railtie.rb +76 -0
- data/lib/htm/sinatra.rb +157 -0
- data/lib/htm/tag_service.rb +135 -0
- data/lib/htm/tasks.rb +38 -0
- data/lib/htm/version.rb +5 -0
- data/lib/htm/working_memory.rb +182 -0
- data/lib/htm.rb +400 -0
- data/lib/tasks/db.rake +19 -0
- data/lib/tasks/htm.rake +147 -0
- data/lib/tasks/jobs.rake +312 -0
- data/mkdocs.yml +190 -0
- data/scripts/install_local_database.sh +309 -0
- metadata +341 -0
|
@@ -0,0 +1,645 @@
|
|
|
1
|
+
# ADR-007: Working Memory Eviction Strategy
|
|
2
|
+
|
|
3
|
+
**Status**: Accepted
|
|
4
|
+
|
|
5
|
+
**Date**: 2025-10-25
|
|
6
|
+
|
|
7
|
+
**Decision Makers**: Dewayne VanHoozer, Claude (Anthropic)
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Quick Summary
|
|
12
|
+
|
|
13
|
+
HTM implements a **hybrid eviction strategy** that prioritizes memories by both importance and recency, evicting low-importance older memories first when working memory reaches token capacity. Evicted memories are preserved in long-term storage, never deleted.
|
|
14
|
+
|
|
15
|
+
**Why**: Token limits are hard constraints, but eviction policy determines which context stays active. Hybrid approach balances preserving critical knowledge with maintaining temporal relevance.
|
|
16
|
+
|
|
17
|
+
**Impact**: Predictable eviction behavior with never-forget guarantees, at the cost of requiring meaningful importance scoring from users.
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Context
|
|
22
|
+
|
|
23
|
+
Working memory is token-limited (default 128,000 tokens). When adding new memories that would exceed the token limit, HTM must decide which existing memories to evict.
|
|
24
|
+
|
|
25
|
+
### Eviction Challenges
|
|
26
|
+
|
|
27
|
+
- **Token limits**: Hard constraint, cannot exceed working memory capacity
|
|
28
|
+
- **Importance preservation**: Critical memories should remain in working memory
|
|
29
|
+
- **Recency**: Recent context often more relevant than old context
|
|
30
|
+
- **Fairness**: Don't always evict the same types of memories
|
|
31
|
+
- **Performance**: Eviction should be fast (< 10ms)
|
|
32
|
+
- **Never forget**: Evicted memories must be preserved in long-term memory
|
|
33
|
+
|
|
34
|
+
### Alternative Eviction Policies
|
|
35
|
+
|
|
36
|
+
1. **FIFO (First-In-First-Out)**: Evict oldest memories
|
|
37
|
+
2. **LRU (Least Recently Used)**: Evict least recently accessed
|
|
38
|
+
3. **LFU (Least Frequently Used)**: Evict least frequently accessed
|
|
39
|
+
4. **Random**: Evict random memories
|
|
40
|
+
5. **Importance-only**: Evict lowest importance first
|
|
41
|
+
6. **Hybrid**: Combine importance and recency with scoring function
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## Decision
|
|
46
|
+
|
|
47
|
+
We will implement a **hybrid eviction strategy** that prioritizes memories by both importance and recency, evicting low-importance older memories first.
|
|
48
|
+
|
|
49
|
+
<svg viewBox="0 0 950 750" xmlns="http://www.w3.org/2000/svg" style="background: transparent;">
|
|
50
|
+
<defs>
|
|
51
|
+
<style>
|
|
52
|
+
.flow-box { fill: rgba(33, 150, 243, 0.2); stroke: #2196F3; stroke-width: 2; }
|
|
53
|
+
.tier-box { fill: rgba(76, 175, 80, 0.2); stroke: #4CAF50; stroke-width: 2; }
|
|
54
|
+
.tier1-box { fill: rgba(244, 67, 54, 0.3); stroke: #F44336; stroke-width: 2; }
|
|
55
|
+
.tier2-box { fill: rgba(255, 152, 0, 0.3); stroke: #FF9800; stroke-width: 2; }
|
|
56
|
+
.tier3-box { fill: rgba(255, 235, 59, 0.3); stroke: #FFEB3B; stroke-width: 2; }
|
|
57
|
+
.tier4-box { fill: rgba(76, 175, 80, 0.3); stroke: #4CAF50; stroke-width: 2; }
|
|
58
|
+
.text-header { fill: #E0E0E0; font-size: 18px; font-weight: bold; }
|
|
59
|
+
.text-label { fill: #E0E0E0; font-size: 14px; font-weight: bold; }
|
|
60
|
+
.text-small { fill: #B0B0B0; font-size: 11px; }
|
|
61
|
+
.text-tiny { fill: #A0A0A0; font-size: 10px; }
|
|
62
|
+
.arrow { stroke: #4A9EFF; stroke-width: 2; fill: none; marker-end: url(#arrowhead); }
|
|
63
|
+
</style>
|
|
64
|
+
<marker id="arrowhead" markerWidth="10" markerHeight="10" refX="9" refY="3" orient="auto">
|
|
65
|
+
<polygon points="0 0, 10 3, 0 6" fill="#4A9EFF" />
|
|
66
|
+
</marker>
|
|
67
|
+
</defs>
|
|
68
|
+
|
|
69
|
+
<!-- Title -->
|
|
70
|
+
<text x="475" y="30" text-anchor="middle" class="text-header">Hybrid Eviction Algorithm</text>
|
|
71
|
+
|
|
72
|
+
<!-- Algorithm Flow -->
|
|
73
|
+
<text x="50" y="65" class="text-label">Eviction Process</text>
|
|
74
|
+
|
|
75
|
+
<!-- Step 1 -->
|
|
76
|
+
<rect x="50" y="80" width="380" height="60" class="flow-box" rx="5"/>
|
|
77
|
+
<text x="60" y="100" class="text-label">1. Calculate Eviction Score</text>
|
|
78
|
+
<text x="60" y="120" class="text-small">score = [importance, -recency]</text>
|
|
79
|
+
|
|
80
|
+
<!-- Step 2 -->
|
|
81
|
+
<rect x="50" y="155" width="380" height="60" class="flow-box" rx="5"/>
|
|
82
|
+
<text x="60" y="175" class="text-label">2. Sort Memories by Score</text>
|
|
83
|
+
<text x="60" y="195" class="text-small">Primary: importance (asc), Secondary: age (desc)</text>
|
|
84
|
+
|
|
85
|
+
<!-- Step 3 -->
|
|
86
|
+
<rect x="50" y="230" width="380" height="60" class="flow-box" rx="5"/>
|
|
87
|
+
<text x="60" y="250" class="text-label">3. Greedy Eviction</text>
|
|
88
|
+
<text x="60" y="270" class="text-small">Evict from front until needed_tokens freed</text>
|
|
89
|
+
|
|
90
|
+
<!-- Step 4 -->
|
|
91
|
+
<rect x="50" y="305" width="380" height="60" class="flow-box" rx="5"/>
|
|
92
|
+
<text x="60" y="325" class="text-label">4. Mark as Evicted</text>
|
|
93
|
+
<text x="60" y="345" class="text-small">Set in_working_memory = false in database</text>
|
|
94
|
+
|
|
95
|
+
<!-- Arrows -->
|
|
96
|
+
<path d="M 240 140 L 240 155" class="arrow"/>
|
|
97
|
+
<path d="M 240 215 L 240 230" class="arrow"/>
|
|
98
|
+
<path d="M 240 290 L 240 305" class="arrow"/>
|
|
99
|
+
|
|
100
|
+
<!-- Eviction Tiers -->
|
|
101
|
+
<text x="520" y="65" class="text-label">Eviction Priority Tiers</text>
|
|
102
|
+
<text x="520" y="80" class="text-small">(Lower tier = evicted first)</text>
|
|
103
|
+
|
|
104
|
+
<!-- Tier 1 -->
|
|
105
|
+
<rect x="520" y="100" width="380" height="110" class="tier1-box" rx="5"/>
|
|
106
|
+
<text x="530" y="120" class="text-label" fill="#F44336">Tier 1: Low Importance + Old</text>
|
|
107
|
+
<text x="530" y="140" class="text-small">Evicted First</text>
|
|
108
|
+
<text x="530" y="165" class="text-tiny">importance: 1.0, age: 5 days → evicted 1st</text>
|
|
109
|
+
<text x="530" y="180" class="text-tiny">importance: 2.0, age: 3 days → evicted 2nd</text>
|
|
110
|
+
<text x="530" y="195" class="text-tiny">Examples: Temporary notes, scratch data</text>
|
|
111
|
+
|
|
112
|
+
<!-- Tier 2 -->
|
|
113
|
+
<rect x="520" y="225" width="380" height="110" class="tier2-box" rx="5"/>
|
|
114
|
+
<text x="530" y="245" class="text-label" fill="#FF9800">Tier 2: Low Importance + Recent</text>
|
|
115
|
+
<text x="530" y="265" class="text-small">Evicted Second</text>
|
|
116
|
+
<text x="530" y="290" class="text-tiny">importance: 1.0, age: 1 hour → evicted 3rd</text>
|
|
117
|
+
<text x="530" y="305" class="text-tiny">importance: 2.0, age: 30 min → evicted 4th</text>
|
|
118
|
+
<text x="530" y="320" class="text-tiny">Examples: Recent but low-value context</text>
|
|
119
|
+
|
|
120
|
+
<!-- Tier 3 -->
|
|
121
|
+
<rect x="520" y="350" width="380" height="110" class="tier3-box" rx="5"/>
|
|
122
|
+
<text x="530" y="370" class="text-label" fill="#FDD835">Tier 3: High Importance + Old</text>
|
|
123
|
+
<text x="530" y="390" class="text-small">Evicted Third</text>
|
|
124
|
+
<text x="530" y="415" class="text-tiny">importance: 9.0, age: 30 days → evicted 5th</text>
|
|
125
|
+
<text x="530" y="430" class="text-tiny">importance: 10.0, age: 90 days → evicted 6th</text>
|
|
126
|
+
<text x="530" y="445" class="text-tiny">Examples: Old but critical decisions</text>
|
|
127
|
+
|
|
128
|
+
<!-- Tier 4 -->
|
|
129
|
+
<rect x="520" y="475" width="380" height="110" class="tier4-box" rx="5"/>
|
|
130
|
+
<text x="530" y="495" class="text-label" fill="#4CAF50">Tier 4: High Importance + Recent</text>
|
|
131
|
+
<text x="530" y="515" class="text-small">Kept (Evicted Last)</text>
|
|
132
|
+
<text x="530" y="540" class="text-tiny">importance: 9.0, age: 1 hour → kept longest</text>
|
|
133
|
+
<text x="530" y="555" class="text-tiny">importance: 10.0, age: 5 min → never evicted (if possible)</text>
|
|
134
|
+
<text x="530" y="570" class="text-tiny">Examples: Critical + actively used data</text>
|
|
135
|
+
|
|
136
|
+
<!-- Key Features -->
|
|
137
|
+
<text x="50" y="410" class="text-label">Guarantees</text>
|
|
138
|
+
|
|
139
|
+
<rect x="50" y="425" width="170" height="140" fill="rgba(33, 150, 243, 0.15)" stroke="#2196F3" stroke-width="1.5" rx="3"/>
|
|
140
|
+
<text x="60" y="450" class="text-small">✓ Never-forget principle</text>
|
|
141
|
+
<text x="60" y="470" class="text-small">✓ Preserved in long-term</text>
|
|
142
|
+
<text x="60" y="490" class="text-small">✓ Can be recalled</text>
|
|
143
|
+
<text x="60" y="510" class="text-small">✓ No data loss</text>
|
|
144
|
+
<text x="60" y="530" class="text-small">✓ Deterministic order</text>
|
|
145
|
+
<text x="60" y="550" class="text-small">✓ O(n log n) complexity</text>
|
|
146
|
+
|
|
147
|
+
<rect x="240" y="425" width="190" height="140" fill="rgba(33, 150, 243, 0.15)" stroke="#2196F3" stroke-width="1.5" rx="3"/>
|
|
148
|
+
<text x="250" y="450" class="text-small">Performance:</text>
|
|
149
|
+
<text x="250" y="470" class="text-tiny">• Sort: O(n log n)</text>
|
|
150
|
+
<text x="250" y="485" class="text-tiny">• Eviction: O(k) greedy</text>
|
|
151
|
+
<text x="250" y="500" class="text-tiny">• Typical: <10ms</text>
|
|
152
|
+
<text x="250" y="515" class="text-tiny">• Memory: O(n) temp</text>
|
|
153
|
+
<text x="250" y="530" class="text-tiny">• Stops when enough</text>
|
|
154
|
+
<text x="250" y="545" class="text-tiny"> space freed</text>
|
|
155
|
+
|
|
156
|
+
<!-- Example -->
|
|
157
|
+
<text x="50" y="600" class="text-label">Example: Need 5,000 tokens</text>
|
|
158
|
+
|
|
159
|
+
<rect x="50" y="615" width="850" height="110" fill="rgba(33, 150, 243, 0.1)" stroke="#2196F3" stroke-width="1" rx="3"/>
|
|
160
|
+
|
|
161
|
+
<text x="60" y="635" class="text-tiny">Working memory contains:</text>
|
|
162
|
+
<text x="70" y="655" class="text-tiny" fill="#F44336">• "random_note" (imp: 1.0, 2000 tok, 1 hr ago) → EVICTED (Tier 2)</text>
|
|
163
|
+
<text x="70" y="670" class="text-tiny" fill="#F44336">• "debug_log" (imp: 2.0, 1500 tok, 2 days ago) → EVICTED (Tier 1)</text>
|
|
164
|
+
<text x="70" y="685" class="text-tiny" fill="#F44336">• "temp_calc" (imp: 1.5, 1600 tok, 5 days ago) → EVICTED (Tier 1)</text>
|
|
165
|
+
<text x="70" y="700" class="text-tiny" fill="#4CAF50">• "user_pref" (imp: 8.0, 100 tok, 5 days ago) → KEPT (Tier 3)</text>
|
|
166
|
+
<text x="70" y="715" class="text-tiny" fill="#4CAF50">• "architecture_decision" (imp: 10.0, 3000 tok, 3 days ago) → KEPT (Tier 3)</text>
|
|
167
|
+
</svg>
|
|
168
|
+
|
|
169
|
+
### Eviction Algorithm
|
|
170
|
+
|
|
171
|
+
```ruby
|
|
172
|
+
def evict_to_make_space(needed_tokens)
|
|
173
|
+
# Sort by [importance, -recency]
|
|
174
|
+
# Lower importance evicted first
|
|
175
|
+
# Within same importance, older memories evicted first
|
|
176
|
+
candidates = @nodes.sort_by do |key, node|
|
|
177
|
+
recency = Time.now - node[:added_at]
|
|
178
|
+
[node[:importance], -recency]
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# Evict from front until enough space
|
|
182
|
+
evicted = []
|
|
183
|
+
tokens_freed = 0
|
|
184
|
+
|
|
185
|
+
candidates.each do |key, node|
|
|
186
|
+
break if tokens_freed >= needed_tokens
|
|
187
|
+
|
|
188
|
+
evicted << { key: key, value: node[:value] }
|
|
189
|
+
tokens_freed += node[:token_count]
|
|
190
|
+
@nodes.delete(key)
|
|
191
|
+
@access_order.delete(key)
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
evicted
|
|
195
|
+
end
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
### Eviction Scoring
|
|
199
|
+
|
|
200
|
+
**Primary sort**: Importance (ascending)
|
|
201
|
+
|
|
202
|
+
- Importance 1.0 evicted before importance 5.0
|
|
203
|
+
- Importance 5.0 evicted before importance 10.0
|
|
204
|
+
|
|
205
|
+
**Secondary sort**: Recency (descending age)
|
|
206
|
+
|
|
207
|
+
- Within same importance, older memories evicted first
|
|
208
|
+
- Added 5 days ago evicted before added 1 hour ago
|
|
209
|
+
|
|
210
|
+
### Never-Forget Guarantee
|
|
211
|
+
|
|
212
|
+
!!! tip "Critical Design Principle"
|
|
213
|
+
Evicted memories are NOT deleted. They:
|
|
214
|
+
|
|
215
|
+
1. Remain in long-term memory (PostgreSQL) permanently
|
|
216
|
+
2. Can be recalled via `recall()` when needed
|
|
217
|
+
3. Marked as `in_working_memory = FALSE` in database
|
|
218
|
+
4. Automatically reloaded if recalled again
|
|
219
|
+
|
|
220
|
+
---
|
|
221
|
+
|
|
222
|
+
## Rationale
|
|
223
|
+
|
|
224
|
+
### Why Hybrid (Importance + Recency)?
|
|
225
|
+
|
|
226
|
+
**Importance alone is insufficient**:
|
|
227
|
+
|
|
228
|
+
- All memories might have same importance
|
|
229
|
+
- Old important memories eventually become stale
|
|
230
|
+
- Doesn't account for temporal relevance
|
|
231
|
+
|
|
232
|
+
**Recency alone is insufficient**:
|
|
233
|
+
|
|
234
|
+
- Critical information gets evicted just because it's old
|
|
235
|
+
- Architectural decisions disappear after time passes
|
|
236
|
+
- Loses important long-term context
|
|
237
|
+
|
|
238
|
+
**Hybrid balances both**:
|
|
239
|
+
|
|
240
|
+
- Low-importance recent memories evicted before high-importance old ones
|
|
241
|
+
- Within same importance, temporal relevance matters
|
|
242
|
+
- Preserves critical knowledge while making space for new context
|
|
243
|
+
|
|
244
|
+
### Why This Sort Order?
|
|
245
|
+
|
|
246
|
+
```ruby
|
|
247
|
+
[node[:importance], -recency]
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
This creates tiers:
|
|
251
|
+
|
|
252
|
+
1. **Tier 1 (evict first)**: Low importance, old
|
|
253
|
+
2. **Tier 2**: Low importance, recent
|
|
254
|
+
3. **Tier 3**: High importance, old
|
|
255
|
+
4. **Tier 4 (evict last)**: High importance, recent
|
|
256
|
+
|
|
257
|
+
Example eviction order:
|
|
258
|
+
|
|
259
|
+
```
|
|
260
|
+
importance: 1.0, age: 5 days → evicted first
|
|
261
|
+
importance: 1.0, age: 1 hour → evicted second
|
|
262
|
+
importance: 5.0, age: 5 days → evicted third
|
|
263
|
+
importance: 5.0, age: 1 hour → evicted fourth
|
|
264
|
+
importance: 10.0, age: 5 days → evicted fifth
|
|
265
|
+
importance: 10.0, age: 1 hour → kept (evict last)
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
### Eviction Guarantees
|
|
269
|
+
|
|
270
|
+
**Safety**:
|
|
271
|
+
|
|
272
|
+
- Evicted memories preserved in long-term storage
|
|
273
|
+
- No data loss, only working memory removal
|
|
274
|
+
- Can be recalled when needed
|
|
275
|
+
|
|
276
|
+
**Performance**:
|
|
277
|
+
|
|
278
|
+
- Greedy eviction: stops as soon as enough space freed
|
|
279
|
+
- O(n log n) sorting (one-time cost)
|
|
280
|
+
- O(k) eviction where k = nodes evicted
|
|
281
|
+
- Typical eviction: < 10ms for 100-node working memory
|
|
282
|
+
|
|
283
|
+
**Correctness**:
|
|
284
|
+
|
|
285
|
+
- Always frees enough tokens (or evicts all nodes trying)
|
|
286
|
+
- Respects importance ordering
|
|
287
|
+
- Deterministic (same state → same evictions)
|
|
288
|
+
|
|
289
|
+
---
|
|
290
|
+
|
|
291
|
+
## Consequences
|
|
292
|
+
|
|
293
|
+
### Positive
|
|
294
|
+
|
|
295
|
+
- Preserves important context: High-importance memories stay longer
|
|
296
|
+
- Temporal relevance: Recent context preferred over old
|
|
297
|
+
- Never forgets: Evicted memories remain in long-term storage
|
|
298
|
+
- Predictable: Clear eviction order based on importance + recency
|
|
299
|
+
- Fast: O(n log n) sort, greedy eviction
|
|
300
|
+
- Greedy: Only evicts what's necessary, no over-eviction
|
|
301
|
+
- Safe: No data loss, recallable from long-term memory
|
|
302
|
+
|
|
303
|
+
### Negative
|
|
304
|
+
|
|
305
|
+
- No access frequency: Doesn't track how often memory is used
|
|
306
|
+
- No semantic clustering: May split related memories
|
|
307
|
+
- Importance subjectivity: Relies on user-assigned importance
|
|
308
|
+
- Batch eviction cost: O(n log n) sort on every eviction
|
|
309
|
+
- No look-ahead: Doesn't predict which memories will be needed soon
|
|
310
|
+
|
|
311
|
+
### Neutral
|
|
312
|
+
|
|
313
|
+
- Importance matters: Users must assign meaningful importance scores
|
|
314
|
+
- Eviction visibility: No notification when memories evicted
|
|
315
|
+
- Recall overhead: Need to recall evicted memories if needed again
|
|
316
|
+
|
|
317
|
+
---
|
|
318
|
+
|
|
319
|
+
## Use Cases
|
|
320
|
+
|
|
321
|
+
### Use Case 1: Adding Large Memory to Full Working Memory
|
|
322
|
+
|
|
323
|
+
```ruby
|
|
324
|
+
# Working memory: 127,500 / 128,000 tokens (99% full)
|
|
325
|
+
# Attempt to add 5,000 token memory
|
|
326
|
+
|
|
327
|
+
htm.add_node("new_large_memory", large_text, importance: 7.0)
|
|
328
|
+
|
|
329
|
+
# Eviction needed: 5,000 - 500 = 4,500 tokens
|
|
330
|
+
|
|
331
|
+
# Working memory contains:
|
|
332
|
+
# - "user_pref" (importance: 8.0, 100 tokens, 5 days old)
|
|
333
|
+
# - "random_note" (importance: 1.0, 2,000 tokens, 1 hour ago)
|
|
334
|
+
# - "architecture_decision" (importance: 10.0, 3,000 tokens, 3 days ago)
|
|
335
|
+
# - "debug_log" (importance: 2.0, 1,500 tokens, 2 days ago)
|
|
336
|
+
|
|
337
|
+
# Eviction order:
|
|
338
|
+
# 1. "random_note" (importance: 1.0) → 2,000 tokens freed
|
|
339
|
+
# 2. "debug_log" (importance: 2.0) → 3,500 tokens freed
|
|
340
|
+
# 3. Stop (4,500 > needed)
|
|
341
|
+
|
|
342
|
+
# Result: user_pref and architecture_decision preserved
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
### Use Case 2: Importance Ties
|
|
346
|
+
|
|
347
|
+
```ruby
|
|
348
|
+
# All memories have importance: 5.0
|
|
349
|
+
|
|
350
|
+
# Working memory:
|
|
351
|
+
# - "note_1" (5 days old, 1,000 tokens)
|
|
352
|
+
# - "note_2" (3 days old, 1,000 tokens)
|
|
353
|
+
# - "note_3" (1 hour ago, 1,000 tokens)
|
|
354
|
+
|
|
355
|
+
# Need to evict 2,000 tokens
|
|
356
|
+
|
|
357
|
+
# Eviction order:
|
|
358
|
+
# 1. "note_1" (oldest) → 1,000 tokens
|
|
359
|
+
# 2. "note_2" (second oldest) → 2,000 tokens
|
|
360
|
+
# 3. Stop
|
|
361
|
+
|
|
362
|
+
# Result: Most recent note preserved
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
### Use Case 3: Recall Evicted Memory
|
|
366
|
+
|
|
367
|
+
```ruby
|
|
368
|
+
# Memory evicted from working memory
|
|
369
|
+
htm.add_node("temp_note", "Some temporary information", importance: 1.0)
|
|
370
|
+
# ... later evicted to make space ...
|
|
371
|
+
|
|
372
|
+
# Later: recall the evicted memory
|
|
373
|
+
memories = htm.recall(timeframe: "last week", topic: "temporary information")
|
|
374
|
+
|
|
375
|
+
# Result: Memory retrieved from long-term storage
|
|
376
|
+
# And automatically added back to working memory
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
### Use Case 4: High-Importance Old Memory
|
|
380
|
+
|
|
381
|
+
```ruby
|
|
382
|
+
# Critical decision made months ago
|
|
383
|
+
htm.add_node("critical_decision",
|
|
384
|
+
"We must never use MongoDB for time-series data",
|
|
385
|
+
importance: 10.0)
|
|
386
|
+
|
|
387
|
+
# ... 90 days later, working memory full ...
|
|
388
|
+
|
|
389
|
+
# Many recent low-importance memories added
|
|
390
|
+
# Critical decision still in working memory due to importance: 10.0
|
|
391
|
+
|
|
392
|
+
# Eviction: Low-importance recent memories evicted first
|
|
393
|
+
# Result: Critical decision preserved despite being 90 days old
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
---
|
|
397
|
+
|
|
398
|
+
## Performance Characteristics
|
|
399
|
+
|
|
400
|
+
### Time Complexity
|
|
401
|
+
|
|
402
|
+
- **Sorting**: O(n log n) where n = nodes in working memory
|
|
403
|
+
- **Eviction**: O(k) where k = nodes evicted
|
|
404
|
+
- **Total**: O(n log n + k) ≈ O(n log n)
|
|
405
|
+
|
|
406
|
+
### Space Complexity
|
|
407
|
+
|
|
408
|
+
- **Sorted candidates**: O(n) temporary array
|
|
409
|
+
- **Evicted list**: O(k) returned result
|
|
410
|
+
- **Total**: O(n) additional memory
|
|
411
|
+
|
|
412
|
+
### Typical Performance
|
|
413
|
+
|
|
414
|
+
- **Working memory size**: 50-200 nodes
|
|
415
|
+
- **Eviction frequency**: Low (most additions fit without eviction)
|
|
416
|
+
- **Sort time**: < 5ms for 200 nodes
|
|
417
|
+
- **Eviction time**: < 1ms (greedy, stops early)
|
|
418
|
+
- **Total**: < 10ms per eviction event
|
|
419
|
+
|
|
420
|
+
### Optimization Opportunities
|
|
421
|
+
|
|
422
|
+
- **Cache sorted order**: Invalidate on add/remove
|
|
423
|
+
- **Incremental sort**: Use heap for O(k log n) eviction
|
|
424
|
+
- **Lazy eviction**: Evict only when space check fails
|
|
425
|
+
|
|
426
|
+
---
|
|
427
|
+
|
|
428
|
+
## Design Decisions
|
|
429
|
+
|
|
430
|
+
### Decision: Hybrid (Importance + Recency) vs Pure Policies
|
|
431
|
+
|
|
432
|
+
**Rationale**: Balances competing priorities better than any single factor
|
|
433
|
+
|
|
434
|
+
**Alternative**: Pure LRU (least recently used)
|
|
435
|
+
|
|
436
|
+
**Rejected**: Loses important long-term context
|
|
437
|
+
|
|
438
|
+
**Alternative**: Pure importance-based
|
|
439
|
+
|
|
440
|
+
**Rejected**: All old memories evicted regardless of importance
|
|
441
|
+
|
|
442
|
+
### Decision: Primary Sort by Importance
|
|
443
|
+
|
|
444
|
+
**Rationale**: Importance is the stronger signal for retention
|
|
445
|
+
|
|
446
|
+
**Alternative**: Primary sort by recency
|
|
447
|
+
|
|
448
|
+
**Rejected**: Would evict critical old memories before trivial recent ones
|
|
449
|
+
|
|
450
|
+
### Decision: Greedy Eviction (Stop When Enough Space)
|
|
451
|
+
|
|
452
|
+
**Rationale**: Minimize evictions, preserve as much context as possible
|
|
453
|
+
|
|
454
|
+
**Alternative**: Evict to some threshold (e.g., 80% full)
|
|
455
|
+
|
|
456
|
+
**Rejected**: Unnecessary evictions, reduces available context
|
|
457
|
+
|
|
458
|
+
### Decision: No Access Frequency Tracking
|
|
459
|
+
|
|
460
|
+
**Rationale**: Simplicity, access patterns not stable in LLM workflows
|
|
461
|
+
|
|
462
|
+
**Alternative**: LFU (Least Frequently Used)
|
|
463
|
+
|
|
464
|
+
**Deferred**: Can add access_count tracking if real-world usage shows value
|
|
465
|
+
|
|
466
|
+
### Decision: Evicted Memories Preserved in Long-Term
|
|
467
|
+
|
|
468
|
+
**Rationale**: Never-forget philosophy, safety, recallability
|
|
469
|
+
|
|
470
|
+
**Alternative**: Truly delete evicted memories
|
|
471
|
+
|
|
472
|
+
**Rejected**: Violates never-forget principle, data loss
|
|
473
|
+
|
|
474
|
+
---
|
|
475
|
+
|
|
476
|
+
## Risks and Mitigations
|
|
477
|
+
|
|
478
|
+
### Risk: Batch Eviction Cost
|
|
479
|
+
|
|
480
|
+
!!! info "Risk"
|
|
481
|
+
O(n log n) sort on every eviction is expensive
|
|
482
|
+
|
|
483
|
+
**Likelihood**: Low (evictions infrequent, n is small)
|
|
484
|
+
|
|
485
|
+
**Impact**: Low (< 10ms for typical working memory)
|
|
486
|
+
|
|
487
|
+
**Mitigation**:
|
|
488
|
+
|
|
489
|
+
- Working memory stays small (128K tokens ≈ 50-200 nodes)
|
|
490
|
+
- Sort only when eviction needed
|
|
491
|
+
- Consider heap-based eviction if profiling shows bottleneck
|
|
492
|
+
|
|
493
|
+
### Risk: Importance Scoring Inconsistency
|
|
494
|
+
|
|
495
|
+
!!! warning "Risk"
|
|
496
|
+
Users assign arbitrary importance, breaks eviction quality
|
|
497
|
+
|
|
498
|
+
**Likelihood**: Medium (subjective scoring)
|
|
499
|
+
|
|
500
|
+
**Impact**: Medium (suboptimal evictions)
|
|
501
|
+
|
|
502
|
+
**Mitigation**:
|
|
503
|
+
|
|
504
|
+
- Document importance scoring guidelines
|
|
505
|
+
- Provide examples of importance ranges
|
|
506
|
+
- Default importance: 1.0 for most memories
|
|
507
|
+
|
|
508
|
+
### Risk: No Access Frequency Signal
|
|
509
|
+
|
|
510
|
+
!!! info "Risk"
|
|
511
|
+
Frequently accessed memories evicted if old and low importance
|
|
512
|
+
|
|
513
|
+
**Likelihood**: Low (frequently accessed → likely important or recent)
|
|
514
|
+
|
|
515
|
+
**Impact**: Low (can recall from long-term)
|
|
516
|
+
|
|
517
|
+
**Mitigation**:
|
|
518
|
+
|
|
519
|
+
- Monitor real-world eviction patterns
|
|
520
|
+
- Add access_count tracking if needed
|
|
521
|
+
|
|
522
|
+
### Risk: Related Memories Split
|
|
523
|
+
|
|
524
|
+
!!! info "Risk"
|
|
525
|
+
Semantically related memories evicted separately
|
|
526
|
+
|
|
527
|
+
**Likelihood**: Medium (no clustering)
|
|
528
|
+
|
|
529
|
+
**Impact**: Low (can recall together with topic search)
|
|
530
|
+
|
|
531
|
+
**Mitigation**:
|
|
532
|
+
|
|
533
|
+
- Use relationships to co-recall related memories
|
|
534
|
+
- Consider cluster-aware eviction in future
|
|
535
|
+
|
|
536
|
+
---
|
|
537
|
+
|
|
538
|
+
## Future Enhancements
|
|
539
|
+
|
|
540
|
+
### Access Frequency Tracking
|
|
541
|
+
|
|
542
|
+
```ruby
|
|
543
|
+
def add(key, value, ...)
|
|
544
|
+
@nodes[key] = {
|
|
545
|
+
...,
|
|
546
|
+
access_count: 0
|
|
547
|
+
}
|
|
548
|
+
end
|
|
549
|
+
|
|
550
|
+
def evict_to_make_space(needed_tokens)
|
|
551
|
+
candidates = @nodes.sort_by do |key, node|
|
|
552
|
+
recency = Time.now - node[:added_at]
|
|
553
|
+
access_freq = node[:access_count]
|
|
554
|
+
|
|
555
|
+
# Lower score evicted first
|
|
556
|
+
score = node[:importance] * (1 + Math.log(1 + access_freq))
|
|
557
|
+
[score, -recency]
|
|
558
|
+
end
|
|
559
|
+
end
|
|
560
|
+
```
|
|
561
|
+
|
|
562
|
+
### Cluster-Aware Eviction
|
|
563
|
+
|
|
564
|
+
```ruby
|
|
565
|
+
# Keep related memories together
|
|
566
|
+
def evict_to_make_space(needed_tokens)
|
|
567
|
+
# Identify memory clusters
|
|
568
|
+
clusters = identify_clusters(@nodes)
|
|
569
|
+
|
|
570
|
+
# Evict entire clusters to preserve coherence
|
|
571
|
+
clusters.sort_by { |c| cluster_score(c) }.each do |cluster|
|
|
572
|
+
# Evict full cluster
|
|
573
|
+
end
|
|
574
|
+
end
|
|
575
|
+
```
|
|
576
|
+
|
|
577
|
+
### Lazy Eviction
|
|
578
|
+
|
|
579
|
+
```ruby
|
|
580
|
+
# Don't evict until actually assembling context
|
|
581
|
+
def assemble_context(strategy:, max_tokens:)
|
|
582
|
+
# Only NOW evict if needed
|
|
583
|
+
evict_to_fit(max_tokens) if token_count > max_tokens
|
|
584
|
+
end
|
|
585
|
+
```
|
|
586
|
+
|
|
587
|
+
### Predicted Need Scoring
|
|
588
|
+
|
|
589
|
+
```ruby
|
|
590
|
+
# Use LLM or heuristics to predict which memories will be needed
|
|
591
|
+
def evict_to_make_space(needed_tokens)
|
|
592
|
+
candidates = @nodes.sort_by do |key, node|
|
|
593
|
+
predicted_need = predict_future_access(node) # ML model or heuristics
|
|
594
|
+
[node[:importance] * predicted_need, -recency]
|
|
595
|
+
end
|
|
596
|
+
end
|
|
597
|
+
```
|
|
598
|
+
|
|
599
|
+
### Configurable Eviction Policy
|
|
600
|
+
|
|
601
|
+
```ruby
|
|
602
|
+
htm = HTM.new(
|
|
603
|
+
eviction_policy: :lru # or :importance, :hybrid, :custom
|
|
604
|
+
)
|
|
605
|
+
```
|
|
606
|
+
|
|
607
|
+
---
|
|
608
|
+
|
|
609
|
+
## Alternatives Comparison
|
|
610
|
+
|
|
611
|
+
| Policy | Pros | Cons | Decision |
|
|
612
|
+
|--------|------|------|----------|
|
|
613
|
+
| **Hybrid (Importance + Recency)** | **Balanced retention** | **Requires importance** | **ACCEPTED** |
|
|
614
|
+
| Pure LRU | Standard caching, temporal | Loses important context | Rejected |
|
|
615
|
+
| Pure Importance | Preserves critical info | No temporal relevance | Rejected |
|
|
616
|
+
| LFU | Captures access patterns | New memories always evicted | Rejected |
|
|
617
|
+
| Random | Simple, no sorting | Unpredictable, risky | Rejected |
|
|
618
|
+
| Learned Policy | Optimal for patterns | Complex, non-deterministic | Deferred |
|
|
619
|
+
|
|
620
|
+
---
|
|
621
|
+
|
|
622
|
+
## References
|
|
623
|
+
|
|
624
|
+
- [Cache Replacement Policies](https://en.wikipedia.org/wiki/Cache_replacement_policies)
|
|
625
|
+
- [LRU Cache](https://en.wikipedia.org/wiki/Cache_replacement_policies#Least_recently_used_(LRU))
|
|
626
|
+
- [Working Memory Management](https://en.wikipedia.org/wiki/Working_memory)
|
|
627
|
+
- [Multi-Level Memory Hierarchies](https://en.wikipedia.org/wiki/Memory_hierarchy)
|
|
628
|
+
- [ADR-002: Two-Tier Memory](002-two-tier-memory.md)
|
|
629
|
+
- [ADR-006: Context Assembly](006-context-assembly.md)
|
|
630
|
+
- [ADR-009: Never-Forget Philosophy](009-never-forget.md)
|
|
631
|
+
- [Working Memory Guide](../../guides/working-memory.md)
|
|
632
|
+
|
|
633
|
+
---
|
|
634
|
+
|
|
635
|
+
## Review Notes
|
|
636
|
+
|
|
637
|
+
**Systems Architect**: Hybrid eviction is the right choice. Consider heap-based eviction for better asymptotic complexity.
|
|
638
|
+
|
|
639
|
+
**Performance Specialist**: O(n log n) is acceptable for n < 200. Monitor real-world eviction frequency.
|
|
640
|
+
|
|
641
|
+
**Domain Expert**: Never-forget guarantee is essential. Eviction is just working memory management.
|
|
642
|
+
|
|
643
|
+
**AI Engineer**: Importance + recency works well for LLM context. Consider learned eviction in future.
|
|
644
|
+
|
|
645
|
+
**Ruby Expert**: Clean implementation. Consider extracting eviction policy to strategy pattern for extensibility.
|