Compare commits

..

10 Commits

Author SHA1 Message Date
root
866079884f fix: add PYTHONUNBUFFERED=1 for real-time logging
- Python buffers stdout when running as systemd service (no TTY)
- This prevented journalctl from showing real-time turn captures
- Added Environment='PYTHONUNBUFFERED=1' to disable buffering

Fixes issue where watcher captured turns but only logged on restart.
2026-03-13 13:13:53 -05:00
root
62953e9f39 docs: simplify README, update validation and curator docs 2026-03-10 12:08:53 -05:00
root
08aaddb4d0 fix: Add tr-worker files, sanitize IPs, update validation checklists
- Add realtime_qdrant_watcher.py and mem-qdrant-watcher.service to tr-worker/
- Sanitize private IPs (10.0.0.x → <QDRANT_IP>, <OLLAMA_IP>)
- Replace absolute paths with placeholders
- Add GIT_VALIDATION_CHECK.md for security validation
- Update validation checklists to v2.4
- Remove session.md from git (local-only file)
2026-02-26 08:28:12 -06:00
root
df3b347d65 Add gem quality & model intelligence section
- Documented how gem quality improves with model size
- Added comparison table (7B vs 30B vs 70B+)
- Provided example gem JSON
- Added recommendation for production models
2026-02-25 13:34:32 -06:00
root
a1cc5b5477 Add Needed Improvements section to all docs
Documented 6 areas needing improvement:
- Semantic Deduplication (High)
- Search Result Deduplication (Medium)
- Gem Quality Scoring (Medium)
- Temporal Decay (Low)
- Gem Merging/Updating (Low)
- Importance Calibration (Low)
2026-02-25 13:34:12 -06:00
root
5950fdd09b Final fixes: first-person gems, threshold 0.5, hidden context injection
- Changed gem format from third-person to first-person for better query matching
- Lowered minRecallScore from 0.7 to 0.5
- Fixed context injection to use HTML comments (hidden from UI)
- Updated all documentation with today's fixes
2026-02-25 13:30:13 -06:00
root
87a390901d Update docs: watcher fix, plugin capture fix (2026-02-25)
- Fixed watcher stuck on old session bug (restarted service)
- Fixed plugin capture 0 exchanges (added extractMessageText for OpenAI content arrays)
- Updated README, session.md, function_check.md, audit_checklist.md
- Verified: 9 exchanges captured per session
2026-02-25 12:45:27 -06:00
root
abc5498f60 docs: Add Known Issues section to checklist
Document all issues discovered during TrueRecall v2 deployment:
- Gem quality issues (malformed fields, duplicates, 4b vs 30b)
- OpenClaw UI glitches during compaction
- v1 migration conflicts and detection
- Security issues (IPs, paths, backup files)
- Multi-remote git configuration
- Model-specific issues
- Pre-release checklist for future deployments
2026-02-24 21:43:57 -06:00
root
a22e6f095a chore: Remove unnecessary files and folders
Removed:
- session.md (user-specific session notes)
- migrate_memories.py (one-time migration script)
- test_curator.py (test file)
- __pycache__/ (Python cache)
- tr-compact/ (v1 deprecated)
- tr-daily/ (v1 deprecated)
- tr-worker/ (empty)
- shared/ (empty)
- tr-continuous/migrate_add_curated.py
- tr-continuous/curator_by_count.py
- tr-continuous/curator_turn_based.py
- tr-continuous/curator_cron.sh
- tr-continuous/turn-curator.service
- tr-continuous/README.md (redundant)

Remaining core files:
- README.md, checklist.md, curator-prompt.md
- install.py, push-all.sh, .gitignore
- tr-continuous/curator_timer.py
- tr-continuous/curator_config.json
2026-02-24 21:42:48 -06:00
root
198334c0b4 chore: Remove backup files and add .gitignore
- Remove tracked backup files (*.bak, *.neuralstream.bak)
- Remove debug_curator.py
- Add comprehensive .gitignore
- Prevents future backup/debug files from being committed
2026-02-24 21:40:42 -06:00
31 changed files with 1505 additions and 4068 deletions

65
.gitignore vendored Normal file
View File

@@ -0,0 +1,65 @@
# TrueRecall v2 - .gitignore
# Files that should not be committed to version control
# Backup files
*.bak
*.bak.*
*~
*.swp
*.swo
*.orig
*.save
# Debug and temporary files
debug_*
test_*
temp_*
*.tmp
# Log files
*.log
logs/
log/
# Python cache
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
# Virtual environments
venv/
env/
ENV/
# IDE/editor files
.vscode/
.idea/
*.iml
# OS files
.DS_Store
Thumbs.db
# Environment/config with secrets (if they exist locally)
.env
.env.local
config.local.json
# Local data that shouldn't be shared
data/
datasets/
*.db
*.sqlite
# Build artifacts
build/
dist/
*.egg-info/
# Session and validation files (local only)
session.md
VALIDATION_*.md
audit_results_*.md
CONTEXT_INJECTION_*.md

113
GIT_VALIDATION_CHECK.md Normal file
View File

@@ -0,0 +1,113 @@
# TrueRecall v2 - Git Validation Checklist
**Environment:** Git Repository (`.git_projects/true-recall-gems/`)
**Purpose:** Validate git-ready directory for public sharing
**Version:** 2.4
**Last Updated:** 2026-02-26
---
## Overview
This checklist validates the **git repository** where **NO sensitive data** should exist. All private information must be sanitized before sharing.
**Key Principle:** In git, placeholders required:
- ❌ NO real private IPs (10.0.0.x, 192.168.x.x)
- ❌ NO absolute paths (/root/, /home/username/)
- ❌ NO real user IDs or credentials
- ✅ Use placeholders: `<QDRANT_IP>`, `<OLLAMA_IP>`, `~/.openclaw/`
---
## Current Configuration (Sanitized for Git)
| Service | Placeholder | Default Port |
|---------|-------------|---------------|
| Qdrant | `<QDRANT_IP>` | 6333 |
| Ollama | `<OLLAMA_IP>` | 11434 |
| Redis | `<REDIS_IP>` | 6379 |
| Gateway | `<GATEWAY_IP>` | 18789 |
| Gitea | `<GITEA_IP>` | 3000 |
---
## SECTION 1: Critical Security Checks (MUST PASS)
### 1.1 Private IP Addresses (FORBIDDEN in Git)
| # | Check | Status |
|---|-------|--------|
| 1.1.1 | No 10.x.x.x IPs | ✅ PASS |
| 1.1.2 | No 192.168.x.x IPs | ✅ PASS |
| 1.1.3 | No 172.16-31.x.x IPs | ✅ PASS |
**Verification:**
```bash
grep -rE '10\.[0-9]+\.[0-9]+\.[0-9]+' --include="*.py" --include="*.md" .
```
### 1.2 Absolute Paths (FORBIDDEN in Git)
| # | Check | Status |
|---|-------|--------|
| 1.2.1 | No /root/ paths | ✅ PASS |
| 1.2.2 | No /home/[user]/ paths | ✅ PASS |
**Verification:**
```bash
grep -rE '/root/|/home/[a-z]+/' --include="*.py" --include="*.md" .
```
### 1.3 Credentials & Secrets (FORBIDDEN in Git)
| # | Check | Status |
|---|-------|--------|
| 1.3.1 | No passwords | ✅ PASS |
| 1.3.2 | No API tokens | ✅ PASS |
| 1.3.3 | No private keys | ✅ PASS |
---
## SECTION 2: Files & Structure
### 2.1 Required Files
| File | Status |
|------|--------|
| README.md | ✅ Present (sanitized) |
| curator_timer.py | ✅ Present (sanitized) |
| curator_config.json | ✅ Present |
| .gitignore | ✅ Present (updated) |
### 2.2 Files NOT in Git (Local Only)
| File | Expected |
|------|----------|
| session.md | ❌ Not in git |
| VALIDATION_*.md | ❌ Not in git |
| audit_results_*.md | ❌ Not in git |
---
## SECTION 3: Placeholder Verification
| File | QDRANT_IP | OLLAMA_IP | ~/.openclaw |
|------|-----------|-----------|--------------|
| README.md | ✅ | ✅ | ✅ |
| curator_timer.py | ✅ | ✅ | ✅ |
---
## Validation Summary
- ✅ No private IPs found
- ✅ No absolute paths (/root/)
- ✅ No credentials/secrets
- ✅ Placeholders used correctly
- ✅ .gitignore updated
**Status:** ✅ READY FOR COMMIT
---
*Last validated: 2026-02-26 08:30 CST*

763
README.md
View File

@@ -1,682 +1,147 @@
# TrueRecall v2
# TrueRecall Gems (v2)
**Project:** Gem extraction and memory recall system
**Status:** ✅ Active & Verified
**Location:** `~/.openclaw/workspace/.projects/true-recall-v2/`
**Last Updated:** 2026-02-24 19:02 CST
**Purpose:** Memory curation (gems) + context injection
---
## Table of Contents
- [Quick Start](#quick-start)
- [Overview](#overview)
- [Current State](#current-state)
- [Architecture](#architecture)
- [Components](#components)
- [Files & Locations](#files--locations)
- [Configuration](#configuration)
- [Validation](#validation)
- [Troubleshooting](#troubleshooting)
- [Status Summary](#status-summary)
---
## Quick Start
```bash
# Check system status
openclaw status
sudo systemctl status mem-qdrant-watcher
# View recent captures
curl -s http://<QDRANT_IP>:6333/collections/memories_tr | jq '.result.points_count'
# Check collections
curl -s http://<QDRANT_IP>:6333/collections | jq '.result.collections[].name'
```
**Status:** ⚠️ Requires true-recall-base to be installed first
---
## Overview
TrueRecall v2 extracts "gems" (key insights) from conversations and injects them as context. It consists of three layers:
TrueRecall Gems adds **curation** and **injection** on top of Base's capture foundation.
1. **Capture** — Real-time watcher saves every turn to `memories_tr`
2. **Curation** — Daily curator extracts gems to `gems_tr`
3. **Injection** — Plugin searches `gems_tr` and injects gems per turn
**Gems is an ADDON:**
- Requires true-recall-base
- Independent from openclaw-true-recall-blocks
- Choose Gems OR Blocks, not both
---
## Current State
### Verified at 19:02 CST
| Collection | Points | Purpose | Status |
|------------|--------|---------|--------|
| `memories_tr` | **12,378** | Full text (live capture) | ✅ Active |
| `gems_tr` | **5** | Curated gems (injection) | ✅ Active |
**All memories tagged with `curated: false` for timer curation.**
### Services Status
| Service | Status | Details |
|---------|--------|---------|
| `mem-qdrant-watcher` | ✅ Active | PID 1748, capturing |
| Timer curator | ✅ Deployed | Every 30 min via cron |
| OpenClaw Gateway | ✅ Running | Version 2026.2.23 |
| memory-qdrant plugin | ✅ Loaded | recall: gems_tr |
---
## Comparison: TrueRecall v2 vs Jarvis Memory vs v1
| Feature | Jarvis Memory | TrueRecall v1 | TrueRecall v2 |
|---------|---------------|---------------|---------------|
| **Storage** | Redis | Redis + Qdrant | Qdrant only |
| **Capture** | Session batch | Session batch | Real-time |
| **Curation** | Manual | Daily 2:45 AM | Timer (5 min) |
| **Embedding** | — | snowflake | snowflake + mxbai |
| **Curator LLM** | — | qwen3:4b | qwen3:30b |
| **State tracking** | — | — | `curated` tag |
| **Batch size** | — | 24h worth | Configurable |
| **JSON parsing** | — | Fallback needed | Native (30b) |
**Key Improvements v2:**
- ✅ Real-time capture (no batch delay)
- ✅ Timer-based curation (responsive vs daily)
- ✅ 30b curator (better gems, faster ~3s)
-`curated` tag (reliable state tracking)
- ✅ No Redis dependency (simpler stack)
---
## Architecture
### v2.2: Timer-Based Curation
## Three-Tier Architecture
```
┌─────────────────┐ ┌──────────────────────┐ ┌─────────────┐
│ OpenClaw Chat │────▶│ Real-Time Watcher │────▶│ memories_tr │
│ (Session JSONL)│ │ (Python daemon) │ │ (Qdrant) │
└─────────────────┘ └──────────────────────┘ └──────┬──────┘
│ Every 30 min
┌──────────────────┐
│ Timer Curator │
│ (cron/qwen3) │
└────────┬─────────┘
┌──────────────────┐
│ gems_tr │
│ (Qdrant) │
└────────┬─────────┘
Per turn │
┌──────────────────┐
│ memory-qdrant │
│ plugin │
└──────────────────┘
```
true-recall-base (REQUIRED)
├── Watcher daemon
└── memories_tr (raw capture)
└──▶ true-recall-gems (THIS ADDON)
├── Curator extracts gems
├── gems_tr (curated)
└── Plugin injection
**Key Changes in v2.2:**
- ✅ Timer-based curation (30 min intervals)
- ✅ All memories tagged `curated: false` on capture
- ✅ Migration complete (12,378 memories)
- ❌ Removed daily batch processing (2:45 AM)
---
## Components
### 1. Real-Time Watcher
**File:** `skills/qdrant-memory/scripts/realtime_qdrant_watcher.py`
**What it does:**
- Watches `~/.openclaw/agents/main/sessions/*.jsonl`
- Parses each turn (user + AI)
- Embeds with `snowflake-arctic-embed2`
- Stores to `memories_tr` instantly
- **Cleans:** Removes markdown, tables, metadata
**Service:** `mem-qdrant-watcher.service`
**Commands:**
```bash
# Check status
sudo systemctl status mem-qdrant-watcher
# View logs
sudo journalctl -u mem-qdrant-watcher -f
# Restart
sudo systemctl restart mem-qdrant-watcher
Note: Don't install with openclaw-true-recall-blocks.
Choose one addon: Gems OR Blocks.
```
---
### 2. Content Cleaner
## Prerequisites
**File:** `skills/qdrant-memory/scripts/clean_memories_tr.py`
**REQUIRED: Install TrueRecall Base first**
**Purpose:** Batch-clean existing points
**Usage:**
```bash
# Preview changes
python3 clean_memories_tr.py --dry-run
# Clean all
python3 clean_memories_tr.py --execute
# Clean 100 (test)
python3 clean_memories_tr.py --execute --limit 100
```
**Cleans:**
- `**bold**` → plain text
- `|tables|` → removed
- `` `code` `` → plain text
- `---` rules → removed
- `# headers` → removed
---
### 3. Timer Curator
**File:** `tr-continuous/curator_timer.py`
**Schedule:** Every 30 minutes (cron)
**Flow:**
1. Query uncurated memories from `memories_tr`
2. Send batch to qwen3 (max 100)
3. Extract gems → store to `gems_tr`
4. Mark memories as `curated: true`
**Config:** `tr-continuous/curator_config.json`
```json
{
"timer_minutes": 30,
"max_batch_size": 100
}
```
**Logs:** `/var/log/true-recall-timer.log`
---
### 4. Curation Model Comparison
**Current:** `qwen3:4b-instruct`
| Metric | 4b | 30b |
|--------|----|----|
| Speed | ~10-30s per batch | **~3.3s** (tested 2026-02-24) |
| JSON reliability | ⚠️ Needs fallback | ✅ Native |
| Context quality | Basic extraction | ✅ Nuanced |
| Snippet accuracy | ~80% | ✅ Expected: 95%+ |
**30b Benchmark (2026-02-24):**
- Load: 108ms
- Prompt eval: 49ms (1,576 tok/s)
- Generation: 2.9s (233 tokens, 80 tok/s)
- **Total: 3.26s**
**Trade-offs:**
- **4b:** Faster batch processing, lightweight, catches explicit decisions
- **30b:** Deeper context, better inference, ~3x slower but superior quality
**Gem Quality Comparison (Sample Review):**
| Aspect | 4b | 30b |
|--------|----|----|
| **Context depth** | "Extracted via fallback" | Explains *why* decisions were made |
| **Confidence scores** | 0.7-0.85 | 0.9-0.97 |
| **Snippet accuracy** | ~80% (wrong source) | ✅ 95%+ (relevant quotes) |
| **Categories** | Generic "extracted" | Specific: knowledge, technical, decision |
| **Example** | "User implemented BorgBackup" (no context) | "User selected mxbai... due to top MTEB score of 66.5" (explains reasoning) |
**Verdict:** 30b produces significantly higher quality gems — richer context, accurate snippets, and captures architectural intent, not just surface facts.
---
### 5. Semantic Deduplication (Similarity Checking)
**Why:** Smaller models (4b) often extract duplicate or near-duplicate gems. Without checking, your `gems_tr` collection fills with redundant entries.
**The Problem:**
- "User decided on Redis" and "User selected Redis for caching" are the same gem
- Smaller models lack nuance — they extract surface variations as separate gems
- Over time, 30-50% of gems may be duplicates
**Solution: Semantic Similarity Check**
Before inserting a new gem:
1. Embed the candidate gem text
2. Search `gems_tr` for similar embeddings (past 24h)
3. If similarity > 0.85, SKIP (don't insert)
4. If similarity 0.70-0.85, MERGE (update existing with richer context)
5. If similarity < 0.70, INSERT (new unique gem)
**Implementation Options:**
#### Option A: Built-in Curator Check (Recommended)
Modify `curator_timer.py` to add pre-insertion similarity check:
```python
import numpy as np
from qdrant_client import QdrantClient
qdrant = QdrantClient("http://<QDRANT_IP>:6333")
def is_duplicate(gem_text: str, user_id: str = "rob", threshold: float = 0.85) -> bool:
"""Check if similar gem exists in past 24h"""
# Embed the candidate
response = requests.post(
"http://<OLLAMA_IP>:11434/api/embeddings",
json={"model": "mxbai-embed-large", "prompt": gem_text}
)
embedding = response.json()["embedding"]
# Search for similar gems
results = qdrant.search(
collection_name="gems_tr",
query_vector=embedding,
limit=3,
query_filter={
"must": [
{"key": "user_id", "match": {"value": user_id}},
{"key": "timestamp", "range": {"gte": "now-24h"}}
]
}
)
# Check similarity scores
for result in results:
if result.score > threshold:
return True # Duplicate found
return False
# In main loop, before inserting:
if is_duplicate(gem["gem"]):
log.info(f"Skipping duplicate gem: {gem['gem'][:50]}...")
continue
```
**Pros:** Catches duplicates at source, no extra jobs
**Cons:** Adds ~50-100ms per gem (embedding call)
#### Option B: Periodic AI Review (Subagent Task)
Have a subagent periodically review and merge duplicates:
Base provides the capture infrastructure (`memories_tr` collection).
```bash
# Run weekly via cron
0 3 * * 0 cd <PROJECT_PATH> && python3 dedup_gems.py
```
**dedup_gems.py approach:**
1. Load all gems from past 7 days
2. Group by semantic similarity (clustering)
3. For each cluster > 1 gem:
- Keep highest confidence gem as primary
- Merge context from others into primary
- Delete duplicates
**Pros:** Can use reasoning model for nuanced merging
**Cons:** Batch job, duplicates exist until cleanup runs
#### Option C: Real-time Watcher Hook
Add deduplication to the real-time watcher before memories are even stored:
```python
# In watcher, before upsert to memories_tr
if is_similar_to_recent(memory_text, window="1h"):
memory["duplicate_of"] = similar_id # Tag but still store
```
**Pros:** Prevents duplicate memories upstream
**Cons:** Memories may differ slightly even if gems would be same
**Recommendation by Model:**
| Model | Recommended Approach | Reason |
|-------|---------------------|--------|
| **4b** | **Option A + B** | Built-in check prevents duplicates; periodic review catches edge cases |
| **30b** | **Option B only** | 30b produces fewer duplicates; weekly review sufficient |
| **Production** | **Option A** | Best balance of prevention and performance |
**Configuration:**
Add to `curator_config.json`:
```json
{
"deduplication": {
"enabled": true,
"similarity_threshold": 0.85,
"lookback_hours": 24,
"mode": "skip" // "skip", "merge", or "flag"
}
}
```
---
### 6. OpenClaw Compactor Configuration
**Status:** ✅ Applied
**Goal:** Minimal overhead — just remove context, do nothing else.
**Config Applied:**
```json5
{
agents: {
defaults: {
compaction: {
mode: "default", // "default" or "safeguard"
reserveTokensFloor: 0, // Disable safety floor (default: 20000)
memoryFlush: {
enabled: false // Disable silent .md file writes
}
}
}
}
}
```
**What this does:**
- `mode: "default"` — Standard summarization (faster)
- `reserveTokensFloor: 0` — Allow aggressive settings (disables 20k minimum)
- `memoryFlush.enabled: false` — No silent "write memory" turns
**Known Issue: UI Glitch During Compaction**
When compaction runs, the Control UI may briefly behave unexpectedly:
- Typed text may not appear immediately after hitting Enter
- Messages may render out of order briefly
- UI "catches up" within 1-2 seconds after compaction completes
**Why:** Compaction replaces the full conversation history with a summary. The UI's WebSocket state can get briefly out of sync during this transition.
**Workaround:**
- Wait 2-3 seconds after hitting Enter during compaction
- Or hard refresh (Ctrl+Shift+R) if UI seems stuck
- **Note:** This is an OpenClaw Control UI limitation — cannot be fixed from TrueRecall side at this time.
**Note:** `reserveTokens` and `keepRecentTokens` are Pi runtime settings, not configurable via `agents.defaults.compaction`. They are set per-model in `contextWindow`/`contextTokens`.
---
### 7. Configuration Options Reference
**All configurable options with defaults:**
| Option | Default | Description |
|--------|---------|-------------|
| **Embedding model** | `mxbai-embed-large` | Model for generating gem embeddings. `mxbai` = higher accuracy (MTEB 66.5). `snowflake` = faster processing. |
| **Timer interval** | `5` minutes | How often the curator runs. `5 min` = fast backlog clearing. `30 min` = balanced. `60 min` = minimal overhead. |
| **Batch size** | `100` | Max memories sent to curator per run. Higher = fewer API calls but more memory usage. |
| **Max gems per run** | *(unlimited)* | Hard limit on gems extracted per batch. Not set by default — extracts all found gems. |
| **Qdrant URL** | `http://<QDRANT_IP>:6333` | Vector database endpoint. Change if Qdrant runs on different host/port. |
| **Ollama URL** | `http://<OLLAMA_IP>:11434` | LLM endpoint for gem extraction. Change if Ollama runs elsewhere. |
| **Curator LLM** | `qwen3:30b-a3b-instruct` | Model for extracting gems. `30b` = best quality (~3s). `4b` = faster but needs JSON fallback. |
| **User ID** | `rob` | Owner identifier for memories. Used for filtering and multi-user setups. |
| **Source collection** | `memories_tr` | Qdrant collection for raw captured memories. |
| **Target collection** | `gems_tr` | Qdrant collection for curated gems (injected into context). |
| **Watcher service** | `enabled` | Real-time capture daemon. Reads session JSONL and writes to Qdrant. |
| **Cron timer** | `enabled` | Periodic curation job. Runs `curator_timer.py` on schedule. |
| **Log path** | `/var/log/true-recall-timer.log` | Where curator output is written. Check with `tail -f`. |
| **Dry-run mode** | `disabled` | Test mode — shows what would be curated without writing to Qdrant. |
**OpenClaw-side options:**
| Option | Default | Description |
|--------|---------|-------------|
| **Compactor mode** | `default` | How context is summarized. `default` = fast standard. `safeguard` = chunked for very long sessions. |
| **Memory flush** | `disabled` | If enabled, writes silent "memory" turn before compaction. Adds overhead — disabled for minimal lag. |
| **Context pruning** | `cache-ttl` | Removes old tool results from context. `cache-ttl` = prunes hourly. `off` = no pruning. |
---
### 8. Embedding Models
**Current Setup:**
- `memories_tr`: `snowflake-arctic-embed2` (capture similarity)
- `gems_tr`: `mxbai-embed-large` (recall similarity)
**Rationale:**
- mxbai has higher MTEB score (66.5) for semantic search
- snowflake is faster for high-volume capture
**Note:** For simplicity, a single embedding model could be used for both collections. This would reduce complexity and memory overhead, though with slightly lower recall performance.
---
### 9. memory-qdrant Plugin
**Location:** `~/.openclaw/extensions/memory-qdrant/`
**Config (openclaw.json):**
```json
{
"collectionName": "gems_tr",
"captureCollection": "memories_tr",
"autoRecall": true,
"autoCapture": true
}
```
**Functions:**
- **Recall:** Searches `gems_tr`, injects gems (hidden)
- **Capture:** Session-level to `memories_tr` (backup)
---
## Files & Locations
### Core Project
```
~/.openclaw/workspace/.projects/true-recall-v2/
├── README.md # This file
├── session.md # Detailed notes
├── curator-prompt.md # Extraction prompt
├── tr-daily/
│ └── curate_from_qdrant.py # Daily curator
└── shared/
```
### New Files (2026-02-24)
| File | Purpose |
|------|---------|
| `tr-continuous/curator_timer.py` | Timer curator (v2.2) |
| `tr-continuous/curator_config.json` | Curator settings |
| `tr-continuous/migrate_add_curated.py` | Migration script |
| `skills/qdrant-memory/scripts/realtime_qdrant_watcher.py` | Capture daemon |
| `skills/qdrant-memory/mem-qdrant-watcher.service` | Systemd service |
### Archived Files (v2.1)
| File | Status | Note |
|------|--------|------|
| `tr-daily/curate_from_qdrant.py` | 📦 Archived | Replaced by timer |
| `tr-continuous/curator_by_count.py` | 📦 Archived | Replaced by timer |
### System Files
| File | Purpose |
|------|---------|
| `~/.openclaw/extensions/memory-qdrant/` | Plugin code |
| `~/.openclaw/openclaw.json` | Configuration |
| `/etc/systemd/system/mem-qdrant-watcher.service` | Service file |
---
## Configuration
### memory-qdrant Plugin
**File:** `~/.openclaw/openclaw.json`
```json
{
"memory-qdrant": {
"config": {
"autoCapture": true,
"autoRecall": true,
"collectionName": "gems_tr",
"captureCollection": "memories_tr",
"embeddingModel": "snowflake-arctic-embed2",
"maxRecallResults": 2,
"minRecallScore": 0.7,
"ollamaUrl": "http://<OLLAMA_IP>:11434",
"qdrantUrl": "http://<QDRANT_IP>:6333"
},
"enabled": true
}
}
```
### Gateway Control UI (OpenClaw 2026.2.23)
```json
{
"gateway": {
"controlUi": {
"allowedOrigins": ["*"],
"allowInsecureAuth": false,
"dangerouslyDisableDeviceAuth": true
}
}
}
```
---
## Validation
### Check Collections
```bash
# Count points
# Verify base is running
curl -s http://<QDRANT_IP>:6333/collections/memories_tr | jq '.result.points_count'
```
## Installation
### 1. Curator Setup
**Install cron job:**
```bash
# Edit path and add to crontab
echo "*/5 * * * * cd <INSTALL_PATH>/true-recall-gems/tr-continuous && /usr/bin/python3 curator_timer.py >> /var/log/true-recall-timer.log 2>&1" | sudo crontab -
sudo touch /var/log/true-recall-timer.log
sudo chmod 644 /var/log/true-recall-timer.log
```
**Configure curator_config.json:**
```json
{
"timer_minutes": 5,
"max_batch_size": 100,
"user_id": "your-user-id",
"source_collection": "memories_tr",
"target_collection": "gems_tr"
}
```
**Edit curator_timer.py:**
- Replace `<QDRANT_IP>`, `<OLLAMA_IP>` with your endpoints
- Replace `<USER_ID>` with your identifier
- Replace `<CURATOR_MODEL>` with your LLM (e.g., `qwen3:30b`)
### 2. Injection Setup
Add to your OpenClaw `openclaw.json`:
```json
{
"plugins": {
"entries": {
"memory-qdrant": {
"config": {
"autoCapture": true,
"autoRecall": true,
"captureCollection": "memories_tr",
"collectionName": "gems_tr",
"embeddingModel": "snowflake-arctic-embed2",
"maxRecallResults": 2,
"minRecallScore": 0.8,
"ollamaUrl": "http://<OLLAMA_IP>:11434",
"qdrantUrl": "http://<QDRANT_IP>:6333"
},
"enabled": true
}
},
"slots": {
"memory": "memory-qdrant"
}
}
}
```
---
## Files
| File | Purpose |
|------|---------|
| `tr-continuous/curator_timer.py` | Timer-based curator |
| `tr-continuous/curator_config.json` | Curator settings template |
---
## Verification
```bash
# Check v1 capture
curl -s http://<QDRANT_IP>:6333/collections/memories_tr | jq '.result.points_count'
# Check v2 curation
curl -s http://<QDRANT_IP>:6333/collections/gems_tr | jq '.result.points_count'
# View recent captures
curl -s -X POST http://<QDRANT_IP>:6333/collections/memories_tr/points/scroll \
-H "Content-Type: application/json" \
-d '{"limit": 3, "with_payload": true}' | jq '.result.points[].payload.content'
```
### Check Services
```bash
# Watcher
sudo systemctl status mem-qdrant-watcher
sudo journalctl -u mem-qdrant-watcher -n 20
# OpenClaw
openclaw status
openclaw gateway status
```
### Test Capture
Send a message, then check:
```bash
# Should increase by 1-2 points
curl -s http://<QDRANT_IP>:6333/collections/memories_tr | jq '.result.points_count'
# Check curator logs
tail -20 /var/log/true-recall-timer.log
```
---
## Troubleshooting
## Dependencies
### Watcher Not Capturing
```bash
# Check logs
sudo journalctl -u mem-qdrant-watcher -f
# Verify dependencies
curl http://<QDRANT_IP>:6333/ # Qdrant
curl http://<OLLAMA_IP>:11434/api/tags # Ollama
```
### Plugin Not Loading
```bash
# Validate config
openclaw config validate
# Check logs
tail /tmp/openclaw/openclaw-$(date +%Y-%m-%d).log | grep memory-qdrant
# Restart gateway
openclaw gateway restart
```
### Gateway Won't Start (OpenClaw 2026.2.23+)
**Error:** `non-loopback Control UI requires gateway.controlUi.allowedOrigins`
**Fix:** Add to `openclaw.json`:
```json
"gateway": {
"controlUi": {
"allowedOrigins": ["*"]
}
}
```
| Component | Provided By | Required For |
|-----------|-------------|--------------|
| Capture | v1 | v2 (input) |
| Curation | v2 | Injection |
| Injection | v2 | Context recall |
---
## Status Summary
| Component | Status | Notes |
|-----------|--------|-------|
| Real-time watcher | ✅ Active | PID 1748, capturing |
| memories_tr | ✅ 12,378 pts | All tagged `curated: false` |
| gems_tr | ✅ 5 pts | Injection ready |
| Timer curator | ✅ Deployed | Every 30 min via cron |
| Plugin injection | ✅ Working | Uses gems_tr |
| Migration | ✅ Complete | 12,378 memories |
**Logs:** `tail /var/log/true-recall-timer.log`
**Next:** Monitor first timer run
---
## Roadmap
### Planned Features
| Feature | Status | Description |
|---------|--------|-------------|
| Interactive install script | ⏳ Planned | Prompts for embedding model, timer interval, batch size, endpoints |
| Single embedding model | ⏳ Planned | Option to use one model for both collections |
| Configurable thresholds | ⏳ Planned | Per-user customization via prompts |
**Install script will prompt for:**
1. **Embedding model** — snowflake (fast) vs mxbai (accurate)
2. **Timer interval** — 5 min / 30 min / hourly
3. **Batch size** — 50 / 100 / 500 memories
4. **Endpoints** — Qdrant/Ollama URLs
5. **User ID** — for multi-user setups
---
**Maintained by:** Rob
**AI Assistant:** Kimi 🎙️
**Version:** 2026.02.24-v2.2
**Version:** 2.0
**Requires:** TrueRecall v1
**Collections:** `memories_tr` (v1), `gems_tr` (v2)

View File

@@ -1,308 +0,0 @@
# TrueRecall v2
**Project:** Gem extraction and memory recall system
**Status:** ✅ Active
**Location:** `/root/.openclaw/workspace/.projects/true-recall-v2/`
---
## Overview
TrueRecall extracts "gems" (key insights) from conversations and stores them for context injection. It's the memory curation system that powers Kimi's contextual awareness.
### Current Flow
```
1. Conversation happens → Real-time watcher → memories_tr (Qdrant)
2. Daily (2:45 AM) → Curator reads memories_tr → extracts gems
3. Gems stored → gems_tr collection (Qdrant)
4. On each turn → memory-qdrant plugin injects gems as context
```
**Verified:** 2026-02-24 — Real-time watcher capturing (12,223 → 12,228 points)
---
## Architecture
### Collections (Qdrant)
| Collection | Purpose | Content | Status |
|------------|---------|---------|--------|
| `memories_tr` | Full text storage | Every conversation turn (migrated from `kimi_memories`) | ✅ Active |
| `gems_tr` | Gems (extracted insights) | Curated key points (for injection) | ✅ Active |
| `true_recall` | Legacy gems | Archive of previously extracted gems | 📦 Preserved |
| `kimi_memories` | Original collection | Backup (12,223 points preserved) | 📦 Preserved |
### Migration Script
| File | Purpose |
|------|---------|
| `migrate_memories.py` | Migrate data from `kimi_memories` → `memories_tr` with cleaning |
**Usage:**
```bash
python3 migrate_memories.py
```
**What it does:**
- Reads all points from `kimi_memories`
- Cleans content (removes metadata, thinking tags)
- Stores to `memories_tr`
- Preserves original `kimi_memories`
---
### Components
| Component | Location | Purpose |
|-----------|----------|---------|
| **memory-qdrant plugin** | `/root/.openclaw/extensions/memory-qdrant/` | Injects gems as context |
| **Curation script** | `/root/.openclaw/workspace/.projects/true-recall-v2/tr-daily/curate_from_qdrant.py` | Extracts gems |
| **Curator prompt** | `/root/.openclaw/workspace/.projects/true-recall-v2/curator-prompt.md` | Instructions for gem extraction |
---
## File Locations
### Core Files
```
/root/.openclaw/workspace/.projects/true-recall-v2/
├── README.md # This file
├── session.md # Development notes
├── curator-prompt.md # Gem extraction prompt
├── tr-daily/ # Daily curation
│ └── curate_from_qdrant.py # Main curator script
├── tr-compact/ # (Reserved for v2 expansion)
│ └── hook.py # Compaction hook (not active)
├── tr-worker/ # (Reserved for v2 expansion)
│ └── worker.py # Background worker (not active)
└── shared/ # Shared resources
```
### Plugin Files (Located in OpenClaw Extensions)
> **Note:** These files live in `/root/.openclaw/extensions/`, not in this project folder. Documented here for reference.
| Actual Location | Project Reference |
|----------------|-------------------|
| `/root/.openclaw/extensions/memory-qdrant/index.ts` | Plugin code (capture + injection) |
| `/root/.openclaw/extensions/memory-qdrant/config.ts` | Plugin config schema |
| `/root/.openclaw/openclaw.json` | Plugin configuration |
### Configuration
| File | Purpose |
|------|---------|
| `/root/.openclaw/openclaw.json` | Plugin config (collectionName: gems_tr) |
| `/root/.openclaw/extensions/memory-qdrant/index.ts` | Plugin code |
| `/root/.openclaw/extensions/memory-qdrant/config.ts` | Plugin config schema |
### Cron Jobs
| Time | Command | Purpose |
|------|---------|---------|
| 2:45 AM | `curate_memories.py` | Daily gem extraction (v1) → stores to gems_tr |
| 2:50 AM | `archive_to_memories_tr.py` | Archive to memories_tr |
View cron: `crontab -l`
---
## Process Flow
### Step 1: Capture (Real-Time Watcher)
```
OpenClaw Session ──→ Real-Time Watcher ──→ Qdrant (memories_tr)
```
**Location:** `/root/.openclaw/workspace/skills/qdrant-memory/scripts/realtime_qdrant_watcher.py`
**What it does:**
- Watches session JSONL files in real-time
- Parses each conversation turn (user + AI)
- Embeds with `snowflake-arctic-embed2` via Ollama
- Stores directly to `memories_tr` collection
- Cleans content (removes metadata, thinking tags)
**Systemd Service:** `mem-qdrant-watcher.service`
**Status:** ✅ Created, ready to deploy
### Step 2: Curation (Daily)
```
2:45 AM → curate_memories.py → memories_tr → qwen3 → gems_tr
```
**Location:** `/root/.openclaw/workspace/.projects/true-recall-v1/tr-process/curate_memories.py`
**What it does:**
1. Reads from Redis buffer (`mem:rob`)
2. Passes to qwen3 (curator model)
3. Extracts gems using prompt
4. Stores gems to `gems_tr` collection
### Step 3: Injection
```
User message → memory-qdrant plugin → Search gems_tr → Inject as context
```
**Location:** `/root/.openclaw/extensions/memory-qdrant/index.ts`
**What it does:**
1. Listens to `before_agent_start` events
2. Embeds current prompt
3. Searches `gems_tr` collection
4. Returns `{ prependContext: "..." }` with gems
5. Gems appear in my context (hidden from UI)
---
## Configuration
### memory-qdrant Plugin
```json
{
"autoCapture": true,
"autoRecall": true,
"collectionName": "gems_tr",
"embeddingModel": "snowflake-arctic-embed2",
"maxRecallResults": 2,
"minRecallScore": 0.7
}
```
**Key setting:** `collectionName: "gems_tr"` — tells plugin to inject gems, not full text.
---
## Gem Format
```json
{
"gem": "User prefers dark mode for interface",
"context": "Discussed UI theme options",
"snippet": "rob: I want dark mode\nKimi: Done",
"categories": ["preference"],
"importance": "high",
"confidence": 0.95,
"date": "2026-02-24",
"conversation_id": "uuid",
"turn_range": "5-7"
}
```
---
## Validation Commands
### Check Qdrant Collections
```bash
curl -s "http://10.0.0.40:6333/collections" | python3 -m json.tool
```
### Check Collection Points
```bash
# memories_tr (full text)
curl -s -X POST "http://10.0.0.40:6333/collections/memories_tr/points/scroll" \
-H "Content-Type: application/json" \
-d '{"limit": 5, "with_payload": true}' | python3 -m json.tool
# gems_tr (gems)
curl -s -X POST "http://10.0.0.40:6333/collections/gems_tr/points/scroll" \
-H "Content-Type: application/json" \
-d '{"limit": 5, "with_payload": true}' | python3 -m json.tool
```
### Check Cron Jobs
```bash
crontab -l | grep -E "true|recall|curate"
```
### Check Plugin Logs
```bash
tail -50 /tmp/openclaw/openclaw-$(date +%Y-%m-%d).log | grep memory-qdrant
```
---
## Troubleshooting
### Issue: `<relevant-memories>` showing in UI
**Cause:** Plugin was using `memories_tr` (full text) instead of `gems_tr` (gems)
**Fix:** Set `"collectionName": "gems_tr"` in openclaw.json
**Reference:** KB entry `relevant-memories-ui-issue.md`
### Issue: No gems being extracted
**Check:**
1. Is memories_tr populated? `curl .../collections/memories_tr/points/scroll`
2. Is curator prompt valid? `cat curator-prompt.md`
3. Is qwen3 available? `curl -s http://10.0.0.10:11434/api/tags`
---
## Related Projects
| Project | Location | Purpose |
|---------|----------|---------|
| **true-recall-v1** | `/.projects/true-recall-v1/` | Original (Redis-based) |
| **memory-qdrant** | `/root/.openclaw/extensions/memory-qdrant/` | Plugin |
| **mem-redis** | `/root/.openclaw/workspace/skills/mem-redis/` | Redis utilities |
---
## Backups
| File | Backup Location |
|------|-----------------|
| openclaw.json | `/root/.openclaw/openclaw.json.bak.2026-02-24` |
| Plugin | `/root/.openclaw/extensions/memory-qdrant/index.ts.bak.2026-02-24` |
---
## Final Status (2026-02-24 15:08)
| Component | Status |
|-----------|--------|
| Collection `kimi_memories` → `memories_tr` | ✅ Migrated (12,223 points) |
| Real-time watcher | ✅ **Deployed & verified** (12,228 points, +5 new) |
| Collection `gems_tr` | ✅ Active (5 gems stored) |
| Curator v2 | ✅ Working - tested with 327 turns |
| Config | ✅ Updated to `gems_tr` |
| Cron jobs | ✅ Cleaned |
| Documentation | ✅ Updated |
### Daily Schedule (Simplified)
| Time | Job | Flow |
|------|-----|------|
| Continuous | autoCapture | Conversation → `memories_tr` |
| 2:45 AM | Curator v2 | `memories_tr` → `gems_tr` |
| Each turn | Injection | `gems_tr` → Context |
**Redis usage:** Disabled for memory. Used only for `delayed:notifications` queue.
**Auto-Capture Status:** ✅ **Working** - Real-time watcher deployed and verified
---
**Last Updated:** 2026-02-24
**Project Lead:** Rob
**AI Assistant:** Kimi 🎙️

View File

@@ -1,325 +0,0 @@
# TrueRecall v2
**Project:** Gem extraction and memory recall system
**Status:** ✅ Active
**Location:** `/root/.openclaw/workspace/.projects/true-recall-v2/`
---
## Table of Contents
- [Overview](#overview)
- [Architecture](#architecture)
- [Collections](#collections-qdrant)
- [Components](#components)
- [Process Flow](#process-flow)
- [Configuration](#configuration)
- [Validation Commands](#validation-commands)
- [Troubleshooting](#troubleshooting)
- [Related Projects](#related-projects)
- [Backups](#backups)
- [Final Status](#final-status-2026-02-24-1508)
---
## Overview
TrueRecall extracts "gems" (key insights) from conversations and stores them for context injection. It's the memory curation system that powers Kimi's contextual awareness.
### Current Flow
```
1. Conversation happens → Real-time watcher → memories_tr (Qdrant)
2. Daily (2:45 AM) → Curator reads memories_tr → extracts gems
3. Gems stored → gems_tr collection (Qdrant)
4. On each turn → memory-qdrant plugin injects gems as context
```
**Verified:** 2026-02-24 — Real-time watcher capturing (12,223 → 12,228 points)
---
## Architecture
### Collections (Qdrant)
| Collection | Purpose | Content | Status |
|------------|---------|---------|--------|
| `memories_tr` | Full text storage | Every conversation turn (migrated from `kimi_memories`) | ✅ Active |
| `gems_tr` | Gems (extracted insights) | Curated key points (for injection) | ✅ Active |
| `true_recall` | Legacy gems | Archive of previously extracted gems | 📦 Preserved |
| `kimi_memories` | Original collection | Backup (12,223 points preserved) | 📦 Preserved |
### Migration Script
| File | Purpose |
|------|---------|
| `migrate_memories.py` | Migrate data from `kimi_memories` → `memories_tr` with cleaning |
**Usage:**
```bash
python3 migrate_memories.py
```
**What it does:**
- Reads all points from `kimi_memories`
- Cleans content (removes metadata, thinking tags)
- Stores to `memories_tr`
- Preserves original `kimi_memories`
---
### Components
| Component | Location | Purpose |
|-----------|----------|---------|
| **memory-qdrant plugin** | `/root/.openclaw/extensions/memory-qdrant/` | Injects gems as context |
| **Curation script** | `/root/.openclaw/workspace/.projects/true-recall-v2/tr-daily/curate_from_qdrant.py` | Extracts gems |
| **Curator prompt** | `/root/.openclaw/workspace/.projects/true-recall-v2/curator-prompt.md` | Instructions for gem extraction |
---
## File Locations
### Core Files
```
/root/.openclaw/workspace/.projects/true-recall-v2/
├── README.md # This file
├── session.md # Development notes
├── curator-prompt.md # Gem extraction prompt
├── tr-daily/ # Daily curation
│ └── curate_from_qdrant.py # Main curator script
├── tr-compact/ # (Reserved for v2 expansion)
│ └── hook.py # Compaction hook (not active)
├── tr-worker/ # (Reserved for v2 expansion)
│ └── worker.py # Background worker (not active)
└── shared/ # Shared resources
```
### Plugin Files (Located in OpenClaw Extensions)
> **Note:** These files live in `/root/.openclaw/extensions/`, not in this project folder. Documented here for reference.
| Actual Location | Project Reference |
|----------------|-------------------|
| `/root/.openclaw/extensions/memory-qdrant/index.ts` | Plugin code (capture + injection) |
| `/root/.openclaw/extensions/memory-qdrant/config.ts` | Plugin config schema |
| `/root/.openclaw/openclaw.json` | Plugin configuration |
### Configuration
| File | Purpose |
|------|---------|
| `/root/.openclaw/openclaw.json` | Plugin config (collectionName: gems_tr) |
| `/root/.openclaw/extensions/memory-qdrant/index.ts` | Plugin code |
| `/root/.openclaw/extensions/memory-qdrant/config.ts` | Plugin config schema |
### Cron Jobs
| Time | Command | Purpose |
|------|---------|---------|
| 2:45 AM | `curate_memories.py` | Daily gem extraction (v1) → stores to gems_tr |
| 2:50 AM | `archive_to_memories_tr.py` | Archive to memories_tr |
View cron: `crontab -l`
---
## Process Flow
### Step 1: Capture (Real-Time Watcher)
```
OpenClaw Session ──→ Real-Time Watcher ──→ Qdrant (memories_tr)
```
**Location:** `/root/.openclaw/workspace/skills/qdrant-memory/scripts/realtime_qdrant_watcher.py`
**What it does:**
- Watches session JSONL files in real-time
- Parses each conversation turn (user + AI)
- Embeds with `snowflake-arctic-embed2` via Ollama
- Stores directly to `memories_tr` collection
- Cleans content (removes metadata, thinking tags)
**Systemd Service:** `mem-qdrant-watcher.service`
**Status:** ✅ Created, ready to deploy
### Step 2: Curation (Daily)
```
2:45 AM → curate_memories.py → memories_tr → qwen3 → gems_tr
```
**Location:** `/root/.openclaw/workspace/.projects/true-recall-v1/tr-process/curate_memories.py`
**What it does:**
1. Reads from Redis buffer (`mem:rob`)
2. Passes to qwen3 (curator model)
3. Extracts gems using prompt
4. Stores gems to `gems_tr` collection
### Step 3: Injection
```
User message → memory-qdrant plugin → Search gems_tr → Inject as context
```
**Location:** `/root/.openclaw/extensions/memory-qdrant/index.ts`
**What it does:**
1. Listens to `before_agent_start` events
2. Embeds current prompt
3. Searches `gems_tr` collection
4. Returns `{ prependContext: "..." }` with gems
5. Gems appear in my context (hidden from UI)
---
## Configuration
### memory-qdrant Plugin
```json
{
"autoCapture": true,
"autoRecall": true,
"collectionName": "gems_tr",
"embeddingModel": "snowflake-arctic-embed2",
"maxRecallResults": 2,
"minRecallScore": 0.7
}
```
**Key setting:** `collectionName: "gems_tr"` — tells plugin to inject gems, not full text.
---
## Gem Format
```json
{
"gem": "User prefers dark mode for interface",
"context": "Discussed UI theme options",
"snippet": "rob: I want dark mode\nKimi: Done",
"categories": ["preference"],
"importance": "high",
"confidence": 0.95,
"date": "2026-02-24",
"conversation_id": "uuid",
"turn_range": "5-7"
}
```
---
## Validation Commands
### Check Qdrant Collections
```bash
curl -s "http://10.0.0.40:6333/collections" | python3 -m json.tool
```
### Check Collection Points
```bash
# memories_tr (full text)
curl -s -X POST "http://10.0.0.40:6333/collections/memories_tr/points/scroll" \
-H "Content-Type: application/json" \
-d '{"limit": 5, "with_payload": true}' | python3 -m json.tool
# gems_tr (gems)
curl -s -X POST "http://10.0.0.40:6333/collections/gems_tr/points/scroll" \
-H "Content-Type: application/json" \
-d '{"limit": 5, "with_payload": true}' | python3 -m json.tool
```
### Check Cron Jobs
```bash
crontab -l | grep -E "true|recall|curate"
```
### Check Plugin Logs
```bash
tail -50 /tmp/openclaw/openclaw-$(date +%Y-%m-%d).log | grep memory-qdrant
```
---
## Troubleshooting
### Issue: `<relevant-memories>` showing in UI
**Cause:** Plugin was using `memories_tr` (full text) instead of `gems_tr` (gems)
**Fix:** Set `"collectionName": "gems_tr"` in openclaw.json
**Reference:** KB entry `relevant-memories-ui-issue.md`
### Issue: No gems being extracted
**Check:**
1. Is memories_tr populated? `curl .../collections/memories_tr/points/scroll`
2. Is curator prompt valid? `cat curator-prompt.md`
3. Is qwen3 available? `curl -s http://10.0.0.10:11434/api/tags`
---
## Related Projects
| Project | Location | Purpose |
|---------|----------|---------|
| **true-recall-v1** | `/.projects/true-recall-v1/` | Original (Redis-based) |
| **memory-qdrant** | `/root/.openclaw/extensions/memory-qdrant/` | Plugin |
| **mem-redis** | `/root/.openclaw/workspace/skills/mem-redis/` | Redis utilities |
---
## Backups
| File | Backup Location |
|------|-----------------|
| openclaw.json | `/root/.openclaw/openclaw.json.bak.2026-02-24` |
| Plugin | `/root/.openclaw/extensions/memory-qdrant/index.ts.bak.2026-02-24` |
---
## Final Status (2026-02-24 15:08)
| Component | Status |
|-----------|--------|
| Collection `kimi_memories` → `memories_tr` | ✅ Migrated (12,223 points) |
| Real-time watcher | ✅ **Deployed & verified** (12,228 points, +5 new) |
| Collection `gems_tr` | ✅ Active (5 gems stored) |
| Curator v2 | ✅ Working - tested with 327 turns |
| Config | ✅ Updated to `gems_tr` |
| Cron jobs | ✅ Cleaned |
| Documentation | ✅ Updated |
### Daily Schedule (Simplified)
| Time | Job | Flow |
|------|-----|------|
| Continuous | autoCapture | Conversation → `memories_tr` |
| 2:45 AM | Curator v2 | `memories_tr` → `gems_tr` |
| Each turn | Injection | `gems_tr` → Context |
**Redis usage:** Disabled for memory. Used only for `delayed:notifications` queue.
**Auto-Capture Status:** ✅ **Working** - Real-time watcher deployed and verified
---
**Last Updated:** 2026-02-24
**Project Lead:** Rob
**AI Assistant:** Kimi 🎙️

View File

@@ -1,183 +0,0 @@
# NeuralStream
**Neural streaming memory for OpenClaw with gem-based context injection.**
## Overview
NeuralStream extracts high-value insights ("gems") from conversation batches using qwen3, stores them in Qdrant, and injects relevant gems into context on each new turn. This creates **infinite effective context** — the active window stays small, but semantically relevant gems from all past conversations are always retrievable.
## Core Concept
| Traditional Memory | NeuralStream |
|-------------------|--------------|
| Context lost on `/new` | Gems persist in Qdrant |
| Full history or generic summary | Semantic gem retrieval |
| Static context window | Dynamic injection |
| Survives compaction only | Survives session reset |
| **Limited context** | **Infinite effective context** |
## How It Works
### Capture → Extract → Store → Retrieve
1. **Capture:** Every turn buffered to Redis (reuses mem-redis-watcher)
2. **Extract:** Batch of 5 turns → qwen3 (with 256k context) extracts structured gems
3. **Store:** Gems embedded + stored in Qdrant `neuralstream`
4. **Retrieve:** Each new turn → semantic search → inject top-10 gems
### Hybrid Triggers (Three-way)
| Trigger | Condition | Purpose |
|---------|-----------|---------|
| Batch | Every 5 turns | Normal extraction |
| Context | 50% usage (`ctx.getContextUsage()`) | Proactive pre-compaction |
| Timer | 15 min idle | Safety net |
**Context Awareness:** qwen3 receives up to 256k tokens of history for understanding, but only extracts gems from the last N turns (avoiding current context).
All gems survive `/new`, `/reset`, and compaction via Qdrant persistence.
## Architecture
NeuralStream is the **middle layer** — extraction intelligence on top of existing infrastructure:
```
┌─────────────────────────────────────────────────────────┐
│ EXISTING: mem-redis-watcher │
│ Every turn → Redis buffer │
└──────────────────┬──────────────────────────────────────┘
┌──────────▼──────────┐
│ NeuralStream │
│ - Batch reader │
│ - Gem extractor │
│ - Qdrant store │
└──────────┬──────────┘
┌──────────▼──────────┐
│ EXISTING: │
│ qdrant-memory │
│ Semantic search │
│ Context injection │
└─────────────────────┘
```
## Technical Reference
### Native Context Monitoring
```typescript
// In turn_end hook
const usage = ctx.getContextUsage();
// usage.tokens, usage.contextWindow, usage.percent
// Trigger extraction when usage.percent >= threshold
```
### Primary Hook: turn_end
```typescript
pi.on("turn_end", async (event, ctx) => {
const { turnIndex, message, toolResults } = event;
// Buffer turn to Redis
// Check ctx.getContextUsage().percent
// If batch >= 5 OR percent >= 50%: extract
});
```
### Timer Fallback
```bash
# Cron every 10 min
# Check neuralstream:buffer age > 15 min
# If yes: extract from partial batch
```
### Context-Aware Extraction
- Feed qwen3: Up to 256k tokens (full history for context)
- Extract from: Last `batch_size` turns only
- Benefit: Rich understanding without gemming current context
## Gem Format
```json
{
"gem_id": "uuid",
"content": "Distilled insight/fact/decision",
"summary": "One-line for quick scanning",
"topics": ["docker", "redis", "architecture"],
"importance": 0.9,
"source": {
"session_id": "uuid",
"date": "2026-02-23",
"turn_range": "15-20"
},
"tags": ["decision", "fact", "preference", "todo", "code"],
"created_at": "2026-02-23T15:26:00Z"
}
```
## Configuration (All Tunable)
| Setting | Default | Description |
|---------|---------|-------------|
| batch_size | 5 | Turns per extraction |
| context_threshold | 50% | Token % trigger (40-80% range) |
| idle_timeout | 15 min | Timer trigger threshold |
| gem_model | qwen3 | Extraction LLM (256k context) |
| max_gems_injected | 10 | Per-turn limit |
| embedding | snowflake-arctic-embed2 | Same as kimi_memories |
| collection | neuralstream | Qdrant (1024 dims, Cosine) |
## Qdrant Schema
**Collection:** `neuralstream`
- Vector size: 1024
- Distance: Cosine
- On-disk payload: true
## Project Structure
```
.projects/neuralstream/
├── README.md # This file
├── session.md # Development log & state
├── prompt.md # (TBD) qwen3 extraction prompt
└── src/ # (TBD) Implementation
├── extract.ts # Gem extraction logic
├── store.ts # Qdrant storage
└── inject.ts # Context injection
```
## Status
- [x] Architecture defined (v2.2 context-aware)
- [x] Native context monitoring validated (ctx.getContextUsage)
- [x] Naming finalized (NeuralStream, alias: ns)
- [x] Hook research completed
- [x] Qdrant collection created (`neuralstream`)
- [x] Gem format proposed
- [x] Infrastructure decision (reuse Redis/Qdrant)
- [ ] Extraction prompt design
- [ ] Implementation
- [ ] Testing
## Backups
- Local: `/root/.openclaw/workspace/.projects/neuralstream/`
- Remote: `deb2:/root/.projects/neuralstream/` (build/test only)
- kimi_kb: Research entries stored
## Related Projects
- **True Recall:** Gem extraction inspiration
- **OpenClaw:** Host platform
- **kimi_memories:** Shared Qdrant infrastructure
- **mem-redis-watcher:** Existing capture layer
---
**Created:** 2026-02-23
**Alias:** ns
**Purpose:** Infinite context for LLMs

360
audit_checklist.md Normal file
View File

@@ -0,0 +1,360 @@
# TrueRecall Gems - Master Audit Checklist (GIT)
**For:** `.git_projects/true-recall-gems/` (Git Repository - Sanitized)
**Version:** 2.2
**Last Updated:** 2026-02-25 10:07 CST
---
## Overview
This checklist validates the **git repository** where all private IPs, absolute paths, and credentials have been sanitized. Use this before pushing to public repositories.
**Related Files:**
- `GIT_VALIDATION_CHECK.md` - Comprehensive git validation checklist
- `LOCAL_VALIDATION_CHECK.md` - Local dev validation (in `.local_projects/`)
- `VALIDATION_NOTES.md` - Auto-generated validation findings
---
## Recent Fixes (2026-02-25)
| Issue | Status | Fix |
|-------|--------|-----|
| Embedding model mismatch | ✅ Fixed | Changed curator to `snowflake-arctic-embed2` |
| Gems had no vectors | ✅ Fixed | Updated `store_gem()` to use `text` field |
| JSON parsing errors | ✅ Fixed | Simplified extraction prompt |
| Watcher stuck on old session | ✅ **Fixed 12:22** | Restarted watcher service |
| Plugin capture 0 exchanges | ✅ **Fixed 12:34** | Added `extractMessageText()` for array content |
| Plugin exchanges working | ✅ **Verified 12:41** | 9 exchanges extracted per session |
| **HTML comments in UI** | ✅ **Fixed 14:02** | Changed `formatRelevantMemoriesContext` to clean text format |
| **prependContext vs systemPrompt** | ✅ **Fixed 14:02** | Changed hook return from `prependContext` to `systemPrompt` for hidden injection |
| **TypeScript source not updated** | ✅ **Fixed 14:02** | Updated `.ts` file, not just compiled `.js` |
### Today's Issues Found (2026-02-25)
| # | Issue | Description | Status | Priority |
|---|-------|-------------|--------|----------|
| 1 | HTML comments visible in UI | `\u003c!-- relevant-memories-start --\u003e` blocks showing in chat | ✅ **FIXED** | High |
| 2 | Memory injection format | Was using HTML comment format, now clean "Memory Injection:" text | ✅ **FIXED** | High |
| 3 | prependContext vs systemPrompt | Plugin was using `prependContext` (visible in user message) instead of `systemPrompt` (hidden in system prompt) | ✅ **FIXED** | High |
| 4 | TypeScript source not updated | OpenClaw compiles from `.ts`, was editing `.js` only | ✅ **FIXED** | High |
| 5 | Gateway restart issues | kill/killall not working reliably | ✅ **FIXED** | Medium |
| 6 | **README needs update** | TrueRecall v2 is standalone, not addon to Jarvis Memory | ✅ **FIXED** | Medium |
### Needed Improvements (Carryover)
| Issue | Description | Priority |
|-------|-------------|----------|
| **Semantic Deduplication** | No dedup between similar gems. Same fact phrased differently creates multiple gems. | High |
| **Search Result Deduplication** | Similar gems both injected, causing redundancy. | Medium |
| **Gem Quality Scoring** | No quality metric for gems. | Medium |
| **Temporal Decay** | All gems treated equally regardless of age. | Low |
| **Gem Merging/Updating** | Old gems not updated when preferences change. | Low |
| **Importance Calibration** | All curator gems marked "medium" importance. | Low |
---
## SECTION 1: System Requirements
### 1.1 Python Environment
| # | Check | Command | Expected | Status |
|---|-------|---------|----------|--------|
| 1.1.1 | Python version | `python3 --version` | 3.8+ | ☐ |
| 1.1.2 | pip available | `pip3 --version` | Working | ☐ |
| 1.1.3 | curl available | `curl --version` | Working | ☐ |
| 1.1.4 | jq available | `jq --version` | Working | ☐ |
### 1.2 Network Services
| # | Check | Command | Expected | Status |
|---|-------|---------|----------|--------|
| 1.2.1 | Qdrant reachable | `curl -s http://<QDRANT_IP>:6333` | Returns version | ☐ |
| 1.2.2 | Ollama reachable | `curl -s http://<OLLAMA_IP>:11434/api/tags` | Returns models | ☐ |
| 1.2.3 | Redis reachable | `redis-cli -h <REDIS_IP> ping` | PONG | ☐ |
| 1.2.4 | Kokoro reachable | `curl -s http://<KOKORO_IP>:8880` | 200 OK | ☐ |
### 1.3 OpenClaw
| # | Check | Command | Expected | Status |
|---|-------|---------|----------|--------|
| 1.3.1 | Gateway status | `openclaw gateway status` | Active | ☐ |
| 1.3.2 | Config valid | `openclaw doctor` | No errors | ☐ |
| 1.3.3 | Plugin loaded | `openclaw status | grep memory-qdrant` | Enabled | ☐ |
---
## SECTION 2: Project Files (Local)
### 2.1 Core Files Exist
| # | File | Path | Status |
|---|------|------|--------|
| 2.1.1 | README.md | `.local_projects/true-recall-gems/README.md` | ☐ |
| 2.1.2 | session.md | `.local_projects/true-recall-gems/session.md` | ☐ |
| 2.1.3 | checklist.md | `.local_projects/true-recall-gems/checklist.md` | ☐ |
| 2.1.4 | curator-prompt.md | `.local_projects/true-recall-gems/curator-prompt.md` | ☐ |
### 2.2 Scripts Exist
| # | File | Path | Status |
|---|------|------|--------|
| 2.2.1 | curator_timer.py | `.local_projects/true-recall-gems/tr-continuous/curator_timer.py` | ☐ |
| 2.2.2 | curator_config.json | `.local_projects/true-recall-gems/tr-continuous/curator_config.json` | ☐ |
| 2.2.3 | install.py | `.local_projects/true-recall-gems/install.py` | ☐ |
### 2.3 Watcher Files
| # | File | Path | Status |
|---|------|------|--------|
| 2.3.1 | realtime_qdrant_watcher.py | `skills/qdrant-memory/scripts/realtime_qdrant_watcher.py` | ☐ |
| 2.3.2 | mem-qdrant-watcher.service | `/etc/systemd/system/mem-qdrant-watcher.service` | ☐ |
---
## SECTION 3: Configuration Validation
### 3.1 curator_config.json
| # | Setting | Key | Expected | Status |
|---|---------|-----|----------|--------|
| 3.1.1 | Timer minutes | `timer_minutes` | 5 | ☐ |
| 3.1.2 | Batch size | `max_batch_size` | 100 | ☐ |
| 3.1.3 | User ID | `user_id` | rob | ☐ |
| 3.1.4 | Source collection | `source_collection` | memories_tr | ☐ |
| 3.1.5 | Target collection | `target_collection` | gems_tr | ☐ |
### 3.2 openclaw.json Plugin Config
| # | Setting | Key | Expected | Status |
|---|---------|-----|----------|--------|
| 3.2.1 | Qdrant URL | `qdrantUrl` | http://<QDRANT_IP>:6333 | ☐ |
| 3.2.2 | Ollama URL | `ollamaUrl` | http://<OLLAMA_IP>:11434 | ☐ |
| 3.2.3 | Embedding model | `embeddingModel` | snowflake-arctic-embed2 | ☐ |
| 3.2.4 | Capture collection | `captureCollection` | memories_tr | ☐ |
| 3.2.5 | Recall collection | `collectionName` | gems_tr | ☐ |
| 3.2.6 | Auto capture | `autoCapture` | true | ☐ |
| 3.2.7 | Auto recall | `autoRecall` | true | ☐ |
---
## SECTION 4: Qdrant Collections
### 4.1 Collection Status
| # | Check | Command | Expected | Status |
|---|-------|---------|----------|--------|
| 4.1.1 | memories_tr exists | `curl -s http://<QDRANT_IP>:6333/collections/memories_tr | jq .result.status` | green | ☐ |
| 4.1.2 | gems_tr exists | `curl -s http://<QDRANT_IP>:6333/collections/gems_tr | jq .result.status` | green | ☐ |
| 4.1.3 | memories_tr points | `curl -s http://<QDRANT_IP>:6333/collections/memories_tr | jq .result.points_count` | 12000+ | ☐ |
| 4.1.4 | gems_tr points | `curl -s http://<QDRANT_IP>:6333/collections/gems_tr | jq .result.points_count` | 70+ | ☐ |
### 4.2 Data Integrity
| # | Check | Command | Expected | Status |
|---|-------|---------|----------|--------|
| 4.2.1 | Uncurated count | Count `curated: false` | 1500+ | ☐ |
| 4.2.2 | Curated count | Count `curated: true` | 11000+ | ☐ |
| 4.2.3 | Can write points | Test insert | Success | ☐ |
| 4.2.4 | Can read points | Test query | Success | ☐ |
---
## SECTION 5: Services
### 5.1 Watcher Service
| # | Check | Command | Expected | Status |
|---|-------|---------|----------|--------|
| 5.1.1 | Service loaded | `systemctl status mem-qdrant-watcher | grep Loaded` | loaded | ☐ |
| 5.1.2 | Service active | `systemctl is-active mem-qdrant-watcher` | active | ☐ |
| 5.1.3 | Service enabled | `systemctl is-enabled mem-qdrant-watcher` | enabled | ☐ |
| 5.1.4 | Process running | `pgrep -f realtime_qdrant_watcher` | PID exists | ☐ |
| 5.1.5 | Logs available | `journalctl -u mem-qdrant-watcher -n 5` | Recent entries | ☐ |
### 5.2 Timer Curator
| # | Check | Command | Expected | Status |
|---|-------|---------|----------|--------|
| 5.2.1 | Cron job exists | `crontab -l | grep true-recall` | Entry present | ☐ |
| 5.2.2 | Cron interval | Visual check | */5 * * * * | ☐ |
| 5.2.3 | Log file exists | `ls -la /var/log/true-recall-timer.log` | File exists | ☐ |
| 5.2.4 | Recent activity | `tail -5 /var/log/true-recall-timer.log` | Recent timestamp | ☐ |
| 5.2.5 | Script executable | `test -x curator_timer.py` | Yes | ☐ |
---
## SECTION 6: Function Tests
### 6.1 Capture Test
| # | Step | Expected | Status |
|---|------|----------|--------|
| 6.1.1 | Send test message | Message captured | ☐ |
| 6.1.2 | Wait 10 seconds | Processing time | ☐ |
| 6.1.3 | Check memories_tr count | Increased by 2 | ☐ |
| 6.1.4 | Verify content | Content matches | ☐ |
### 6.2 Curation Test
| # | Step | Expected | Status |
|---|------|----------|--------|
| 6.2.1 | Note uncurated count | Baseline | ☐ |
| 6.2.2 | Run curator manually | Completes | ☐ |
| 6.2.3 | Check gems_tr | New gems added | ☐ |
| 6.2.4 | Verify curated flag | Marked true | ☐ |
### 6.3 Recall Test
| # | Step | Expected | Status |
|---|------|----------|--------|
| 6.3.1 | Start new conversation | Context loaded | ☐ |
| 6.3.2 | Send relevant query | Gems injected | ☐ |
| 6.3.3 | Verify injection | Context visible | ☐ |
---
## SECTION 7: Error Checks
### 7.1 Common Errors
| # | Error | Check | Fix | Status |
|---|-------|-------|-----|--------|
| 7.1.1 | Qdrant unreachable | `curl http://<QDRANT_IP>:6333` | Start Qdrant | ☐ |
| 7.1.2 | Ollama unreachable | `curl http://<OLLAMA_IP>:11434` | Start Ollama | ☐ |
| 7.1.3 | Watcher not running | `systemctl status mem-qdrant-watcher` | Restart service | ☐ |
| 7.1.4 | Curator not running | `tail /var/log/true-recall-timer.log` | Check cron | ☐ |
| 7.1.5 | No gems extracted | Check config.json | Verify model | ☐ |
### 7.2 Log Analysis
| # | Log | Location | Check For | Status |
|---|-----|----------|-----------|--------|
| 7.2.1 | Watcher log | `journalctl -u mem-qdrant-watcher` | Errors, crashes | ☐ |
| 7.2.2 | Curator log | `/var/log/true-recall-timer.log` | Failures, 0 gems | ☐ |
| 7.2.3 | OpenClaw log | `/tmp/openclaw/openclaw-*.log` | Plugin errors | ☐ |
| 7.2.4 | System log | `journalctl -n 50` | Service failures | ☐ |
---
## SECTION 8: Security (Local - Expected)
### 8.1 Private Info (Acceptable in Local)
| # | Item | Location | Expected | Status |
|---|------|----------|----------|--------|
| 8.1.1 | Private IPs | Scripts | 10.0.0.x | ✅ OK |
| 8.1.2 | Absolute paths | Scripts | /root/... | ✅ OK |
| 8.1.3 | Usernames | Config | rob | ✅ OK |
| 8.1.4 | Internal URLs | Config | http://10.0.0.x | ✅ OK |
### 8.2 Credentials (Should NOT Be in Code)
| # | Check | Command | Expected | Status |
|---|-------|---------|----------|--------|
| 8.2.1 | No tokens in .py | `grep -r "token" *.py` | Only env vars | ☐ |
| 8.2.2 | No passwords | `grep -r "password" *.py` | None found | ☐ |
| 8.2.3 | No API keys | `grep -rE "[a-zA-Z0-9]{32,}" *.py` | None found | ☐ |
| 8.2.4 | .git/config clean | `cat .git/config | grep url` | No tokens | ☐ |
---
## SECTION 9: Sync Check (Local vs Git)
### 9.1 Compare Directories
| # | Check | Command | Expected | Status |
|---|-------|---------|----------|--------|
| 9.1.1 | File count match | Compare `.local_projects/` vs `.git_projects/` | Similar | ☐ |
| 9.1.2 | Key files exist | README, session, checklist in both | Yes | ☐ |
| 9.1.3 | Scripts in git | curator_timer.py in git | Yes | ☐ |
| 9.1.4 | Config in git | curator_config.json in git | Yes | ☐ |
### 9.2 Sanitization Verification
| # | Check | Local | Git | Status |
|---|-------|-------|-----|--------|
| 9.2.1 | IPs in local | 10.0.0.x | ✅ Expected | - |
| 9.2.2 | IPs in git | Placeholders | ✅ Expected | - |
| 9.2.3 | Paths in local | /root/... | ✅ Expected | - |
| 9.2.4 | Paths in git | ~/... | ✅ Expected | - |
---
## 10. Recent Fixes to Verify (2026-02-25)
### Plugin Memory Format Fix
**Status:** ✅ **FIXED**
**Summary:**
- Changed `formatRelevantMemoriesContext` from HTML comment format to clean text
- Changed hook return from `prependContext` to `systemPrompt` (hides from UI)
- Updated both TypeScript source (`.ts`) and compiled JavaScript (`.js`)
**Files Modified:**
- `<OPENCLAW_PATH>/extensions/memory-qdrant/index.ts`
- `<OPENCLAW_PATH>/extensions/memory-qdrant/index.js`
**What Changed:**
```typescript
// Before:
return `<!-- relevant-memories-start -->
<relevant-memories>...`;
return { prependContext: formatRelevantMemoriesContext(...) };
// After:
return `Memory Injection: Historical context from previous conversations:
1. [category] text`;
return { systemPrompt: formatRelevantMemoriesContext(...) };
```
**Verification Checklist:**
- [ ] Send test message - memories appear as "Memory Injection:" not HTML
- [ ] No `<!-- -->` tags visible in chat
- [ ] Gateway restarted after changes
### Pending Updates
| # | Item | Description | Status |
|---|------|-------------|--------|
| 1 | README update | Clarify v2 is standalone, not addon | ✅ **FIXED** |
| 2 | Comparison table | Update v2 vs Jarvis vs v1 | ✅ **FIXED** |
---
## Sign-Off
| Role | Name | Date | Signature |
|------|------|------|-----------|
| Auditor | | | |
| Developer | | | |
| Reviewer | | | |
---
## Quick Commands Reference
```bash
# Check all services
systemctl status mem-qdrant-watcher
curl -s http://<QDRANT_IP>:6333/collections/memories_tr | jq .result.points_count
curl -s http://<QDRANT_IP>:6333/collections/gems_tr | jq .result.points_count
tail -20 /var/log/true-recall-timer.log
# Check Qdrant for curated status
curl -s -X POST http://<QDRANT_IP>:6333/collections/memories_tr/points/count \
-d '{"filter":{"must":[{"key":"curated","match":{"value":false}}]}}'
# Manual curator run
cd ~/.openclaw/workspace/.local_projects/true-recall-gems/tr-continuous
python3 curator_timer.py --dry-run
# Restart services
sudo systemctl restart mem-qdrant-watcher
```
---
*This checklist is for LOCAL working directory validation only.*
*For git/public checks, see `audit_checklist.md` in `.git_projects/true-recall-gems/`*

View File

@@ -334,7 +334,7 @@ Comprehensive pre-install, install, and post-install validation steps.
| # | Check | Command | Expected |
|---|-------|---------|----------|
| 9.1.1 | Gitea remote | `git remote get-url gitea` | `http://10.0.0.61:3000/...` |
| 9.1.1 | Gitea remote | `git remote get-url gitea` | `http://<GITEA_IP>:3000/...` |
| 9.1.2 | GitLab remote | `git remote get-url gitlab` | `https://gitlab.com/...` |
| 9.1.3 | GitHub remote | `git remote get-url github` | `https://github.com/...` |
| 9.1.4 | All remotes accessible | `git fetch --all` | No errors |
@@ -455,7 +455,142 @@ openclaw config get plugins.entries.memory-qdrant
./push-all.sh
```
### 11.2 Sign-off Checklist
---
## 12. Known Issues & Solutions (Discovered During Deployment)
### 12.1 Gem Quality Issues
| Issue | Cause | Solution | Status |
|-------|-------|----------|--------|
| **Malformed importance** | Model outputs `3` instead of `"high"` | Strengthen prompt: "MUST be string, NEVER 1/2/3" | ✅ Fixed in curator-prompt.md |
| **Malformed confidence** | Model outputs `"0.7"` instead of `0.7` | Strengthen prompt: "MUST be float, NEVER integers" | ✅ Fixed in curator-prompt.md |
| **"User confirmed..." pattern** | Model wraps insights in passive voice | Add rule: "NEVER start with 'User confirmed/asked/said'" | ✅ Fixed in curator-prompt.md |
| **Duplicate gems** | Same decisions extracted multiple times | Add deduplication check (similarity >0.85 = skip) | ✅ Documented in README |
| **Low-quality 4b gems** | 4b model lacks nuance | Use 30b curator for production | ✅ Documented in README |
**Prevention:** See Section 6.3 (Duplication Check) and curator-prompt.md "Critical Validation Rules"
### 12.2 OpenClaw UI Issues
| Issue | Cause | Workaround | Status |
|-------|-------|------------|--------|
| **Text invisible after compaction** | WebSocket sync during history replacement | Wait 2-3s or hard refresh (Ctrl+Shift+R) | ⚠️ Known limitation |
| **Messages out of order** | UI state gets briefly desynced | Hard refresh if stuck | ⚠️ Known limitation |
**Note:** These are OpenClaw Control UI limitations, not fixable from TrueRecall side.
### 12.3 v1 Migration Issues
| Issue | Cause | Solution | Status |
|-------|-------|----------|--------|
| **v1 conflicts** | Both v1 and v2 running simultaneously | Remove v1 completely before installing v2 | ✅ Installer detects v1 and warns |
| **Duplicate cron jobs** | v1 curator still running | Remove v1 cron entries | ✅ Documented in installer |
| **Collection naming** | v1 uses `memories`, v2 uses `memories_tr` | Collections are separate, no data conflict | ✅ By design |
**Prevention:** Installer now detects v1 and requires acknowledgment before proceeding.
### 12.4 Repository & Security Issues
| Issue | Cause | Solution | Status |
|-------|-------|----------|--------|
| **Private IPs in code** | Hardcoded 10.0.0.x addresses | Replace with placeholders (`<QDRANT_IP>`, etc.) | ✅ Fixed in main_projects |
| **Absolute paths** | `/root/`, `/home/n8n/` in docs | Replace with `~/` or placeholders | ✅ Fixed in main_projects |
| **Backup files committed** | `*.bak`, `*.neuralstream.bak` | Removed + added `.gitignore` | ✅ Fixed |
| **Session notes in repo** | `session.md` with timestamps | Removed from git, kept local | ✅ Fixed |
| **Debug files committed** | `debug_curator.py`, `test_*.py` | Removed + `.gitignore` | ✅ Fixed |
| **Repo visibility** | Initially set to private (should have asked) | Changed to public for sharing | ✅ Fixed |
**Prevention:** See Section 7 (Security & Privacy Review) and `.gitignore`
### 12.5 Multi-Remote Git Issues
| Issue | Cause | Solution | Status |
|-------|-------|----------|--------|
| **GitHub blocked** | Account issues | Skip GitHub in push-all.sh | ✅ Configured |
| **Token in remote URL** | Security risk | Remove after push, use credential helper | ✅ Fixed |
| **Remote naming** | `origin` vs `github` | Renamed to `github` for clarity | ✅ Fixed |
### 12.6 Model-Specific Issues
| Issue | Cause | Solution | Status |
|-------|-------|----------|--------|
| **4b JSON reliability** | Smaller model struggles with JSON output | Add JSON fallback/parsing retry | ✅ Documented |
| **30b speed** | 30b slower than 4b | Acceptable for quality (~3s vs ~10s) | ✅ Documented |
| **Embedding model choice** | `snowflake` vs `mxbai` | `mxbai` = higher accuracy, `snowflake` = faster | ✅ Documented |
### 12.7 Checklist for Future Deployments
Before releasing/sharing:
- [ ] Run all commands in Section 7.1 (Security Checks)
- [ ] Verify no `*.bak` files exist: `find . -name "*.bak"`
- [ ] Verify no `__pycache__` committed
- [ ] Verify no session-specific timestamps in markdown
- [ ] Verify no debug/test files in repo
- [ ] Check `.gitignore` is comprehensive
- [ ] Test installer in dry-run mode
- [ ] Verify v1 detection works
- [ ] Check all remotes push successfully
- [ ] Verify public repo has no sensitive data
---
#### 12.8 Plugin Memory Injection Fix (2026-02-25)
| Issue | Cause | Solution | Status |
|-------|-------|----------|--------|
| **HTML comments visible in UI** | `formatRelevantMemoriesContext` wrapped memories in HTML comments | Changed to clean text: "Memory Injection: Historical context..." | ✅ Fixed |
| **prependContext vs systemPrompt** | Plugin was returning `prependContext` which injects into user message (visible) | Changed to `systemPrompt` which injects into system prompt (hidden) | ✅ Fixed |
| **TypeScript source not updated** | OpenClaw compiles from `.ts`, edits were only to `.js` | Updated both `index.ts` and `index.js` | ✅ Fixed |
| **Gateway restart needed** | Plugin changes require gateway restart to take effect | Restarted gateway after file updates | ✅ Fixed |
**Files Modified:**
- `/root/.openclaw/extensions/memory-qdrant/index.ts` - Main TypeScript source
- `/root/.openclaw/extensions/memory-qdrant/index.js` - Compiled JavaScript
**What Changed:**
```typescript
// Before:
function formatRelevantMemoriesContext(memories) {
return `<!-- relevant-memories-start -->
<relevant-memories>
...`;
}
return { prependContext: formatRelevantMemoriesContext(...) };
// After:
function formatRelevantMemoriesContext(memories) {
return `Memory Injection: Historical context from previous conversations:
1. [category] text`;
}
return { systemPrompt: formatRelevantMemoriesContext(...) };
```
**Verification:**
- [ ] Send test message - memories appear as clean text, not HTML
- [ ] Memories inject into system prompt (not user-visible message)
- [ ] Both `.ts` and `.js` files updated consistently
- [ ] Gateway restarted and running
---
### 12.9 README Update
| Issue | Description | Status |
|-------|-------------|--------|
| **Standalone vs Addon** | README clarified: TrueRecall v2 is standalone, not addon | ✅ **FIXED** |
| **Architecture description** | Updated: v2 is complete replacement of Jarvis Memory and v1 | ✅ **FIXED** |
**Changes Made:**
- [x] Updated README Overview section
- [x] Added "standalone" declaration with comparison table
- [x] Clarified relationship: Jarvis (legacy) → v1 (deprecated) → v2 (active)
- [x] Added note: v2 requires no components from previous systems
---
## 13. Sign-off Checklist
| Section | Status | Date | Checked By |
|---------|--------|------|------------|
@@ -471,6 +606,7 @@ openclaw config get plugins.entries.memory-qdrant
| Security review | ⏳ | | |
| Multi-remote git | ⏳ | | |
| Backup verified | ⏳ | | |
| Known issues reviewed | ⏳ | | |
**Final Sign-off:** _______________
**Date:** _______________

View File

@@ -1,6 +1,6 @@
# The Curator System Prompt
You are The Curator, a discerning AI expert in memory preservation for True-Recall-Out. Like a museum curator selecting priceless artifacts for an exhibit, you exercise careful judgment to identify and preserve only the most valuable "gems" from conversations—moments that truly matter for long-term recall. You are not a hoarder; you focus on substance, context, and lasting value, discarding noise to create a meaningful archive. You run daily at 3 AM, processing 24 hours of conversation data from Redis (a temporary buffer at REDIS_HOST:REDIS_PORT, key pattern 'mem:user_id', list of JSON strings with 24-hour TTL). You treat the entire input as one cohesive narrative story, not isolated messages, to uncover arcs, patterns, and pivotal moments. After extracting gems, you store them in Qdrant (vector database at http://10.0.0.40:6333, collection 'kimi_memories', using snowflake-arctic-embed2 with 1024 dimensions and cosine similarity; payload is the full gem object). Then, clear the Redis buffer. Your input is a JSON array of conversation turns. Each turn object includes: user_id (speaker), user_message (user's text), ai_response (AI's text), turn (number), timestamp (ISO 8601, e.g., "2026-02-22T14:30:00"), date (YYYY-MM-DD, e.g., "2026-02-22"), conversation_id (unique string, e.g., "abc123"). Example input snippet: [ { "user_id": "rob", "user_message": "Should I use Redis or Postgres for caching?", "ai_response": "For short-term caching, Redis is faster; Postgres is better for persistence.", "turn": 15, "timestamp": "2026-02-22T14:28:00", "date": "2026-02-22", "conversation_id": "abc123" }, { "user_id": "rob", "user_message": "I decided on Redis. Speed matters more for this use case.", "ai_response": "Good choice; Redis will handle the caching layer efficiently.", "turn": 16, "timestamp": "2026-02-22T14:30:00", "date": "2026-02-22", "conversation_id": "abc123" } ] Your task: Read the full narrative, identify gems (important moments like decisions or insights), extract them with rich details, and output a JSON array of gems. If no gems, return an empty array []. Each gem MUST have exactly these 11 required fields (all present, no extras): - "gem": String, 1-2 sentences summarizing the main insight/decision (e.g., "User decided to use Redis over Postgres for memory system caching."). - "context": String, 2-3 sentences explaining why it matters (e.g., "After discussing tradeoffs between persistence versus speed for short-term storage, user prioritized speed over data durability. This choice impacts system performance."). - "snippet": String, raw conversation excerpt (2-3 turns, with speakers, e.g., "rob: Should I use Redis or Postgres for caching? Kimi: For short-term caching, Redis is faster; Postgres is better for persistence. rob: I decided on Redis. Speed matters more for this use case."). - "categories": Array of strings, tags like ["decision", "technical", "preference", "project", "knowledge", "insight", "plan", "architecture", "workflow"] (non-empty, 1-5 items). - "importance": String, "high", "medium", or "low" (must be medium or high for storage). - "confidence": Float, 0.0-1.0 (must be >=0.6; target 0.8+). - "timestamp": String, exact ISO 8601 from the last turn in the range (e.g., "2026-02-22T14:30:00"). - "date": String, YYYY-MM-DD from timestamp (e.g., "2026-02-22"). - "conversation_id": String, from input (e.g., "abc123"). - "turn_range": String, first-last turn (e.g., "15-16"). - "source_turns": Array of integers, all turns involved (e.g., [15, 16]). Output strictly as JSON array, no extra text. ### What Makes a Gem Extract gems only for: - Decisions: User chooses one option (e.g., "I decided on Redis", "Let's go with Mattermost", "I'm switching to Linux"). - Technical solutions: Problem-solving methods (e.g., "Use Python asyncio", "Fix by increasing timeout", "Deploy with Docker Compose"). - Preferences: Likes/dislikes (e.g., "I prefer dark mode", "I hate popups", "Local is better than cloud"). - Projects: Work details (e.g., "Building a memory system", "Setting up True-Recall", "Working on the website"). - Knowledge: Learned facts (e1. **Timestamp:** Use the exact ISO 8601 from the final turn where the gem crystallized (e.g., decision finalized).
You are The Curator, a discerning AI expert in memory preservation for True-Recall-Out. Like a museum curator selecting priceless artifacts for an exhibit, you exercise careful judgment to identify and preserve only the most valuable "gems" from conversations—moments that truly matter for long-term recall. You are not a hoarder; you focus on substance, context, and lasting value, discarding noise to create a meaningful archive. You run daily at 3 AM, processing 24 hours of conversation data from Redis (a temporary buffer at REDIS_HOST:REDIS_PORT, key pattern 'mem:user_id', list of JSON strings with 24-hour TTL). You treat the entire input as one cohesive narrative story, not isolated messages, to uncover arcs, patterns, and pivotal moments. After extracting gems, you store them in Qdrant (vector database at http://<QDRANT_IP>:6333, collection 'kimi_memories', using snowflake-arctic-embed2 with 1024 dimensions and cosine similarity; payload is the full gem object). Then, clear the Redis buffer. Your input is a JSON array of conversation turns. Each turn object includes: user_id (speaker), user_message (user's text), ai_response (AI's text), turn (number), timestamp (ISO 8601, e.g., "2026-02-22T14:30:00"), date (YYYY-MM-DD, e.g., "2026-02-22"), conversation_id (unique string, e.g., "abc123"). Example input snippet: [ { "user_id": "rob", "user_message": "Should I use Redis or Postgres for caching?", "ai_response": "For short-term caching, Redis is faster; Postgres is better for persistence.", "turn": 15, "timestamp": "2026-02-22T14:28:00", "date": "2026-02-22", "conversation_id": "abc123" }, { "user_id": "rob", "user_message": "I decided on Redis. Speed matters more for this use case.", "ai_response": "Good choice; Redis will handle the caching layer efficiently.", "turn": 16, "timestamp": "2026-02-22T14:30:00", "date": "2026-02-22", "conversation_id": "abc123" } ] Your task: Read the full narrative, identify gems (important moments like decisions or insights), extract them with rich details, and output a JSON array of gems. If no gems, return an empty array []. Each gem MUST have exactly these 11 required fields (all present, no extras): - "gem": String, 1-2 sentences summarizing the main insight/decision (e.g., "User decided to use Redis over Postgres for memory system caching."). - "context": String, 2-3 sentences explaining why it matters (e.g., "After discussing tradeoffs between persistence versus speed for short-term storage, user prioritized speed over data durability. This choice impacts system performance."). - "snippet": String, raw conversation excerpt (2-3 turns, with speakers, e.g., "rob: Should I use Redis or Postgres for caching? Kimi: For short-term caching, Redis is faster; Postgres is better for persistence. rob: I decided on Redis. Speed matters more for this use case."). - "categories": Array of strings, tags like ["decision", "technical", "preference", "project", "knowledge", "insight", "plan", "architecture", "workflow"] (non-empty, 1-5 items). - "importance": String, "high", "medium", or "low" (must be medium or high for storage). - "confidence": Float, 0.0-1.0 (must be >=0.6; target 0.8+). - "timestamp": String, exact ISO 8601 from the last turn in the range (e.g., "2026-02-22T14:30:00"). - "date": String, YYYY-MM-DD from timestamp (e.g., "2026-02-22"). - "conversation_id": String, from input (e.g., "abc123"). - "turn_range": String, first-last turn (e.g., "15-16"). - "source_turns": Array of integers, all turns involved (e.g., [15, 16]). Output strictly as JSON array, no extra text. ### What Makes a Gem Extract gems only for: - Decisions: User chooses one option (e.g., "I decided on Redis", "Let's go with Mattermost", "I'm switching to Linux"). - Technical solutions: Problem-solving methods (e.g., "Use Python asyncio", "Fix by increasing timeout", "Deploy with Docker Compose"). - Preferences: Likes/dislikes (e.g., "I prefer dark mode", "I hate popups", "Local is better than cloud"). - Projects: Work details (e.g., "Building a memory system", "Setting up True-Recall", "Working on the website"). - Knowledge: Learned facts (e1. **Timestamp:** Use the exact ISO 8601 from the final turn where the gem crystallized (e.g., decision finalized).
2. **Date:** Derive as YYYY-MM-DD from timestamp.
3. **Conversation_id:** Copy from input (consistent across turns).
4. **Turn_range:** "first-last" (e.g., "15-16" for contiguous; "15-16,18" if non-contiguous but prefer contiguous).

View File

@@ -1,106 +0,0 @@
#!/usr/bin/env python3
"""Debug curator with real data"""
import json
import requests
import urllib.request
QDRANT_URL = "http://10.0.0.40:6333"
SOURCE_COLLECTION = "kimi_memories"
# Get sample turns from real data
filter_data = {
"must": [
{"key": "user_id", "match": {"value": "rob"}},
{"key": "date", "match": {"value": "2026-02-23"}}
]
}
req = urllib.request.Request(
f"{QDRANT_URL}/collections/{SOURCE_COLLECTION}/points/scroll",
data=json.dumps({"limit": 5, "with_payload": True, "filter": filter_data}).encode(),
headers={"Content-Type": "application/json"},
method="POST"
)
with urllib.request.urlopen(req, timeout=30) as response:
result = json.loads(response.read().decode())
points = result.get("result", {}).get("points", [])
turns = []
for point in points:
payload = point.get("payload", {})
user_msg = payload.get("user_message", "")
ai_msg = payload.get("ai_response", "")
if user_msg or ai_msg:
turn = {
"turn": payload.get("turn_number", 0),
"user_id": payload.get("user_id", "rob"),
"user": user_msg[:300], # Truncate
"ai": ai_msg[:300], # Truncate
"conversation_id": payload.get("conversation_id", ""),
"timestamp": payload.get("created_at", ""),
"date": payload.get("date", "2026-02-23")
}
turns.append(turn)
turns.sort(key=lambda x: (x.get("conversation_id", ""), x.get("turn", 0)))
print(f"Got {len(turns)} turns")
print("Sample:")
for t in turns[:2]:
print(f" User: {t['user'][:100]}...")
print(f" AI: {t['ai'][:100]}...")
# Now test with curator
with open('/root/.openclaw/workspace/.projects/true-recall-v1/curator-prompt.md') as f:
prompt = f.read()
conversation_json = json.dumps(turns[:5], indent=2)
prompt_text = f"""## Input Conversation
```json
{conversation_json}
```
## Output
"""
response = requests.post(
'http://10.0.0.10:11434/api/generate',
json={
'model': 'qwen3:4b-instruct',
'system': prompt,
'prompt': prompt_text,
'stream': False,
'options': {'temperature': 0.1, 'num_predict': 3000}
},
timeout=120
)
result = response.json()
output = result.get('response', '').strip()
print("\n=== CURATOR OUTPUT ===")
print(output[:3000])
print("\n=== TRYING TO PARSE ===")
# Try to parse
try:
if '```json' in output:
parsed = output.split('```json')[1].split('```')[0].strip()
gems = json.loads(parsed)
print(f"Parsed {len(gems)} gems")
elif '```' in output:
parsed = output.split('```')[1].split('```')[0].strip()
gems = json.loads(parsed)
print(f"Parsed {len(gems)} gems")
else:
gems = json.loads(output)
print(f"Parsed {len(gems)} gems")
except Exception as e:
print(f"Parse error: {e}")
print("Trying raw parse...")
gems = json.loads(output.strip())
print(f"Parsed {len(gems)} gems")

216
function_check.md Normal file
View File

@@ -0,0 +1,216 @@
# TrueRecall v2 - Function Check (LOCAL)
**Quick validation checklist for OUR TrueRecall v2 setup**
**User:** rob
**Qdrant:** http://<QDRANT_IP>:6333
**Ollama:** http://<OLLAMA_IP>:11434
**Timer:** 5 minutes
**Working Dir:** ~/.openclaw/workspace/.local_projects/true-recall-gems
---
## Quick Status Check
```bash
cd ~/.openclaw/workspace/.local_projects/true-recall-gems
```
---
## 1. Directory Structure
| Check | Command | Expected |
|-------|---------|----------|
| Local project exists | `ls ~/.openclaw/workspace/.local_projects/true-recall-gems` | Files listed |
| Git project exists | `ls ~/.openclaw/workspace/.git_projects/true-recall-gems` | Files listed |
| Watcher script | `ls ~/.openclaw/workspace/skills/qdrant-memory/scripts/realtime_qdrant_watcher.py` | File exists |
**Our Paths:**
- Local: `~/.openclaw/workspace/.local_projects/true-recall-gems/`
- Git: `~/.openclaw/workspace/.git_projects/true-recall-gems/`
- Watcher: `~/.openclaw/workspace/skills/qdrant-memory/scripts/realtime_qdrant_watcher.py`
- Systemd: `/etc/systemd/system/mem-qdrant-watcher.service`
---
## 2. Services
| Check | Command | Expected |
|-------|---------|----------|
| Watcher running | `systemctl is-active mem-qdrant-watcher` | `active` |
| Watcher enabled | `systemctl is-enabled mem-qdrant-watcher` | `enabled` |
| Cron job set | `crontab -l \| grep true-recall` | `*/5 * * * *` entry |
**Our Service:**
- Service: `mem-qdrant-watcher.service`
- Status: `systemctl status mem-qdrant-watcher --no-pager`
- Logs: `journalctl -u mem-qdrant-watcher -n 20`
- Cron: `*/5 * * * *` (every 5 minutes)
---
## 3. Qdrant Collections
| Check | Command | Expected |
|-------|---------|----------|
| memories_tr status | `curl -s http://<QDRANT_IP>:6333/collections/memories_tr \| jq .result.status` | `green` |
| gems_tr status | `curl -s http://<QDRANT_IP>:6333/collections/gems_tr \| jq .result.status` | `green` |
| memories_tr count | `curl -s http://<QDRANT_IP>:6333/collections/memories_tr \| jq .result.points_count` | `12000+` |
| gems_tr count | `curl -s http://<QDRANT_IP>:6333/collections/gems_tr \| jq .result.points_count` | `70+` |
**Our Qdrant:**
- URL: http://<QDRANT_IP>:6333
- Collections: memories_tr, gems_tr
- Embedding Model: snowflake-arctic-embed2
---
## 4. Curation Status
| Check | Command | Expected |
|-------|---------|----------|
| Uncurated count | See Section 7 | `1490` |
| Curated count | See Section 7 | `11239` |
| Curator config | `cat tr-continuous/curator_config.json` | `timer_minutes: 5` |
**Our Config:**
- Timer: 5 minutes (`*/5 * * * *`)
- Batch Size: 100
- User ID: rob
- Source: memories_tr
- Target: gems_tr
- Curator Log: `/var/log/true-recall-timer.log`
---
## 5. Capture Test
| Step | Action | Check |
|------|--------|-------|
| 1 | Send a test message to Kimi | Message received |
| 2 | Wait 10 seconds | Allow processing |
| 3 | Check memories count increased | `curl -s http://<QDRANT_IP>:6333/collections/memories_tr \| jq .result.points_count` |
| 4 | Verify memory has user_id | `user_id: "rob"` in payload |
| 5 | Verify memory has curated=false | `curated: false` in payload |
**Our Watcher:**
- Script: `~/.openclaw/workspace/skills/qdrant-memory/scripts/realtime_qdrant_watcher.py`
- User ID: `rob`
- Collection: `memories_tr`
- Embeddings: `snowflake-arctic-embed2`
---
## 6. Curation Test
| Step | Action | Check |
|------|--------|-------|
| 1 | Note current gems count | Baseline |
| 2 | Run curator manually | `cd tr-continuous && python3 curator_timer.py` |
| 3 | Check gems count increased | New gems added |
| 4 | Check memories marked curated | `curated: true` |
| 5 | Check curator log | `tail /var/log/true-recall-timer.log` |
---
## 7. Recall Test ✅ **WORKING**
| Step | Action | Check |
|------|--------|-------|
| 1 | Start new conversation | Context loads |
| 2 | Ask about previous topic | Gems injected |
| 3 | Verify context visible | ✅ **Score 0.587** - Working! |
**Verified 2026-02-25:** Context injection successfully returns relevant gems with similarity scores above threshold (0.5+).
---
## 8. Path Validation
| Path | Check | Status |
|------|-------|--------|
| Watcher script | `skills/qdrant-memory/scripts/realtime_qdrant_watcher.py` | ☐ |
| Curator script | `.local_projects/true-recall-gems/tr-continuous/curator_timer.py` | ☐ |
| Config file | `.local_projects/true-recall-gems/tr-continuous/curator_config.json` | ☐ |
| Log file | `/var/log/true-recall-timer.log` | ☐ |
---
## 9. Quick Commands Reference
```bash
# Check all services
systemctl status mem-qdrant-watcher --no-pager
tail -20 /var/log/true-recall-timer.log
# Check Qdrant collections (Our Qdrant: <QDRANT_IP>:6333)
curl -s http://<QDRANT_IP>:6333/collections/memories_tr | jq '{status: .result.status, points: .result.points_count}'
curl -s http://<QDRANT_IP>:6333/collections/gems_tr | jq '{status: .result.status, points: .result.points_count}'
# Check uncurated memories (Our user_id: rob)
curl -s -X POST http://<QDRANT_IP>:6333/collections/memories_tr/points/count \
-d '{"filter":{"must":[{"key":"user_id","match":{"value":"rob"}},{"key":"curated","match":{"value":false}}]}}' | jq .result.count
# Run curator manually (Our path: .local_projects)
cd ~/.openclaw/workspace/.local_projects/true-recall-gems/tr-continuous
python3 curator_timer.py
# Check OpenClaw plugin
openclaw status | grep memory-qdrant
# Restart watcher (if needed)
sudo systemctl restart mem-qdrant-watcher
# View watcher logs
journalctl -u mem-qdrant-watcher -n 50 --no-pager
```
---
## Recent Fixes (2026-02-25)
| Issue | Status | Fix |
|-------|--------|-----|
| Embedding model mismatch | ✅ Fixed | Changed curator from `mxbai-embed-large` to `snowflake-arctic-embed2` |
| Gems had no vectors | ✅ Fixed | Updated `store_gem()` to use `text` field |
| JSON parsing errors | ✅ Fixed | Simplified extraction prompt |
| Field mismatch | ✅ Fixed | Curator now supports both `text` and `content` fields |
| Context injection | ✅ **WORKING** | Verified with score 0.587 on test query |
| **Watcher session bug** | ✅ **Fixed 12:22** | Watcher was stuck on old session, restarted and now follows current session |
| **Plugin capture** | ✅ **Fixed 12:34** | Added `extractMessageText()` to handle OpenAI-style content arrays |
| **Plugin exchanges** | ✅ **Verified 12:41** | Now extracting exchanges: parsed 17 user, 116 assistant, 9 exchanges |
| **Gem ID collision** | ✅ **Fixed 12:50** | Hash now uses `embedding_text_for_hash[:100]` instead of empty fields |
| **Meta-gem filtering** | ✅ **Fixed 12:52** | Curator skips patterns: "gems extracted", "curator", "✅", "🔍", debug messages, system messages |
| **gems_tr cleaned** | ✅ **Done 12:53** | Removed 5 meta-gems, kept 1 real gem |
| **Gem format (1st person)** | ✅ **Fixed 13:15** | Changed from "User decided..." to "I decided..." for better query matching |
### Needed Improvements
| Issue | Description | Priority |
|-------|-------------|----------|
| **Semantic Deduplication** | No dedup between similar gems. Same fact phrased differently creates multiple gems. | High |
| **Search Result Deduplication** | Similar gems both injected, causing redundancy. | Medium |
| **Gem Quality Scoring** | No quality metric for gems. | Medium |
| **Temporal Decay** | All gems treated equally regardless of age. | Low |
| **Gem Merging/Updating** | Old gems not updated when preferences change. | Low |
| **Importance Calibration** | All curator gems marked "medium" importance. | Low |
**Result:** Context injection now functional. Gems are embedded and searchable. Both watcher and plugin capture working.
| Check | Date | Status |
|-------|------|--------|
| All services running | 2026-02-25 | ✅ |
| Collections healthy | 2026-02-25 | ✅ |
| Capture working | 2026-02-25 | ✅ |
| Curation working | 2026-02-25 | ✅ |
| Recall working | 2026-02-25 | ✅ **Context injection verified** |
---
*Last updated: 2026-02-25 12:04 CST*
*User: rob*
*Qdrant: <QDRANT_IP>:6333*
*Timer: 5 minutes*
*Collections: memories_tr (12,729), gems_tr (14+)*
*Status: ✅ Context injection WORKING*

View File

@@ -1,187 +0,0 @@
#!/usr/bin/env python3
"""
Migrate memories from kimi_memories to memories_tr
- Reads from kimi_memories (Qdrant)
- Cleans/strips noise (metadata, thinking tags)
- Stores to memories_tr (Qdrant)
- Keeps original kimi_memories intact
"""
import json
import urllib.request
import urllib.error
from datetime import datetime
from typing import List, Dict, Any
QDRANT_URL = "http://10.0.0.40:6333"
SOURCE_COLLECTION = "kimi_memories"
TARGET_COLLECTION = "memories_tr"
def clean_content(text: str) -> str:
"""Clean noise from content"""
if not text:
return ""
cleaned = text
# Remove metadata JSON blocks
import re
cleaned = re.sub(r'Conversation info \(untrusted metadata\):\s*```json\s*\{[\s\S]*?\}\s*```', '', cleaned)
# Remove thinking tags
cleaned = re.sub(r'\[thinking:[^\]]*\]', '', cleaned)
# Remove timestamp lines
cleaned = re.sub(r'\[\w{3} \d{4}-\d{2}-\d{2} \d{2}:\d{2} [A-Z]{3}\]', '', cleaned)
# Clean up whitespace
cleaned = re.sub(r'\n{3,}', '\n\n', cleaned)
cleaned = cleaned.strip()
return cleaned
def get_all_points(collection: str) -> List[Dict]:
"""Get all points from a collection"""
all_points = []
offset = None
max_iterations = 1000
iterations = 0
while iterations < max_iterations:
iterations += 1
scroll_data = {
"limit": 100,
"with_payload": True,
"with_vector": True
}
if offset:
scroll_data["offset"] = offset
req = urllib.request.Request(
f"{QDRANT_URL}/collections/{collection}/points/scroll",
data=json.dumps(scroll_data).encode(),
headers={"Content-Type": "application/json"},
method="POST"
)
try:
with urllib.request.urlopen(req, timeout=60) as response:
result = json.loads(response.read().decode())
points = result.get("result", {}).get("points", [])
if not points:
break
all_points.extend(points)
offset = result.get("result", {}).get("next_page_offset")
if not offset:
break
except urllib.error.HTTPError as e:
print(f"Error: {e}")
break
return all_points
def store_points(collection: str, points: List[Dict]) -> int:
"""Store points to collection"""
if not points:
return 0
# Batch upload
batch_size = 100
stored = 0
for i in range(0, len(points), batch_size):
batch = points[i:i+batch_size]
points_data = {
"points": batch
}
req = urllib.request.Request(
f"{QDRANT_URL}/collections/{collection}/points",
data=json.dumps(points_data).encode(),
headers={"Content-Type": "application/json"},
method="PUT"
)
try:
with urllib.request.urlopen(req, timeout=60) as response:
if response.status == 200:
stored += len(batch)
except urllib.error.HTTPError as e:
print(f"Error storing batch: {e}")
return stored
def migrate_point(point: Dict) -> Dict:
"""Clean a single point"""
payload = point.get("payload", {})
# Clean user and AI messages
user_msg = clean_content(payload.get("user_message", ""))
ai_msg = clean_content(payload.get("ai_response", ""))
# Keep other fields
cleaned_payload = {
**payload,
"user_message": user_msg,
"ai_response": ai_msg,
"migrated_from": "kimi_memories",
"migrated_at": datetime.now().isoformat()
}
return {
"id": point.get("id"),
"vector": point.get("vector"),
"payload": cleaned_payload
}
def main():
print("=" * 60)
print("Memory Migration: kimi_memories → memories_tr")
print("=" * 60)
print()
# Check source
print(f"📥 Reading from {SOURCE_COLLECTION}...")
source_points = get_all_points(SOURCE_COLLECTION)
print(f" Found {len(source_points)} points")
if not source_points:
print("❌ No points to migrate")
return
# Clean points
print(f"\n🧹 Cleaning {len(source_points)} points...")
cleaned_points = [migrate_point(p) for p in source_points]
print(f" ✓ Cleaned")
# Store to target
print(f"\n💾 Storing to {TARGET_COLLECTION}...")
stored = store_points(TARGET_COLLECTION, cleaned_points)
print(f" ✓ Stored {stored} points")
# Verify
print(f"\n🔍 Verifying...")
target_points = get_all_points(TARGET_COLLECTION)
print(f" Target now has {len(target_points)} points")
# Summary
print()
print("=" * 60)
print("Migration Summary:")
print(f" Source ({SOURCE_COLLECTION}): {len(source_points)} points")
print(f" Target ({TARGET_COLLECTION}): {len(target_points)} points")
print(f" Cleaned & migrated: {stored} points")
print("=" * 60)
if stored == len(source_points):
print("\n✅ Migration complete!")
else:
print(f"\n⚠️ Warning: Only migrated {stored}/{len(source_points)} points")
if __name__ == "__main__":
main()

View File

@@ -8,7 +8,7 @@
set -e
REPO_DIR="/root/.openclaw/workspace/.main_projects/true-recall-v2"
REPO_DIR="$(cd "$(dirname "$0")" && pwd)"
cd "$REPO_DIR"
# Colors for output

View File

@@ -1,494 +0,0 @@
# TrueRecall v2 - Session Notes
**Last Updated:** 2026-02-24 19:02 CST
**Status:** ✅ Active & Verified
**Version:** v2.2 (Timer-based curation deployed)
---
## Session End (18:09 CST)
**Reason:** User starting new session
**Current State:**
- Real-time watcher: ✅ Active (capturing live)
- Timer curator: ✅ Deployed (every 30 min via cron)
- Daily curator: ❌ Removed (replaced by timer)
- Total memories: 12,378 (all tagged with `curated: false`)
- Gems: 5 (from Feb 18 test)
**Next session start:** Read this file, then check:
```bash
# Quick status
python3 ~/.openclaw/workspace/.projects/true-recall-v2/tr-continuous/curator_by_count.py --status
sudo systemctl status mem-qdrant-watcher
curl -s http://<QDRANT_IP>:6333/collections/memories_tr | jq '.result.points_count'
```
---
## Executive Summary
TrueRecall v2 is a complete memory system with real-time capture, daily curation, and context injection. All components are operational.
---
## Current State (Verified 18:09 CST)
### Qdrant Collections
| Collection | Points | Purpose | Status |
|------------|--------|---------|--------|
| `memories_tr` | **12,378** | Full text (live capture) | ✅ Active |
| `gems_tr` | **5** | Curated gems (injection) | ✅ Active |
| `true_recall` | existing | Legacy archive | 📦 Preserved |
| `kimi_memories` | 12,223 | Original backup | 📦 Preserved |
**Note:** All memories tagged with `curated: false` for timer curator.
### Services
| Service | Status | Uptime |
|---------|--------|--------|
| `mem-qdrant-watcher` | ✅ Active | 30+ min |
| OpenClaw Gateway | ✅ Running | 2026.2.23 |
| memory-qdrant plugin | ✅ Loaded | recall: gems_tr, capture: memories_tr |
---
## Architecture
### v2.2: Timer-Based Curation (DEPLOYED)
**Data Flow:**
```
┌─────────────────┐ ┌──────────────────────┐ ┌─────────────┐
│ OpenClaw Chat │────▶│ Real-Time Watcher │────▶│ memories_tr │
│ (Session JSONL)│ │ (Python daemon) │ │ (Qdrant) │
└─────────────────┘ └──────────────────────┘ └──────┬──────┘
│ Every 30 min
┌──────────────────┐
│ Timer Curator │
│ (cron/qwen3) │
└────────┬─────────┘
┌──────────────────┐
│ gems_tr │
│ (Qdrant) │
└────────┬─────────┘
Per turn │
┌──────────────────┐
│ memory-qdrant │
│ plugin │
└──────────────────┘
```
**Key Changes:**
- ✅ Replaced daily 2:45 AM batch with 30-minute timer
- ✅ All memories tagged `curated: false` on write
- ✅ Migration completed for 12,378 existing memories
- ✅ No Redis dependency (direct Qdrant only)
---
## Components
### Curation Mode: Timer-Based (DEPLOYED v2.2)
| Setting | Value | Adjustable |
|---------|-------|------------|
| **Trigger** | Cron timer | ✅ |
| **Interval** | 30 minutes | ✅ Config file |
| **Batch size** | 100 memories max | ✅ Config file |
| **Minimum** | None (0 is OK) | — |
**Config:** `/tr-continuous/curator_config.json`
```json
{
"timer_minutes": 30,
"max_batch_size": 100,
"user_id": "rob",
"source_collection": "memories_tr",
"target_collection": "gems_tr"
}
```
**Cron:**
```
*/30 * * * * cd .../tr-continuous && python3 curator_timer.py
```
**Old modes deprecated:**
- ❌ Turn-based (every N turns)
- ❌ Hybrid (timer + turn)
- ❌ Daily batch (2:45 AM)
### 1. Real-Time Watcher (Primary Capture)
**Location:** `~/.openclaw/workspace/skills/qdrant-memory/scripts/realtime_qdrant_watcher.py`
**Function:**
- Watches `~/.openclaw/agents/main/sessions/*.jsonl`
- Parses every conversation turn in real-time
- Embeds with `snowflake-arctic-embed2` (Ollama @ <OLLAMA_IP>)
- Stores directly to `memories_tr` (no Redis)
- **Cleans content:** Removes markdown, tables, metadata, thinking tags
**Service:** `mem-qdrant-watcher.service`
- **Status:** Active since 16:46:53 CST
- **Systemd:** Enabled, auto-restart
**Log:** `journalctl -u mem-qdrant-watcher -f`
---
### 2. Content Cleaner (Existing Data)
**Location:** `~/.openclaw/workspace/skills/qdrant-memory/scripts/clean_memories_tr.py`
**Function:**
- Batch-cleans existing `memories_tr` points
- Removes: `**bold**`, `|tables|`, `` `code` ``, `---` rules, `# headers`
- Flattens nested content dicts
- Rate-limited to prevent Qdrant overload
**Usage:**
```bash
# Dry run (preview)
python3 clean_memories_tr.py --dry-run
# Clean all
python3 clean_memories_tr.py --execute
# Clean limited (test)
python3 clean_memories_tr.py --execute --limit 100
```
---
### 3. Timer Curator (v2.2 - DEPLOYED)
**Replaces:** Daily curator (2:45 AM batch) and turn-based curator
**Location:** `~/.openclaw/workspace/.projects/true-recall-v2/tr-continuous/curator_timer.py`
**Schedule:** Every 30 minutes (cron)
**Flow:**
1. Query uncurated memories (`curated: false`)
2. Send batch to qwen3 (max 100)
3. Extract gems using curator prompt
4. Store gems to `gems_tr`
5. Mark processed memories as `curated: true`
**Files:**
| File | Purpose |
|------|---------|
| `curator_timer.py` | Main curator script |
| `curator_config.json` | Adjustable settings |
| `migrate_add_curated.py` | One-time migration (completed) |
**Usage:**
```bash
# Dry run (preview)
python3 curator_timer.py --dry-run
# Manual run
python3 curator_timer.py --config curator_config.json
```
**Status:** ✅ Deployed, first run will process ~12,378 existing memories
### 5. Silent Compacting (NEW - Concept)
**Idea:** Automatically remove old context from prompt when token limit approached.
**Behavior:**
- Trigger: Context window > 80% full
- Action: Remove oldest messages (silently)
- Preserve: Gems always kept, recent N turns kept
- Result: Seamless conversation without "compacting" notification
**Config:**
```json
{
"compacting": {
"enabled": true,
"triggerAtPercent": 80,
"keepRecentTurns": 20,
"preserveGems": true,
"silent": true
}
}
```
**Status:** ⏳ Concept only - requires OpenClaw core changes
### 6. memory-qdrant Plugin
**Location:** `~/.openclaw/extensions/memory-qdrant/`
**Config:**
```json
{
"collectionName": "gems_tr",
"captureCollection": "memories_tr",
"autoRecall": true,
"autoCapture": true
}
```
**Function:**
- **Recall:** Searches `gems_tr`, injects as context (hidden)
- **Capture:** Session-level capture to `memories_tr` (backup)
**Status:** Loaded, dual collection support working
---
## Files & Locations
### Core Project Files
```
~/.openclaw/workspace/.projects/true-recall-v2/
├── README.md # Architecture docs
├── session.md # This file
├── curator-prompt.md # Gem extraction prompt
├── tr-daily/ # Daily batch curation
│ └── curate_from_qdrant.py # Daily curator (2:45 AM)
├── tr-continuous/ # Real-time curation (NEW)
│ ├── curator_by_count.py # Turn-based curator
│ ├── curator_turn_based.py # Alternative approach
│ ├── curator_cron.sh # Cron wrapper
│ ├── turn-curator.service # Systemd service
│ └── README.md # Documentation
└── shared/
└── (shared resources)
```
### New Files (2026-02-24 19:00)
| File | Purpose |
|------|---------|
| `tr-continuous/curator_timer.py` | Timer-based curator (deployed) |
| `tr-continuous/curator_config.json` | Curator settings |
| `tr-continuous/migrate_add_curated.py` | Migration script (completed) |
### Legacy Files (Pre-v2.2)
| File | Status | Note |
|------|--------|------|
| `tr-daily/curate_from_qdrant.py` | 📦 Archived | Replaced by timer |
| `tr-continuous/curator_by_count.py` | 📦 Archived | Replaced by timer |
| `tr-continuous/curator_turn_based.py` | 📦 Archived | Replaced by timer |
### System Locations
| File | Purpose |
|------|---------|
| `~/.openclaw/extensions/memory-qdrant/` | Plugin code |
| `~/.openclaw/openclaw.json` | Plugin configuration |
| `/etc/systemd/system/mem-qdrant-watcher.service` | Systemd service |
---
## Changes Made Today (2026-02-24 19:00)
### 1. Timer Curator Deployed (v2.2)
- Created `curator_timer.py` — simplified timer-based curation
- Created `curator_config.json` — adjustable settings
- Removed daily 2:45 AM cron job
- Added `*/30 * * * *` cron timer
- **Status:** ✅ Deployed, logs to `/var/log/true-recall-timer.log`
### 2. Migration Completed
- Created `migrate_add_curated.py`
- Tagged 12,378 existing memories with `curated: false`
- Updated watcher to add `curated: false` to new memories
- **Status:** ✅ Complete
### 3. Simplified Architecture
- ❌ Removed turn-based curator complexity
- ❌ Removed daily batch processing
- ✅ Single timer trigger every 30 minutes
- ✅ No minimum threshold (processes 0-N memories)
---
## Configuration
### memory-qdrant Plugin
**File:** `~/.openclaw/openclaw.json`
```json
{
"memory-qdrant": {
"config": {
"autoCapture": true,
"autoRecall": true,
"collectionName": "gems_tr",
"captureCollection": "memories_tr",
"embeddingModel": "snowflake-arctic-embed2",
"maxRecallResults": 2,
"minRecallScore": 0.7,
"ollamaUrl": "http://<OLLAMA_IP>:11434",
"qdrantUrl": "http://<QDRANT_IP>:6333"
},
"enabled": true
}
}
```
### Gateway (OpenClaw Update Fix)
```json
{
"gateway": {
"controlUi": {
"allowedOrigins": ["*"],
"allowInsecureAuth": false,
"dangerouslyDisableDeviceAuth": true
}
}
}
```
---
## Validation Commands
### Check Collections
```bash
# Points count
curl -s http://<QDRANT_IP>:6333/collections/memories_tr | jq '.result.points_count'
curl -s http://<QDRANT_IP>:6333/collections/gems_tr | jq '.result.points_count'
# Recent points
curl -s -X POST http://<QDRANT_IP>:6333/collections/memories_tr/points/scroll \
-H "Content-Type: application/json" \
-d '{"limit": 5, "with_payload": true}' | jq '.result.points[].payload.content'
```
### Check Services
```bash
# Watcher status
sudo systemctl status mem-qdrant-watcher
# Watcher logs
sudo journalctl -u mem-qdrant-watcher -n 20
# OpenClaw status
openclaw status
```
---
## Troubleshooting
### Issue: Watcher Not Capturing
**Check:**
1. Service running? `systemctl status mem-qdrant-watcher`
2. Logs: `journalctl -u mem-qdrant-watcher -f`
3. Qdrant accessible? `curl http://<QDRANT_IP>:6333/`
4. Ollama accessible? `curl http://<OLLAMA_IP>:11434/api/tags`
### Issue: Cleaner Fails
**Common causes:**
- Qdrant connection timeout (add `time.sleep(0.1)` between batches)
- Nested content dicts (handled in updated script)
- Type errors (non-string content — handled)
### Issue: Plugin Not Loading
**Check:**
1. `openclaw.json` syntax valid? `openclaw config validate`
2. Plugin compiled? `cd ~/.openclaw/extensions/memory-qdrant && npx tsc`
3. Gateway logs: `tail /tmp/openclaw/openclaw-$(date +%Y-%m-%d).log`
---
## Cron Schedule (Updated v2.2)
| Time | Job | Script | Status |
|------|-----|--------|--------|
| Every 30 min | Timer curator | `tr-continuous/curator_timer.py` | ✅ Active |
| Per turn | Capture | `mem-qdrant-watcher` | ✅ Daemon |
| Per turn | Injection | `memory-qdrant` plugin | ✅ Active |
**Removed:**
- ❌ 2:45 AM daily curator
- ❌ Every-minute turn curator check
---
## Next Steps
### Immediate
- ⏳ Monitor first timer run (logs: `/var/log/true-recall-timer.log`)
- ⏳ Validate gem extraction quality from timer curator
- ⏳ Archive old curator scripts if timer works
### Completed ✅
-**Compactor config** — Minimal overhead: `mode: default`, `reserveTokensFloor: 0`, `memoryFlush: false`
### Future
- ⏳ Curator tuning based on timer results
- ⏳ Silent compacting (requires OpenClaw core changes)
### Planned Features (Backlog)
-**Interactive install script** — Prompts for embedding model, timer interval, batch size, endpoints
-**Single embedding model option** — Use one model for both collections
-**Configurable thresholds** — Per-user customization via prompts
**Compactor Settings (Applied):**
```json5
{
agents: {
defaults: {
compaction: {
mode: "default",
reserveTokensFloor: 0,
memoryFlush: { enabled: false }
}
}
}
}
```
**Note:** Only `mode`, `reserveTokensFloor`, and `memoryFlush` are valid under `agents.defaults.compaction`. Other settings are Pi runtime parameters.
**Install script prompts:**
1. Embedding model (snowflake vs mxbai)
2. Timer interval (5 min / 30 min / hourly)
3. Batch size (50 / 100 / 500)
4. Qdrant/Ollama URLs
5. User ID
---
## Session Recovery
If starting fresh:
1. Read `README.md` for architecture overview
2. Check service status: `sudo systemctl status mem-qdrant-watcher`
3. Check timer curator: `tail /var/log/true-recall-timer.log`
4. Verify collections: `curl http://<QDRANT_IP>:6333/collections`
---
*Last Verified: 2026-02-24 19:29 CST*
*Version: v2.2 (30b curator, install script planned)*

View File

@@ -1,224 +0,0 @@
# TrueRecall v2 - Session Notes
**Last Updated:** 2026-02-24 15:08 CST
---
## Summary of Changes (2026-02-24)
### Collection Migration
| Action | From | To | Status |
|--------|------|----|--------|
| **Rename** | `kimi_memories` | `memories_tr` | ✅ Done + data migrated (12,223 points) |
| **Create** | — | `gems_tr` | ✅ Created (empty, ready for curation) |
| **Archive** | `true_recall` | `true_recall` | ✅ Preserved (existing data kept) |
### Dual Collection Support (NEW - 14:10)
**Problem:** Auto-capture was saving to `gems_tr` because that's where recall pulled from. This was wrong.
**Solution:** Added `captureCollection` option to memory-qdrant plugin:
- `collectionName`: `gems_tr` — for recall/injection (gems only)
- `captureCollection`: `memories_tr` — for auto-capture (full conversations)
**Files Modified:**
1. `/root/.openclaw/extensions/memory-qdrant/config.ts` — Added `captureCollection` option
2. `/root/.openclaw/extensions/memory-qdrant/index.ts` — Uses `dbRecall` and `dbCapture` separately
3. `/root/.openclaw/openclaw.json` — Added `"captureCollection": "memories_tr"`
### Auto-Capture Architecture (Real-Time Watcher)
**Mechanism:** `realtime_qdrant_watcher.py` daemon
- Watches OpenClaw session JSONL files in real-time
- Parses each conversation turn
- Embeds with `snowflake-arctic-embed2` via Ollama
- Stores directly to `memories_tr` collection (no Redis)
- Cleans content (removes metadata, thinking tags)
**Files:**
- Script: `/root/.openclaw/workspace/skills/qdrant-memory/scripts/realtime_qdrant_watcher.py`
- Service: `/root/.openclaw/workspace/skills/qdrant-memory/mem-qdrant-watcher.service`
**Status:** ✅ Deployed and running
**Deployment:**
- Stopped: `mem-redis-watcher`
- Started: `mem-qdrant-watcher`
- Status: Active (PID 84465)
- Verified: ✅ Capturing (points: 12,223 → 12,224)
### Current State (2026-02-24 15:08)
| Collection | Purpose | Points | Last Update |
|------------|---------|--------|-------------|
| `memories_tr` | Full text (autoCapture via watcher) | 12,228 | **Live** |
| `gems_tr` | Gems (for injection) | 5 | Feb 24 |
**Flow:**
```
OpenClaw Session → Real-Time Watcher → memories_tr (Qdrant)
Daily Curator (2:45 AM)
gems_tr (Qdrant)
memory-qdrant plugin → Injection
```
### Files Modified Today
1. `/root/.openclaw/openclaw.json` — Changed collectionName to `gems_tr`
2. `/root/.openclaw/extensions/memory-qdrant/config.ts` — Default to `memories_tr`
3. `/root/.openclaw/extensions/memory-qdrant/openclaw.plugin.json` — Defaults to `memories_tr`
4. `/root/.openclaw/workspace/skills/mem-redis/scripts/save_mem.py` — Added `--silent` flag
5. `/root/.openclaw/workspace/HEARTBEAT.md` — Updated to use `--silent`
6. `/root/.openclaw/workspace/.projects/true-recall-v2/tr-daily/curate_from_qdrant.py` — TARGET_COLLECTION = `gems_tr`
7. `/root/.openclaw/workspace/SOUL.md` — Updated collection references
8. `/root/.openclaw/workspace/kb/relevant-memories-ui-issue.md` — Updated documentation
### Migration Script Created
- `/root/.openclaw/workspace/.projects/true-recall-v2/migrate_memories.py`
- Migrated 12,223 points from `kimi_memories` → `memories_tr`
- Cleaned noise (metadata, thinking tags) during migration
- Preserved original `kimi_memories` as backup
### Current Collections (Qdrant)
| Collection | Purpose | Points |
|------------|---------|--------|
| `memories_tr` | Full text (live capture) | **12,228** |
| `gems_tr` | Gems (for injection) | 5 |
| `true_recall` | Legacy gems archive | existing |
| `kimi_memories` | Original (backup) | 12,223 |
---
## Final Status (2026-02-24)
| Component | Status |
|-----------|--------|
| **Collections** | 4 active (memories_tr, gems_tr, true_recall, kimi_memories) |
| **Curator v2** | ✅ Tested & working - 327 turns → 5 gems |
| **Config** | ✅ Using `gems_tr` for injection |
| **Cron** | ✅ Simplified - only essential jobs |
| **Redis** | ✅ Only for notifications (not memory) |
### Collection Summary
| Collection | Points | Purpose | Status |
|------------|--------|---------|--------|
| `memories_tr` | **12,228** | Full text (live capture) | ✅ Active |
| `gems_tr` | 5 | Gems (curated) | ✅ Active |
| `true_recall` | existing | Legacy archive | 📦 Preserved |
| `kimi_memories` | 12,223 | Original backup | 📦 Preserved |
### Cron Schedule (Cleaned)
| Time | Job |
|------|-----|
| 2:45 AM | Curator v2: memories_tr → gems_tr |
| 2:20 AM | File backup |
| 2:00 AM | Log monitoring |
**Removed:** 2:15 AM Redis backup, 2:50 AM archive, 5-min cron_capture (all redundant)
---
## What We Did Today
### Issue: `<relevant-memories>` showing in UI
**Problem:** The `<relevant-memories>` block was showing full conversation text in webchat UI. It should be hidden/internal.
**Investigation:**
1. Initially thought it was `save_mem.py` output (wrong)
2. Then thought it was HTML comment issue (wrong)
3. Found root cause: memory-qdrant plugin was recalling from `memories_tr` (full text) instead of `gems_tr` (gems)
**Solution Applied:**
Changed `openclaw.json` config:
- Before: `"collectionName": "memories_tr"`
- After: `"collectionName": "gems_tr"`
**Result:** ✅ Fixed! Now injection uses gems from true_recall, not full text.
### Issue: Auto-Capture Architecture Change
**Problem:** Real-time capture was going through Redis, we needed direct Qdrant storage
**Solution:**
1. Created `realtime_qdrant_watcher.py` — watches session JSONL, embeds, stores directly to Qdrant
2. Created `mem-qdrant-watcher.service` — systemd service for the watcher
3. Deployed and verified:
- Points before: 12,223
- Points after test messages: 12,228
- Verified new captures have correct structure (role, content, date, source)
**Status:** ✅ **Deployed and working**
### Other Changes Made
1. **Plugin dual collection support** — Added `captureCollection` option
2. **Session/Readme updates** — Clarified architecture
### Files Modified
| File | Change |
|------|--------|
| `/root/.openclaw/openclaw.json` | collectionName: memories_tr → gems_tr, added captureCollection |
| `/root/.openclaw/extensions/memory-qdrant/config.ts` | Added captureCollection option |
| `/root/.openclaw/extensions/memory-qdrant/index.ts` | Dual collection support, debug logging |
| `/root/.openclaw/extensions/memory-qdrant/openclaw.plugin.json` | Added captureCollection to schema |
### Files Created (NEW)
| File | Purpose |
|------|---------|
| `/root/.openclaw/workspace/skills/qdrant-memory/scripts/realtime_qdrant_watcher.py` | Real-time watcher daemon (Qdrant direct) |
| `/root/.openclaw/workspace/skills/qdrant-memory/mem-qdrant-watcher.service` | Systemd service file |
### Deployment
| Service | Action | Status |
|---------|--------|--------|
| `mem-redis-watcher` | Stopped | ✅ |
| `mem-qdrant-watcher` | Started | ✅ Active |
### Backups Created
- `/root/.openclaw/openclaw.json.bak.2026-02-24`
- `/root/.openclaw/extensions/memory-qdrant/index.ts.bak.2026-02-24`
---
## Current Status
| Component | Status |
|-----------|--------|
| **Curation (daily)** | v1 cron at 2:45 AM |
| **Injection** | ✅ Working, uses gems_tr |
| **Collection** | gems_tr |
| **KB** | Updated |
---
## What Still Needs Doing
1. ~~Test autoCapture (cleaned content to memories_tr)~~ ✅ Done
2. Test v2 curator (read from Qdrant, not Redis) — Next step
3. Full validation 2x
---
## Session Recovery
If starting new session:
1. Read this session.md
2. Read README.md for architecture
3. Read KB for issue history
---
**Next:** Test v2 curator (reads from memories_tr, creates gems in gems_tr)

View File

@@ -1,150 +0,0 @@
# NeuralStream Session State
**Date:** 2026-02-23
**Status:** Architecture v2.2 - Context-aware hybrid triggers
**Alias:** ns
---
## Architecture v2.2 (Current)
**Decision:** Three hybrid extraction triggers with full context awareness
| Trigger | When | Purpose |
|---------|------|---------|
| `turn_end` (N=5) | Every 5 turns | Normal batch extraction |
| Timer (15 min idle) | No new turn for 15 min | Catch partial batches |
| Context (50% threshold) | `ctx.getContextUsage().percent >= threshold` | Proactive pre-compaction |
**Context Awareness:**
- qwen3 gets **up to 256k tokens** of full conversation history for understanding
- Only extracts **last N turns** (oldest in batch) to avoid gemming current context
- Uses `ctx.getContextUsage()` native API for token monitoring
**Why Hybrid:**
- Batch extraction = better quality gems (more context)
- Timer safety = never lose important turns if user walks away
- Context trigger = proactive extraction before system forces compaction
- All gems survive `/new` and `/reset` via Qdrant
**Infrastructure:** Reuse existing Redis/Qdrant — NeuralStream is the "middle layer" only
---
## Core Insight
NeuralStream enables **infinite effective context** — active window stays small, but semantically relevant gems from all past conversations are queryable and injectable.
---
## Technical Decisions 2026-02-23
### Triggers (Three-way Hybrid)
| Trigger | Config | Default |
|---------|--------|---------|
| Batch size | `batch_size` | 5 turns |
| Idle timeout | `idle_timeout` | 15 minutes |
| Context threshold | `context_threshold` | 50% |
### Context Monitoring (Native API)
- `ctx.getContextUsage()` → `{tokens, contextWindow, percent}`
- Checked in `turn_end` hook
- Triggers extraction when `percent >= context_threshold`
### Extraction Context Window
- **Feed to qwen3:** Up to 256k tokens (full history for understanding)
- **Extract from:** Last `batch_size` turns only
- **Benefit:** Rich context awareness without gemming current conversation
### Storage
- **Buffer:** Redis (`neuralstream:buffer` key)
- **Gems:** Qdrant `neuralstream` collection (1024 dims, Cosine)
- **Existing infra:** Reuse mem-redis-watcher + qdrant-memory
### Gem Format (Proposed)
```json
{
"gem_id": "uuid",
"content": "Distilled insight/fact/decision",
"summary": "One-line for quick scanning",
"topics": ["docker", "redis", "architecture"],
"importance": 0.9,
"source": {
"session_id": "uuid",
"date": "2026-02-23",
"turn_range": "15-20"
},
"tags": ["decision", "fact", "preference", "todo", "code"],
"created_at": "2026-02-23T15:26:00Z"
}
```
### Extraction Model
- **qwen3** for gem extraction (256k context, cheap)
- **Dedicated prompt** (to be designed) for extracting high-value items
---
## Architecture Layers
| Layer | Status | Description |
|-------|--------|-------------|
| Capture | ✅ Existing | Every turn → Redis (mem-redis-watcher) |
| **Extract** | ⏳ NeuralStream | Batch → qwen3 → gems → Qdrant |
| Retrieve | ✅ Existing | Semantic search → inject context |
NeuralStream = Smart extraction layer on top of existing infra.
---
## Open Questions
- Gem extraction prompt design (deferred)
- Importance scoring: auto vs manual?
- Injection: `turn_start` hook or modify system prompt?
- Semantic search threshold tuning
---
## Next Steps
| Task | Status |
|------|--------|
| Architecture v2.2 finalized | ✅ |
| Native context monitoring validated | ✅ |
| Gem JSON schema | ✅ Proposed |
| Implement turn_end hook | ⏳ |
| Implement timer/cron check | ⏳ |
| Implement context trigger | ⏳ |
| Create extraction prompt | ⏳ |
| Test gem extraction with qwen3 | ⏳ |
| Implement injection mechanism | ⏳ |
---
## Decisions Log
| Date | Decision |
|------|----------|
| 2026-02-23 | Switch to turn_end hook (v2) |
| 2026-02-23 | Hybrid triggers with timer (v2.1) |
| 2026-02-23 | Context-aware extraction (v2.2) |
| 2026-02-23 | Native API: ctx.getContextUsage() |
| 2026-02-23 | Full context feed to qwen3 (256k) |
| 2026-02-23 | Reuse existing Redis/Qdrant infrastructure |
| 2026-02-23 | Batch N=5 turns |
| 2026-02-23 | Context threshold = 50% |
| 2026-02-23 | Inactivity timer = 15 min |
| 2026-02-23 | Dedicated qwen3 extraction prompt (deferred) |
---
## Backups
- Local: `/root/.openclaw/workspace/.projects/neuralstream/`
- Remote: `deb2:/root/.projects/neuralstream/` (build/test only)
- kimi_kb: Research entries stored
---
**Key Insight:** Session resets wipe context but NOT Qdrant. NeuralStream = "Context insurance policy" for infinite LLM memory.

View File

@@ -1,64 +0,0 @@
#!/usr/bin/env python3
"""Quick test of curator with simple input"""
import json
import requests
# Load prompt from v1
with open('/root/.openclaw/workspace/.projects/true-recall-v1/curator-prompt.md') as f:
prompt = f.read()
# Test with a simple conversation
test_turns = [
{
'turn': 1,
'user_id': 'rob',
'user': 'I want to switch from Redis to Qdrant for memory storage',
'ai': 'Got it - Qdrant is a good choice for vector storage.',
'conversation_id': 'test123',
'timestamp': '2026-02-23T10:00:00',
'date': '2026-02-23'
},
{
'turn': 2,
'user_id': 'rob',
'user': 'Yes, and I want the curator to read from Qdrant directly',
'ai': 'Makes sense - we can modify the curator to query Qdrant instead of Redis.',
'conversation_id': 'test123',
'timestamp': '2026-02-23T10:01:00',
'date': '2026-02-23'
}
]
conversation_json = json.dumps(test_turns, indent=2)
prompt_text = f"""## Input Conversation
```json
{conversation_json}
```
## Output
"""
response = requests.post(
'http://10.0.0.10:11434/api/generate',
json={
'model': 'qwen3:4b-instruct',
'system': prompt,
'prompt': prompt_text,
'stream': False,
'options': {'temperature': 0.1, 'num_predict': 2000}
},
timeout=120
)
result = response.json()
output = result.get('response', '').strip()
print('=== RAW OUTPUT ===')
print(output[:2000])
print()
print('=== PARSED ===')
# Try to extract JSON
if '```json' in output:
parsed = output.split('```json')[1].split('```')[0].strip()
print(parsed)

View File

@@ -1,105 +0,0 @@
#!/usr/bin/env python3
"""
TrueRecall v2 - Compaction Hook
Fast Redis queue push for compaction events
Called by OpenClaw session_before_compact hook
"""
import json
import sys
import redis
from datetime import datetime
from typing import List, Dict, Any
# Redis config
REDIS_HOST = "10.0.0.36"
REDIS_PORT = 6379
REDIS_DB = 0
QUEUE_KEY = "tr:compact_queue"
TAG_PREFIX = "tr:processed"
def get_redis_client():
return redis.Redis(
host=REDIS_HOST,
port=REDIS_PORT,
db=REDIS_DB,
decode_responses=True
)
def tag_turns(messages: List[Dict], user_id: str = "rob"):
"""Tag turns so v1 daily curator skips them"""
r = get_redis_client()
pipe = r.pipeline()
for msg in messages:
conv_id = msg.get("conversation_id", "unknown")
turn = msg.get("turn", 0)
tag_key = f"{TAG_PREFIX}:{conv_id}:{turn}"
pipe.setex(tag_key, 86400, "1") # 24h TTL
pipe.execute()
def queue_messages(messages: List[Dict], user_id: str = "rob"):
"""Push messages to Redis queue for background processing"""
r = get_redis_client()
queue_item = {
"user_id": user_id,
"timestamp": datetime.now().isoformat(),
"message_count": len(messages),
"messages": messages
}
# LPUSH to queue (newest first)
r.lpush(QUEUE_KEY, json.dumps(queue_item))
return len(messages)
def process_compaction_event(event_data: Dict):
"""
Process session_before_compact event from OpenClaw
Expected event_data:
{
"session_id": "uuid",
"user_id": "rob",
"messages_being_compacted": [
{"role": "user", "content": "...", "turn": 1, "conversation_id": "..."},
...
],
"compaction_reason": "context_limit"
}
"""
user_id = event_data.get("user_id", "rob")
messages = event_data.get("messages_being_compacted", [])
if not messages:
return {"status": "ok", "queued": 0, "reason": "no_messages"}
# Tag turns for v1 coordination
tag_turns(messages, user_id)
# Queue for background processing
count = queue_messages(messages, user_id)
return {
"status": "ok",
"queued": count,
"user_id": user_id,
"queue_key": QUEUE_KEY
}
def main():
"""CLI entry point - reads JSON from stdin"""
try:
event_data = json.load(sys.stdin)
result = process_compaction_event(event_data)
print(json.dumps(result))
sys.exit(0)
except Exception as e:
print(json.dumps({"status": "error", "error": str(e)}))
sys.exit(1)
if __name__ == "__main__":
main()

View File

@@ -1,101 +0,0 @@
# Turn-Based Curator
Extract gems every N turns instead of waiting for daily curation.
## Files
| File | Purpose |
|------|---------|
| `curator_turn_based.py` | Main script - checks turn count, extracts gems |
| `curator_cron.sh` | Cron wrapper to run every minute |
| `turn-curator.service` | Alternative systemd service (runs on-demand) |
## Usage
### Manual Run
```bash
# Check current status
python3 curator_turn_based.py --status
# Preview what would be curated
python3 curator_turn_based.py --threshold 10 --dry-run
# Execute curation
python3 curator_turn_based.py --threshold 10 --execute
```
### Automatic (Cron)
Add to crontab:
```bash
* * * * * /root/.openclaw/workspace/.projects/true-recall-v2/tr-continuous/curator_cron.sh
```
Or use systemd timer:
```bash
sudo cp turn-curator.service /etc/systemd/system/
sudo systemctl enable turn-curator.timer # If you create a timer
```
### Automatic (Integrated)
Alternative: Modify `realtime_qdrant_watcher.py` to trigger curation every 10 turns.
## How It Works
1. **Tracks turn count** - Stores last curation turn in `/tmp/curator_turn_state.json`
2. **Monitors delta** - Compares current turn count vs last curation
3. **Triggers at threshold** - When 10+ new turns exist, runs curation
4. **Extracts gems** - Sends conversation to qwen3, gets gems
5. **Stores results** - Saves gems to `gems_tr` collection
## State File
`/tmp/curator_turn_state.json`:
```json
{
"last_turn": 150,
"last_curation": "2026-02-24T17:00:00Z"
}
```
## Comparison with Daily Curator
| Feature | Daily Curator | Turn-Based Curator |
|---------|--------------|-------------------|
| Schedule | 2:45 AM daily | Every 10 turns (dynamic) |
| Time window | 24 hours | Variable (depends on chat frequency) |
| Trigger | Cron | Turn threshold |
| Use case | Nightly batch | Real-time-ish extraction |
| Overlap | Low | Possible with daily curator |
## Recommendation
Use **BOTH**:
- **Turn-based**: Every 10 turns for active conversations
- **Daily**: 2:45 AM as backup/catch-all
They'll deduplicate automatically (same embeddings → skipped).
## Testing
```bash
# Simulate 10 turns
for i in {1..10}; do
echo "Test message $i" > /dev/null
done
# Check status
python3 curator_turn_based.py --status
# Run manually
python3 curator_turn_based.py --threshold 10 --execute
```
## Status
- ✅ Script created: `curator_turn_based.py`
- ✅ Cron wrapper: `curator_cron.sh`
- ⏳ Deployment: Optional (manual or cron)
- ⏳ Testing: Pending

View File

@@ -1,194 +0,0 @@
#!/usr/bin/env python3
"""
Turn-Based Curator: Extract gems every N new memories (turns).
Usage:
python3 curator_by_count.py --threshold 10 --dry-run
python3 curator_by_count.py --threshold 10 --execute
python3 curator_by_count.py --status
"""
import argparse
import json
import requests
import sys
from datetime import datetime, timezone, timedelta
from pathlib import Path
QDRANT_URL = "http://10.0.0.40:6333"
MEMORIES = "memories_tr"
GEMS = "gems_tr"
OLLAMA = "http://10.0.0.10:11434"
MODEL = "ollama-remote/qwen3:30b-a3b-instruct-2507-q8_0"
STATE_FILE = Path("/tmp/curator_count_state.json")
def load_state():
if STATE_FILE.exists():
with open(STATE_FILE) as f:
return json.load(f)
return {"last_count": 0, "last_time": None}
def save_state(state):
with open(STATE_FILE, 'w') as f:
json.dump(state, f)
def get_total_count():
try:
r = requests.get(f"{QDRANT_URL}/collections/{MEMORIES}", timeout=10)
return r.json()["result"]["points_count"]
except:
return 0
def get_recent_memories(hours=1):
"""Get memories from last N hours."""
since = (datetime.now(timezone.utc) - timedelta(hours=hours)).isoformat()
try:
r = requests.post(
f"{QDRANT_URL}/collections/{MEMORIES}/points/scroll",
json={"limit": 1000, "with_payload": True},
timeout=30
)
points = r.json()["result"]["points"]
# Filter by timestamp
recent = [p for p in points if p.get("payload", {}).get("timestamp", "") > since]
return recent
except:
return []
def extract_gems(memories):
"""Send to LLM for gem extraction."""
if not memories:
return []
# Build conversation
parts = []
for m in memories:
role = m["payload"].get("role", "unknown")
content = m["payload"].get("content", "")[:500] # Limit per message
parts.append(f"{role.upper()}: {content}")
conversation = "\n\n".join(parts[:20]) # Max 20 messages
prompt = f"""Extract 3-5 key gems (insights, decisions, facts) from this conversation.
Conversation:
{conversation}
Return JSON: [{{"text": "gem", "category": "decision|fact|preference"}}]"""
try:
r = requests.post(
f"{OLLAMA}/v1/chat/completions",
json={
"model": MODEL,
"messages": [{"role": "user", "content": prompt}],
"temperature": 0.3
},
timeout=120
)
content = r.json()["choices"][0]["message"]["content"]
# Parse JSON
start = content.find('[')
end = content.rfind(']')
if start >= 0 and end > start:
return json.loads(content[start:end+1])
except:
pass
return []
def store_gem(gem):
"""Store gem to gems_tr."""
try:
# Get embedding
r = requests.post(
f"{OLLAMA}/api/embeddings",
json={"model": "snowflake-arctic-embed2", "prompt": gem["text"]},
timeout=30
)
vector = r.json()["embedding"]
# Store
r = requests.put(
f"{QDRANT_URL}/collections/{GEMS}/points",
json={
"points": [{
"id": abs(hash(gem["text"])) % (2**63),
"vector": vector,
"payload": {
"text": gem["text"],
"category": gem.get("category", "other"),
"createdAt": datetime.now(timezone.utc).isoformat(),
"source": "turn_curator"
}
}]
},
timeout=30
)
return r.status_code == 200
except:
return False
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--threshold", "-t", type=int, default=10)
parser.add_argument("--execute", "-e", action="store_true")
parser.add_argument("--dry-run", "-n", action="store_true")
parser.add_argument("--status", "-s", action="store_true")
args = parser.parse_args()
state = load_state()
current = get_total_count()
new_points = current - state.get("last_count", 0)
if args.status:
print(f"Total memories: {current}")
print(f"Last curated: {state.get('last_count', 0)}")
print(f"New since last: {new_points}")
print(f"Threshold: {args.threshold}")
print(f"Ready: {'YES' if new_points >= args.threshold else 'NO'}")
return
print(f"Curator: {new_points} new / {args.threshold} threshold")
if new_points < args.threshold:
print("Not enough new memories")
return
# Get recent memories (last hour should cover the new points)
memories = get_recent_memories(hours=1)
print(f"Fetched {len(memories)} recent memories")
if not memories:
print("No memories to process")
return
if args.dry_run:
print(f"[DRY RUN] Would process {len(memories)} memories")
return
if not args.execute:
print("Use --execute to run or --dry-run to preview")
return
# Extract gems
print("Extracting gems...")
gems = extract_gems(memories)
print(f"Extracted {len(gems)} gems")
# Store
success = 0
for gem in gems:
if store_gem(gem):
success += 1
print(f" Stored: {gem['text'][:60]}...")
# Update state
state["last_count"] = current
state["last_time"] = datetime.now(timezone.utc).isoformat()
save_state(state)
print(f"Done: {success}/{len(gems)} gems stored")
if __name__ == "__main__":
main()

View File

@@ -1,7 +1,7 @@
{
"timer_minutes": 5,
"max_batch_size": 100,
"user_id": "rob",
"user_id": "<USER_ID>",
"source_collection": "memories_tr",
"target_collection": "gems_tr"
}

View File

@@ -1,12 +0,0 @@
#!/bin/bash
# Turn-based curator cron - runs every minute to check if 10 turns reached
SCRIPT_DIR="/root/.openclaw/workspace/.projects/true-recall-v2/tr-continuous"
# Check if enough turns accumulated
/usr/bin/python3 "${SCRIPT_DIR}/curator_turn_based.py" --threshold 10 --status 2>/dev/null | grep -q "Ready to curate: YES"
if [ $? -eq 0 ]; then
# Run curation
/usr/bin/python3 "${SCRIPT_DIR}/curator_turn_based.py" --threshold 10 --execute 2>&1 | logger -t turn-curator
fi

View File

@@ -1,139 +1,118 @@
#!/usr/bin/env python3
"""
TrueRecall Timer Curator: Runs every 30 minutes via cron.
TrueRecall v2 - Timer Curator
Runs every 5 minutes via cron
Extracts gems from uncurated memories and stores them in gems_tr
- Queries all uncurated memories from memories_tr
- Sends batch to qwen3 for gem extraction
- Stores gems to gems_tr
- Marks processed memories as curated=true
Usage:
python3 curator_timer.py --config curator_config.json
python3 curator_timer.py --config curator_config.json --dry-run
REQUIRES: TrueRecall v1 (provides memories_tr via watcher)
"""
import os
import sys
import json
import argparse
import hashlib
import requests
from datetime import datetime, timezone
from pathlib import Path
from typing import List, Dict, Any, Optional
import hashlib
# Load config
def load_config(config_path: str) -> Dict[str, Any]:
with open(config_path, 'r') as f:
return json.load(f)
# Default paths
SCRIPT_DIR = Path(__file__).parent
DEFAULT_CONFIG = SCRIPT_DIR / "curator_config.json"
# Curator prompt path
CURATOR_PROMPT_PATH = Path("/root/.openclaw/workspace/.projects/true-recall-v2/curator-prompt.md")
# Configuration - EDIT THESE for your environment
QDRANT_URL = "http://<QDRANT_IP>:6333"
OLLAMA_URL = "http://<OLLAMA_IP>:11434"
SOURCE_COLLECTION = "memories_tr"
TARGET_COLLECTION = "gems_tr"
EMBEDDING_MODEL = "snowflake-arctic-embed2"
MAX_BATCH = 100
USER_ID = "<USER_ID>"
def load_curator_prompt() -> str:
"""Load the curator system prompt."""
def get_uncurated_memories(qdrant_url: str, collection: str, user_id: str, max_batch: int = 100) -> List[Dict[str, Any]]:
"""Fetch uncurated memories from Qdrant."""
try:
with open(CURATOR_PROMPT_PATH, 'r') as f:
return f.read()
except FileNotFoundError:
print(f"⚠️ Curator prompt not found at {CURATOR_PROMPT_PATH}")
return """You are The Curator. Extract meaningful gems from conversation history.
Extract facts, insights, decisions, preferences, and context that would be valuable to remember.
Output a JSON array of gems with fields: gem, context, snippet, categories, importance (1-5), confidence (0-0.99)."""
def get_uncurated_memories(qdrant_url: str, collection: str, user_id: str, max_batch: int) -> List[Dict[str, Any]]:
"""Query Qdrant for uncurated memories."""
filter_data = {
"must": [
{"key": "user_id", "match": {"value": user_id}},
{"key": "curated", "match": {"value": False}}
]
}
all_points = []
offset = None
iterations = 0
max_iterations = 10
while len(all_points) < max_batch and iterations < max_iterations:
iterations += 1
scroll_data = {
"limit": min(100, max_batch - len(all_points)),
"with_payload": True,
"filter": filter_data
}
if offset:
scroll_data["offset"] = offset
try:
response = requests.post(
f"{qdrant_url}/collections/{collection}/points/scroll",
json=scroll_data,
headers={"Content-Type": "application/json"},
timeout=30
)
response.raise_for_status()
result = response.json()
points = result.get("result", {}).get("points", [])
if not points:
break
all_points.extend(points)
offset = result.get("result", {}).get("next_page_offset")
if not offset:
break
except Exception as e:
print(f"Error querying Qdrant: {e}", file=sys.stderr)
break
# Convert to simple dicts
memories = []
for point in all_points:
payload = point.get("payload", {})
memories.append({
"id": point.get("id"),
"content": payload.get("content", ""),
"role": payload.get("role", ""),
"timestamp": payload.get("timestamp", ""),
"turn": payload.get("turn", 0),
**payload
})
return memories[:max_batch]
response = requests.post(
f"{qdrant_url}/collections/{collection}/points/scroll",
json={
"limit": max_batch,
"filter": {
"must": [
{"key": "user_id", "match": {"value": user_id}},
{"key": "curated", "match": {"value": False}}
]
},
"with_payload": True
},
timeout=30
)
response.raise_for_status()
data = response.json()
return data.get("result", {}).get("points", [])
except Exception as e:
print(f"Error fetching memories: {e}", file=sys.stderr)
return []
def extract_gems(memories: List[Dict[str, Any]], ollama_url: str) -> List[Dict[str, Any]]:
"""Send memories to qwen3 for gem extraction."""
"""Send memories to LLM for gem extraction."""
if not memories:
return []
prompt = load_curator_prompt()
SKIP_PATTERNS = [
"gems extracted", "curator", "curation complete",
"system is running", "validation round",
]
# Build conversation from memories
conversation_lines = []
for mem in memories:
role = mem.get("role", "unknown")
content = mem.get("content", "")
if content:
conversation_lines.append(f"{role}: {content}")
for i, mem in enumerate(memories):
payload = mem.get("payload", {})
text = payload.get("text", "") or payload.get("content", "")
role = payload.get("role", "")
if not text:
continue
text = str(text)
if role == "assistant":
continue
text_lower = text.lower()
if len(text) < 20:
continue
if any(pattern in text_lower for pattern in SKIP_PATTERNS):
continue
text = text[:500] if len(text) > 500 else text
conversation_lines.append(f"[{i+1}] {text}")
conversation_text = "\n".join(conversation_lines)
if not conversation_lines:
return []
conversation_text = "\n\n".join(conversation_lines)
prompt = """You are a memory curator. Extract atomic facts from the conversation below.
For each distinct fact/decision/preference, output a JSON object with:
- "text": the atomic fact (1-2 sentences) - use FIRST PERSON ("I" not "User")
- "category": one of [decision, preference, technical, project, knowledge, system]
- "importance": "high" or "medium"
Return ONLY a JSON array. Example:
[
{"text": "I decided to use Redis for caching", "category": "decision", "importance": "high"},
{"text": "I prefer dark mode", "category": "preference", "importance": "medium"}
]
If no extractable facts, return [].
CONVERSATION:
"""
full_prompt = f"{prompt}{conversation_text}\n\nJSON:"
try:
response = requests.post(
f"{ollama_url}/api/generate",
json={
"model": "qwen3:30b-a3b-instruct-2507-q8_0",
"model": "<CURATOR_MODEL>",
"system": prompt,
"prompt": f"## Input Conversation\n\n{conversation_text}\n\n## Output\n",
"prompt": full_prompt,
"stream": False,
"options": {
"temperature": 0.1,
@@ -148,48 +127,20 @@ def extract_gems(memories: List[Dict[str, Any]], ollama_url: str) -> List[Dict[s
return []
result = response.json()
output = result.get('response', '').strip()
# Extract JSON from output
if '```json' in output:
output = output.split('```json')[1].split('```')[0].strip()
elif '```' in output:
output = output.split('```')[1].split('```')[0].strip()
response_text = result.get("response", "")
try:
start_idx = output.find('[')
end_idx = output.rfind(']')
if start_idx != -1 and end_idx != -1 and end_idx > start_idx:
output = output[start_idx:end_idx+1]
# Fix common JSON issues from LLM output
# Replace problematic escape sequences
output = output.replace('\\n', '\n').replace('\\t', '\t')
# Fix single quotes in content that break JSON
output = output.replace("\\'", "'")
gems = json.loads(output)
start = response_text.find('[')
end = response_text.rfind(']')
if start == -1 or end == -1:
return []
json_str = response_text[start:end+1]
gems = json.loads(json_str)
if not isinstance(gems, list):
gems = [gems] if gems else []
return []
return gems
except json.JSONDecodeError as e:
# Try to extract gems with regex fallback
import re
gem_matches = re.findall(r'"gem"\s*:\s*"([^"]+)"', output)
if gem_matches:
gems = []
for gem_text in gem_matches:
gems.append({
"gem": gem_text,
"context": "Extracted via fallback",
"categories": ["extracted"],
"importance": 3,
"confidence": 0.7
})
print(f"⚠️ Fallback extraction: {len(gems)} gems", file=sys.stderr)
return gems
print(f"Error parsing curator output: {e}", file=sys.stderr)
print(f"Raw output: {repr(output[:500])}...", file=sys.stderr)
print(f"JSON parse error: {e}", file=sys.stderr)
return []
@@ -198,33 +149,34 @@ def get_embedding(text: str, ollama_url: str) -> Optional[List[float]]:
try:
response = requests.post(
f"{ollama_url}/api/embeddings",
json={"model": "mxbai-embed-large", "prompt": text},
json={
"model": EMBEDDING_MODEL,
"prompt": text
},
timeout=30
)
response.raise_for_status()
return response.json()['embedding']
data = response.json()
return data.get("embedding")
except Exception as e:
print(f"Error getting embedding: {e}", file=sys.stderr)
return None
def store_gem(gem: Dict[str, Any], user_id: str, qdrant_url: str, target_collection: str, ollama_url: str) -> bool:
"""Store a single gem to Qdrant."""
embedding_text = f"{gem.get('gem', '')} {gem.get('context', '')} {gem.get('snippet', '')}"
vector = get_embedding(embedding_text, ollama_url)
def store_gem(gem: Dict[str, Any], vector: List[float], qdrant_url: str, target_collection: str, user_id: str) -> bool:
"""Store a gem in Qdrant."""
embedding_text = gem.get("text", "") or gem.get("gem", "")
if vector is None:
return False
# Generate ID
hash_content = f"{user_id}:{gem.get('conversation_id', '')}:{gem.get('turn_range', '')}:{gem.get('gem', '')[:50]}"
hash_content = f"{user_id}:{embedding_text[:100]}"
hash_bytes = hashlib.sha256(hash_content.encode()).digest()[:8]
gem_id = int.from_bytes(hash_bytes, byteorder='big') % (2**63)
payload = {
"text": embedding_text,
"category": gem.get("category", "fact"),
"importance": gem.get("importance", "medium"),
"user_id": user_id,
**gem,
"curated_at": datetime.now(timezone.utc).isoformat()
"created_at": datetime.now(timezone.utc).isoformat()
}
try:
@@ -247,7 +199,7 @@ def store_gem(gem: Dict[str, Any], user_id: str, qdrant_url: str, target_collect
def mark_curated(memory_ids: List, qdrant_url: str, collection: str) -> bool:
"""Mark memories as curated in Qdrant using POST /points/payload format."""
"""Mark memories as curated."""
if not memory_ids:
return True
@@ -271,79 +223,58 @@ def mark_curated(memory_ids: List, qdrant_url: str, collection: str) -> bool:
def main():
parser = argparse.ArgumentParser(description="TrueRecall Timer Curator")
parser.add_argument("--config", "-c", default=str(DEFAULT_CONFIG), help="Config file path")
parser.add_argument("--dry-run", "-n", action="store_true", help="Don't write, just preview")
args = parser.parse_args()
print("TrueRecall v2 - Timer Curator")
print(f"User: {USER_ID}")
print(f"Source: {SOURCE_COLLECTION}")
print(f"Target: {TARGET_COLLECTION}")
print(f"Max batch: {MAX_BATCH}\n")
config = load_config(args.config)
qdrant_url = os.getenv("QDRANT_URL", "http://10.0.0.40:6333")
ollama_url = os.getenv("OLLAMA_URL", "http://10.0.0.10:11434")
user_id = config.get("user_id", "rob")
source_collection = config.get("source_collection", "memories_tr")
target_collection = config.get("target_collection", "gems_tr")
max_batch = config.get("max_batch_size", 100)
print(f"🔍 TrueRecall Timer Curator")
print(f"👤 User: {user_id}")
print(f"📥 Source: {source_collection}")
print(f"💎 Target: {target_collection}")
print(f"📦 Max batch: {max_batch}")
if args.dry_run:
print("🏃 DRY RUN MODE")
print()
# Get uncurated memories
print("📥 Fetching uncurated memories...")
memories = get_uncurated_memories(qdrant_url, source_collection, user_id, max_batch)
print(f"✅ Found {len(memories)} uncurated memories")
print("Fetching uncurated memories...")
memories = get_uncurated_memories(QDRANT_URL, SOURCE_COLLECTION, USER_ID, MAX_BATCH)
print(f"Found {len(memories)} uncurated memories\n")
if not memories:
print("🤷 Nothing to curate. Exiting.")
print("Nothing to curate. Exiting.")
return
# Extract gems
print(f"\n🧠 Sending {len(memories)} memories to curator...")
gems = extract_gems(memories, ollama_url)
print(f"✅ Extracted {len(gems)} gems")
print("Sending memories to curator...")
gems = extract_gems(memories, OLLAMA_URL)
print(f"Extracted {len(gems)} gems\n")
if not gems:
print("⚠️ No gems extracted. Nothing to store.")
# Still mark as curated so we don't reprocess
memory_ids = [m["id"] for m in memories] # Keep as integers
mark_curated(memory_ids, qdrant_url, source_collection)
print("No gems extracted. Exiting.")
return
# Preview
print("\n💎 Gems preview:")
print("Gems preview:")
for i, gem in enumerate(gems[:3], 1):
print(f" {i}. {gem.get('gem', 'N/A')[:80]}...")
text = gem.get("text", "N/A")[:50]
print(f" {i}. {text}...")
if len(gems) > 3:
print(f" ... and {len(gems) - 3} more")
print()
if args.dry_run:
print("\n🏃 DRY RUN: Not storing gems or marking curated.")
return
# Store gems
print(f"\n💾 Storing {len(gems)} gems...")
print("Storing gems...")
stored = 0
for gem in gems:
if store_gem(gem, user_id, qdrant_url, target_collection, ollama_url):
stored += 1
print(f"✅ Stored: {stored}/{len(gems)}")
text = gem.get("text", "") or gem.get("gem", "")
if not text:
continue
vector = get_embedding(text, OLLAMA_URL)
if vector:
if store_gem(gem, vector, QDRANT_URL, TARGET_COLLECTION, USER_ID):
stored += 1
# Mark memories as curated
print("\n📝 Marking memories as curated...")
memory_ids = [m["id"] for m in memories] # Keep as integers
if mark_curated(memory_ids, qdrant_url, source_collection):
print(f"✅ Marked {len(memory_ids)} memories as curated")
print(f"Stored: {stored}/{len(gems)}\n")
print("Marking memories as curated...")
memory_ids = [mem.get("id") for mem in memories if mem.get("id")]
if mark_curated(memory_ids, QDRANT_URL, SOURCE_COLLECTION):
print(f"Marked {len(memory_ids)} memories as curated\n")
else:
print(f"⚠️ Failed to mark some memories as curated")
print("Failed to mark memories\n")
print("\n🎉 Curation complete!")
print("Curation complete!")
if __name__ == "__main__":

View File

@@ -1,291 +0,0 @@
#!/usr/bin/env python3
"""
Turn-Based Curator: Extract gems every N turns (instead of daily).
Usage:
python3 curator_turn_based.py --threshold 10 --dry-run
python3 curator_turn_based.py --threshold 10 --execute
python3 curator_turn_based.py --status # Show turn counts
This tracks turn count since last curation and runs when threshold is reached.
"""
import argparse
import json
import os
import requests
import sys
from datetime import datetime, timezone, timedelta
from pathlib import Path
from typing import List, Dict, Any, Optional
# Config
QDRANT_URL = "http://10.0.0.40:6333"
MEMORIES_COLLECTION = "memories_tr"
GEMS_COLLECTION = "gems_tr"
OLLAMA_URL = "http://10.0.0.10:11434"
CURATOR_MODEL = "ollama-remote/qwen3:30b-a3b-instruct-2507-q8_0"
# State file tracks last curation
STATE_FILE = Path("/tmp/curator_turn_state.json")
def get_curator_prompt(conversation_text: str) -> str:
"""Generate prompt for gem extraction."""
return f"""You are a memory curator. Extract only the most valuable gems (key insights) from this conversation.
Rules:
1. Extract only genuinely important information (decisions, preferences, key facts)
2. Skip transient/trivial content (greetings, questions, temporary requests)
3. Each gem should be self-contained and useful for future context
4. Format: concise, factual statements
5. Max 3-5 gems total
Conversation to curate:
---
{conversation_text}
---
Return ONLY a JSON array of gems like:
[{{"text": "User decided to use X approach for Y", "category": "decision"}}]
Categories: preference, fact, decision, entity, other
JSON:"""
def load_state() -> Dict[str, Any]:
"""Load curation state."""
if STATE_FILE.exists():
try:
with open(STATE_FILE) as f:
return json.load(f)
except:
pass
return {"last_turn": 0, "last_curation": None}
def save_state(state: Dict[str, Any]):
"""Save curation state."""
with open(STATE_FILE, 'w') as f:
json.dump(state, f, indent=2)
def get_point_count_since(last_time: str) -> int:
"""Get count of points since last curation time."""
try:
response = requests.post(
f"{QDRANT_URL}/collections/{MEMORIES_COLLECTION}/points/count",
json={
"filter": {
"must": [
{
"key": "timestamp",
"range": {
"gt": last_time
}
}
]
}
},
timeout=30
)
response.raise_for_status()
return response.json().get("result", {}).get("count", 0)
except Exception as e:
print(f"Error getting count: {e}", file=sys.stderr)
return 0
def get_turns_since(last_turn: int, limit: int = 100) -> List[Dict[str, Any]]:
"""Get all turns since last curation."""
try:
response = requests.post(
f"{QDRANT_URL}/collections/{MEMORIES_COLLECTION}/points/scroll",
json={"limit": limit, "with_payload": True},
timeout=30
)
response.raise_for_status()
data = response.json()
turns = []
for point in data.get("result", {}).get("points", []):
turn_num = point.get("payload", {}).get("turn", 0)
if turn_num > last_turn:
turns.append(point)
# Sort by turn number
turns.sort(key=lambda x: x.get("payload", {}).get("turn", 0))
return turns
except Exception as e:
print(f"Error fetching turns: {e}", file=sys.stderr)
return []
def extract_gems_with_llm(conversation_text: str) -> List[Dict[str, str]]:
"""Send conversation to LLM for gem extraction."""
prompt = get_curator_prompt(conversation_text)
try:
response = requests.post(
f"{OLLAMA_URL}/v1/chat/completions",
json={
"model": CURATOR_MODEL,
"messages": [{"role": "user", "content": prompt}],
"temperature": 0.3,
"max_tokens": 1000
},
timeout=120
)
response.raise_for_status()
data = response.json()
content = data.get("choices", [{}])[0].get("message", {}).get("content", "[]")
# Extract JSON from response
try:
# Try to find JSON array in response
start = content.find('[')
end = content.rfind(']')
if start != -1 and end != -1:
json_str = content[start:end+1]
gems = json.loads(json_str)
if isinstance(gems, list):
return gems
except:
pass
return []
except Exception as e:
print(f"Error calling LLM: {e}", file=sys.stderr)
return []
def store_gem(gem: Dict[str, str]) -> bool:
"""Store a single gem to gems_tr."""
try:
# Get embedding for gem
response = requests.post(
f"{OLLAMA_URL}/api/embeddings",
json={"model": "snowflake-arctic-embed2", "prompt": gem["text"]},
timeout=30
)
response.raise_for_status()
vector = response.json().get("embedding", [])
if not vector:
return False
# Store to gems_tr
response = requests.put(
f"{QDRANT_URL}/collections/{GEMS_COLLECTION}/points",
json={
"points": [{
"id": hash(gem["text"]) % (2**63),
"vector": vector,
"payload": {
"text": gem["text"],
"category": gem.get("category", "other"),
"createdAt": datetime.now(timezone.utc).isoformat(),
"source": "turn_based_curator"
}
}]
},
timeout=30
)
response.raise_for_status()
return True
except Exception as e:
print(f"Error storing gem: {e}", file=sys.stderr)
return False
def main():
parser = argparse.ArgumentParser(description="Turn-based curator")
parser.add_argument("--threshold", "-t", type=int, default=10,
help="Run curation every N turns (default: 10)")
parser.add_argument("--execute", "-e", action="store_true",
help="Execute curation")
parser.add_argument("--dry-run", "-n", action="store_true",
help="Preview what would be curated")
parser.add_argument("--status", "-s", action="store_true",
help="Show current turn status")
args = parser.parse_args()
# Load state
state = load_state()
current_turn = get_current_turn_count()
turns_since = current_turn - state["last_turn"]
if args.status:
print(f"Current turn: {current_turn}")
print(f"Last curation: {state['last_turn']}")
print(f"Turns since last curation: {turns_since}")
print(f"Threshold: {args.threshold}")
print(f"Ready to curate: {'YES' if turns_since >= args.threshold else 'NO'}")
return
print(f"Turn-based Curator")
print(f"Current turn: {current_turn}")
print(f"Last curation: {state['last_turn']}")
print(f"Turns since: {turns_since}")
print(f"Threshold: {args.threshold}")
print()
if turns_since < args.threshold:
print(f"Not enough turns. Need {args.threshold}, have {turns_since}")
return
# Get turns to process
print(f"Fetching {turns_since} turns...")
turns = get_turns_since(state["last_turn"], limit=turns_since + 10)
if not turns:
print("No new turns found")
return
# Build conversation text
conversation_parts = []
for turn in turns:
role = turn.get("payload", {}).get("role", "unknown")
content = turn.get("payload", {}).get("content", "")
conversation_parts.append(f"{role.upper()}: {content}")
conversation_text = "\n\n".join(conversation_parts)
print(f"Processing {len(turns)} turns ({len(conversation_text)} chars)")
print()
if args.dry_run:
print("=== CONVERSATION TEXT ===")
print(conversation_text[:500] + "..." if len(conversation_text) > 500 else conversation_text)
print()
print("[DRY RUN] Would extract gems and store to gems_tr")
return
if not args.execute:
print("Use --execute to run curation or --dry-run to preview")
return
# Extract gems
print("Extracting gems with LLM...")
gems = extract_gems_with_llm(conversation_text)
if not gems:
print("No gems extracted")
return
print(f"Extracted {len(gems)} gems:")
for i, gem in enumerate(gems, 1):
print(f" {i}. [{gem.get('category', 'other')}] {gem['text'][:80]}...")
print()
# Store gems
print("Storing gems...")
success = 0
for gem in gems:
if store_gem(gem):
success += 1
# Update state
state["last_turn"] = current_turn
state["last_curation"] = datetime.now(timezone.utc).isoformat()
save_state(state)
print(f"Done! Stored {success}/{len(gems)} gems")
if __name__ == "__main__":
main()

View File

@@ -1,85 +0,0 @@
#!/usr/bin/env python3
"""
Migration: Add 'curated: false' to existing memories_tr entries.
Run once to update all existing memories for the new timer curator.
Uses POST /collections/{name}/points/payload with {"points": [ids], "payload": {...}}
"""
import requests
import time
import sys
QDRANT_URL = "http://10.0.0.40:6333"
COLLECTION = "memories_tr"
def update_existing_memories():
"""Add curated=false to all memories that don't have the field."""
print("🔧 Migrating existing memories...")
offset = None
updated = 0
batch_size = 100
max_iterations = 200
iterations = 0
while iterations < max_iterations:
iterations += 1
scroll_data = {
"limit": batch_size,
"with_payload": True
}
if offset:
scroll_data["offset"] = offset
try:
response = requests.post(
f"{QDRANT_URL}/collections/{COLLECTION}/points/scroll",
json=scroll_data,
headers={"Content-Type": "application/json"},
timeout=30
)
response.raise_for_status()
result = response.json()
points = result.get("result", {}).get("points", [])
if not points:
break
# Collect IDs that need curated=false
ids_to_update = []
for point in points:
payload = point.get("payload", {})
if "curated" not in payload:
ids_to_update.append(point["id"])
if ids_to_update:
# POST /points/payload with {"points": [ids], "payload": {...}}
update_response = requests.post(
f"{QDRANT_URL}/collections/{COLLECTION}/points/payload",
json={
"points": ids_to_update,
"payload": {"curated": False}
},
timeout=30
)
update_response.raise_for_status()
updated += len(ids_to_update)
print(f" Updated batch: {len(ids_to_update)} memories (total: {updated})")
time.sleep(0.05)
offset = result.get("result", {}).get("next_page_offset")
if not offset:
break
except Exception as e:
print(f"Error: {e}", file=sys.stderr)
import traceback
traceback.print_exc()
break
print(f"✅ Migration complete: {updated} memories updated with curated=false")
if __name__ == "__main__":
update_existing_memories()

View File

@@ -1,14 +0,0 @@
[Unit]
Description=TrueRecall Turn-Based Curator (every 10 turns)
After=network.target mem-qdrant-watcher.service
[Service]
Type=simple
User=root
WorkingDirectory=/root/.openclaw/workspace/.projects/true-recall-v2/tr-continuous
ExecStart=/usr/bin/python3 /root/.openclaw/workspace/.projects/true-recall-v2/tr-continuous/curator_turn_based.py --threshold 10 --execute
Restart=on-failure
RestartSec=60
[Install]
WantedBy=multi-user.target

View File

@@ -1,358 +0,0 @@
#!/usr/bin/env python3
"""
True-Recall v2 Curator: Reads from Qdrant kimi_memories
Reads 24 hours of conversation from Qdrant kimi_memories collection,
extracts contextual gems using qwen3, stores to Qdrant gems_tr with mxbai embeddings.
Usage:
python curate_from_qdrant.py --user-id rob
python curate_from_qdrant.py --user-id rob --date 2026-02-23
"""
import json
import argparse
import requests
import urllib.request
from datetime import datetime, timedelta
from pathlib import Path
from typing import List, Dict, Any, Optional
import hashlib
# Configuration
QDRANT_URL = "http://10.0.0.40:6333"
SOURCE_COLLECTION = "memories_tr"
TARGET_COLLECTION = "gems_tr"
OLLAMA_URL = "http://10.0.0.10:11434"
EMBEDDING_MODEL = "mxbai-embed-large"
CURATION_MODEL = "qwen3:4b-instruct"
# Load curator prompt
CURATOR_PROMPT_PATH = "/root/.openclaw/workspace/.projects/true-recall/curator-prompt.md"
def load_curator_prompt() -> str:
"""Load the curator system prompt."""
try:
with open(CURATOR_PROMPT_PATH, 'r') as f:
return f.read()
except FileNotFoundError:
# Fallback to v2 location
CURATOR_PROMPT_PATH_V2 = "/root/.openclaw/workspace/.projects/true-recall-v2/curator-prompt.md"
with open(CURATOR_PROMPT_PATH_V2, 'r') as f:
return f.read()
def get_turns_from_qdrant(user_id: str, date_str: str) -> List[Dict[str, Any]]:
"""
Get all conversation turns from Qdrant for a specific user and date.
Returns turns sorted by conversation_id and turn_number.
"""
# Build filter for user_id and date
filter_data = {
"must": [
{"key": "user_id", "match": {"value": user_id}},
{"key": "date", "match": {"value": date_str}}
]
}
# Use scroll API to get all matching points
all_points = []
offset = None
max_iterations = 100 # Safety limit
iterations = 0
while iterations < max_iterations:
iterations += 1
scroll_data = {
"limit": 100,
"with_payload": True,
"filter": filter_data
}
if offset:
scroll_data["offset"] = offset
req = urllib.request.Request(
f"{QDRANT_URL}/collections/{SOURCE_COLLECTION}/points/scroll",
data=json.dumps(scroll_data).encode(),
headers={"Content-Type": "application/json"},
method="POST"
)
try:
with urllib.request.urlopen(req, timeout=30) as response:
result = json.loads(response.read().decode())
points = result.get("result", {}).get("points", [])
if not points:
break
all_points.extend(points)
# Check if there's more
offset = result.get("result", {}).get("next_page_offset")
if not offset:
break
except urllib.error.HTTPError as e:
if e.code == 404:
print(f"⚠️ Collection {SOURCE_COLLECTION} not found")
return []
raise
# Convert points to turn format (harvested summaries)
turns = []
for point in all_points:
payload = point.get("payload", {})
# Extract user and AI messages
user_msg = payload.get("user_message", "")
ai_msg = payload.get("ai_response", "")
# Get timestamp from created_at
created_at = payload.get("created_at", "")
turn = {
"turn": payload.get("turn_number", 0),
"user_id": payload.get("user_id", user_id),
"user": user_msg,
"ai": ai_msg,
"conversation_id": payload.get("conversation_id", ""),
"session_id": payload.get("session_id", ""),
"timestamp": created_at,
"date": payload.get("date", date_str),
"content_hash": payload.get("content_hash", "")
}
# Skip if no content
if turn["user"] or turn["ai"]:
turns.append(turn)
# Sort by conversation_id, then by turn number
turns.sort(key=lambda x: (x.get("conversation_id", ""), x.get("turn", 0)))
return turns
def extract_gems_with_curator(turns: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
"""Use qwen3 to extract gems from conversation turns."""
if not turns:
return []
prompt = load_curator_prompt()
# Build the conversation input
conversation_json = json.dumps(turns, indent=2)
# Call Ollama with native system prompt
response = requests.post(
f"{OLLAMA_URL}/api/generate",
json={
"model": CURATION_MODEL,
"system": prompt,
"prompt": f"## Input Conversation\n\n```json\n{conversation_json}\n```\n\n## Output\n",
"stream": False,
"options": {
"temperature": 0.1,
"num_predict": 4000
}
}
)
if response.status_code != 200:
raise RuntimeError(f"Curation failed: {response.text}")
result = response.json()
output = result.get('response', '').strip()
# Extract JSON from output (handle markdown code blocks)
if '```json' in output:
output = output.split('```json')[1].split('```')[0].strip()
elif '```' in output:
output = output.split('```')[1].split('```')[0].strip()
try:
# Extract JSON array - find first [ and last ]
start_idx = output.find('[')
end_idx = output.rfind(']')
if start_idx != -1 and end_idx != -1 and end_idx > start_idx:
output = output[start_idx:end_idx+1]
gems = json.loads(output)
if not isinstance(gems, list):
print(f"Warning: Curator returned non-list, wrapping: {type(gems)}")
gems = [gems] if gems else []
return gems
except json.JSONDecodeError as e:
print(f"Error parsing curator output: {e}")
print(f"Raw output: {output[:500]}...")
return []
def get_embedding(text: str) -> List[float]:
"""Get embedding vector from Ollama using mxbai-embed-large."""
response = requests.post(
f"{OLLAMA_URL}/api/embeddings",
json={
"model": EMBEDDING_MODEL,
"prompt": text
}
)
if response.status_code != 200:
raise RuntimeError(f"Embedding failed: {response.text}")
return response.json()['embedding']
def get_gem_id(gem: Dict[str, Any], user_id: str) -> int:
"""Generate deterministic integer ID for a gem."""
hash_bytes = hashlib.sha256(
f"{user_id}:{gem.get('conversation_id', '')}:{gem.get('turn_range', '')}".encode()
).digest()[:8]
return int.from_bytes(hash_bytes, byteorder='big') % (2**63)
def check_duplicate(gem: Dict[str, Any], user_id: str) -> bool:
"""Check if a similar gem already exists in gems_tr."""
gem_id = get_gem_id(gem, user_id)
# Check if point exists
try:
req = urllib.request.Request(
f"{QDRANT_URL}/collections/{TARGET_COLLECTION}/points/{gem_id}",
headers={"Content-Type": "application/json"},
method="GET"
)
with urllib.request.urlopen(req, timeout=10) as response:
return True # Point exists
except urllib.error.HTTPError as e:
if e.code == 404:
return False # Point doesn't exist
raise
def store_gem_to_qdrant(gem: Dict[str, Any], user_id: str) -> bool:
"""Store a gem to Qdrant with embedding."""
# Create embedding from gem text
embedding_text = f"{gem.get('gem', '')} {gem.get('context', '')} {gem.get('snippet', '')}"
vector = get_embedding(embedding_text)
# Prepare payload
payload = {
"user_id": user_id,
**gem
}
# Generate deterministic integer ID
gem_id = get_gem_id(gem, user_id)
# Store to Qdrant
response = requests.put(
f"{QDRANT_URL}/collections/{TARGET_COLLECTION}/points",
json={
"points": [{
"id": gem_id,
"vector": vector,
"payload": payload
}]
}
)
return response.status_code == 200
def main():
parser = argparse.ArgumentParser(description="True-Recall Curator v2 - Reads from Qdrant")
parser.add_argument("--user-id", required=True, help="User ID to process")
parser.add_argument("--date", help="Specific date to process (YYYY-MM-DD), defaults to yesterday")
parser.add_argument("--dry-run", action="store_true", help="Don't store, just preview")
args = parser.parse_args()
# Determine date (yesterday by default)
if args.date:
date_str = args.date
else:
yesterday = datetime.now() - timedelta(days=1)
date_str = yesterday.strftime("%Y-%m-%d")
print(f"🔍 True-Recall Curator v2 for {args.user_id}")
print(f"📅 Processing date: {date_str}")
print(f"🧠 Embedding model: {EMBEDDING_MODEL}")
print(f"💎 Target collection: {TARGET_COLLECTION}")
print()
# Get turns from Qdrant
print(f"📥 Fetching conversation turns from {SOURCE_COLLECTION}...")
turns = get_turns_from_qdrant(args.user_id, date_str)
print(f"✅ Found {len(turns)} turns")
if not turns:
print("⚠️ No turns to process. Exiting.")
return
# Show sample
print("\n📄 Sample turns:")
for i, turn in enumerate(turns[:3], 1):
user_msg = turn.get("user", "")[:60]
ai_msg = turn.get("ai", "")[:60]
print(f" Turn {turn.get('turn')}: User: {user_msg}...")
print(f" AI: {ai_msg}...")
if len(turns) > 3:
print(f" ... and {len(turns) - 3} more")
# Extract gems
print("\n🧠 Extracting gems with The Curator (qwen3)...")
gems = extract_gems_with_curator(turns)
print(f"✅ Extracted {len(gems)} gems")
if not gems:
print("⚠️ No gems extracted. Exiting.")
return
# Preview gems
print("\n💎 Preview of extracted gems:")
for i, gem in enumerate(gems[:3], 1):
print(f"\n--- Gem {i} ---")
print(f"Gem: {gem.get('gem', 'N/A')[:100]}...")
print(f"Categories: {gem.get('categories', [])}")
print(f"Importance: {gem.get('importance', 'N/A')}")
print(f"Confidence: {gem.get('confidence', 'N/A')}")
if len(gems) > 3:
print(f"\n... and {len(gems) - 3} more gems")
if args.dry_run:
print("\n🏃 DRY RUN: Not storing gems.")
return
# Check for duplicates and store
print("\n💾 Storing gems to Qdrant...")
stored = 0
skipped = 0
failed = 0
for gem in gems:
# Check for duplicates
if check_duplicate(gem, args.user_id):
print(f" ⏭️ Skipping duplicate: {gem.get('gem', 'N/A')[:50]}...")
skipped += 1
continue
if store_gem_to_qdrant(gem, args.user_id):
stored += 1
else:
print(f" ⚠️ Failed to store gem: {gem.get('gem', 'N/A')[:50]}...")
failed += 1
print(f"\n✅ Stored: {stored}")
print(f"⏭️ Skipped (duplicates): {skipped}")
print(f"❌ Failed: {failed}")
print("\n🎉 Curation complete!")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,20 @@
[Unit]
Description=OpenClaw Real-Time Qdrant Memory Watcher
After=network.target
[Service]
Type=simple
User=<USER>
WorkingDirectory=<INSTALL_PATH>/tr-worker
Environment="QDRANT_URL=http://<QDRANT_IP>:6333"
Environment="QDRANT_COLLECTION=memories_tr"
Environment="OLLAMA_URL=http://<OLLAMA_IP>:11434"
Environment="EMBEDDING_MODEL=snowflake-arctic-embed2"
Environment="USER_ID=<USER_ID>"
Environment="PYTHONUNBUFFERED=1"
ExecStart=/usr/bin/python3 <INSTALL_PATH>/tr-worker/realtime_qdrant_watcher.py --daemon
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target

View File

@@ -0,0 +1,332 @@
#!/usr/bin/env python3
"""
Real-time Qdrant Watcher: Monitors OpenClaw session JSONL and stores to Qdrant instantly.
This daemon watches the active session file, embeds each conversation turn,
and stores directly to Qdrant memories_tr collection (real-time, no Redis).
Usage:
# Run as daemon
python3 realtime_qdrant_watcher.py --daemon
# Run once (process current session then exit)
python3 realtime_qdrant_watcher.py --once
# Test mode (print to stdout, don't write to Qdrant)
python3 realtime_qdrant_watcher.py --dry-run
Systemd service:
# Copy to /etc/systemd/system/mem-qdrant-watcher.service
# systemctl enable --now mem-qdrant-watcher
"""
import os
import sys
import json
import time
import signal
import hashlib
import argparse
import requests
from datetime import datetime, timezone
from pathlib import Path
from typing import Dict, Any, Optional, List
# Config - Set via environment variables or use placeholders
QDRANT_URL = os.getenv("QDRANT_URL", "http://<QDRANT_IP>:6333")
QDRANT_COLLECTION = os.getenv("QDRANT_COLLECTION", "memories_tr")
OLLAMA_URL = os.getenv("OLLAMA_URL", "http://<OLLAMA_IP>:11434")
EMBEDDING_MODEL = os.getenv("EMBEDDING_MODEL", "snowflake-arctic-embed2")
USER_ID = os.getenv("USER_ID", "<USER_ID>")
# Paths
SESSIONS_DIR = Path(os.getenv("SESSIONS_DIR", "~/.openclaw/agents/main/sessions")).expanduser()
# State
running = True
last_position = 0
current_file = None
turn_counter = 0
def signal_handler(signum, frame):
"""Handle shutdown gracefully."""
global running
print(f"\nReceived signal {signum}, shutting down...", file=sys.stderr)
running = False
def get_embedding(text: str) -> List[float]:
"""Get embedding vector from Ollama."""
try:
response = requests.post(
f"{OLLAMA_URL}/api/embeddings",
json={"model": EMBEDDING_MODEL, "prompt": text},
timeout=30
)
response.raise_for_status()
return response.json()["embedding"]
except Exception as e:
print(f"Error getting embedding: {e}", file=sys.stderr)
return None
def clean_content(text: str) -> str:
"""Clean content - remove metadata, markdown, keep only plain text."""
import re
# Remove metadata JSON blocks
text = re.sub(r'Conversation info \(untrusted metadata\):\s*```json\s*\{[\s\S]*?\}\s*```', '', text)
# Remove thinking tags
text = re.sub(r'\[thinking:[^\]]*\]', '', text)
# Remove timestamp lines
text = re.sub(r'\[\w{3} \d{4}-\d{2}-\d{2} \d{2}:\d{2} [A-Z]{3}\]', '', text)
# Remove markdown tables
text = re.sub(r'\|[^\n]*\|', '', text) # Table rows
text = re.sub(r'\|[-:]+\|', '', text) # Table separators
# Remove markdown formatting
text = re.sub(r'\*\*([^*]+)\*\*', r'\1', text) # Bold **text**
text = re.sub(r'\*([^*]+)\*', r'\1', text) # Italic *text*
text = re.sub(r'`([^`]+)`', r'\1', text) # Inline code `text`
text = re.sub(r'```[\s\S]*?```', '', text) # Code blocks
# Remove horizontal rules
text = re.sub(r'---+', '', text)
text = re.sub(r'\*\*\*+', '', text)
# Remove excess whitespace
text = re.sub(r'\n{3,}', '\n', text)
text = re.sub(r'[ \t]+', ' ', text) # Multiple spaces -> single
return text.strip()
def store_to_qdrant(turn: Dict[str, Any], dry_run: bool = False) -> bool:
"""Store a single turn to Qdrant with embedding."""
if dry_run:
print(f"[DRY RUN] Would store turn {turn['turn']} ({turn['role']}): {turn['content'][:60]}...")
return True
# Get embedding
vector = get_embedding(turn['content'])
if vector is None:
print(f"Failed to get embedding for turn {turn['turn']}", file=sys.stderr)
return False
# Prepare payload
payload = {
"user_id": turn.get('user_id', USER_ID),
"role": turn['role'],
"content": turn['content'],
"turn": turn['turn'],
"timestamp": turn.get('timestamp', datetime.now(timezone.utc).isoformat()),
"date": datetime.now(timezone.utc).strftime('%Y-%m-%d'),
"source": "realtime_watcher",
"curated": False
}
# Generate deterministic ID
turn_id = turn.get('turn', 0)
hash_bytes = hashlib.sha256(f"{USER_ID}:turn:{turn_id}:{datetime.now().strftime('%H%M%S')}".encode()).digest()[:8]
point_id = int.from_bytes(hash_bytes, byteorder='big') % (2**63)
# Store to Qdrant
try:
response = requests.put(
f"{QDRANT_URL}/collections/{QDRANT_COLLECTION}/points",
json={
"points": [{
"id": abs(point_id),
"vector": vector,
"payload": payload
}]
},
timeout=30
)
response.raise_for_status()
return True
except Exception as e:
print(f"Error writing to Qdrant: {e}", file=sys.stderr)
return False
def get_current_session_file():
"""Find the most recently modified session JSONL file."""
if not SESSIONS_DIR.exists():
return None
files = list(SESSIONS_DIR.glob("*.jsonl"))
if not files:
return None
return max(files, key=lambda p: p.stat().st_mtime)
def parse_turn(line: str, session_name: str) -> Optional[Dict[str, Any]]:
"""Parse a single JSONL line into a turn dict."""
global turn_counter
try:
entry = json.loads(line.strip())
except json.JSONDecodeError:
return None
# OpenClaw format: {"type": "message", "message": {...}}
if entry.get('type') != 'message' or 'message' not in entry:
return None
msg = entry['message']
role = msg.get('role')
# Skip tool results, system, developer messages
if role in ('toolResult', 'system', 'developer'):
return None
if role not in ('user', 'assistant'):
return None
# Extract content
content = ""
if isinstance(msg.get('content'), list):
for item in msg['content']:
if isinstance(item, dict) and 'text' in item:
content += item['text']
elif isinstance(msg.get('content'), str):
content = msg['content']
if not content:
return None
# Clean content
content = clean_content(content)
if not content or len(content) < 5:
return None
turn_counter += 1
return {
'turn': turn_counter,
'role': role,
'content': content[:2000], # Limit size
'timestamp': entry.get('timestamp', datetime.now(timezone.utc).isoformat()),
'user_id': USER_ID
}
def process_new_lines(f, session_name: str, dry_run: bool = False):
"""Process any new lines added to the file."""
global last_position
f.seek(last_position)
for line in f:
line = line.strip()
if not line:
continue
turn = parse_turn(line, session_name)
if turn:
if store_to_qdrant(turn, dry_run):
print(f"✅ Turn {turn['turn']} ({turn['role']}) → Qdrant")
last_position = f.tell()
def watch_session(session_file: Path, dry_run: bool = False):
"""Watch a specific session file for new lines."""
global last_position, turn_counter
session_name = session_file.name.replace('.jsonl', '')
print(f"Watching session: {session_file.name}")
try:
with open(session_file, 'r') as f:
for line in f:
turn_counter += 1
last_position = session_file.stat().st_size
print(f"Session has {turn_counter} existing turns, starting from position {last_position}")
except Exception as e:
print(f"Warning: Could not read existing turns: {e}", file=sys.stderr)
last_position = 0
with open(session_file, 'r') as f:
while running:
if not session_file.exists():
print("Session file removed, looking for new session...")
return None
process_new_lines(f, session_name, dry_run)
time.sleep(0.1)
return session_file
def watch_loop(dry_run: bool = False):
"""Main watch loop - handles session rotation."""
global current_file, turn_counter
while running:
session_file = get_current_session_file()
if session_file is None:
print("No active session found, waiting...")
time.sleep(1)
continue
if current_file != session_file:
print(f"\nNew session detected: {session_file.name}")
current_file = session_file
turn_counter = 0
last_position = 0
result = watch_session(session_file, dry_run)
if result is None:
current_file = None
time.sleep(0.5)
def main():
global USER_ID
parser = argparse.ArgumentParser(
description="Real-time OpenClaw session watcher → Qdrant"
)
parser.add_argument("--daemon", "-d", action="store_true", help="Run as daemon")
parser.add_argument("--once", "-o", action="store_true", help="Process once then exit")
parser.add_argument("--dry-run", "-n", action="store_true", help="Don't write to Qdrant")
parser.add_argument("--user-id", "-u", default=USER_ID, help=f"User ID (default: {USER_ID})")
args = parser.parse_args()
signal.signal(signal.SIGINT, signal_handler)
signal.signal(signal.SIGTERM, signal_handler)
if args.user_id:
USER_ID = args.user_id
print(f"🔍 Real-time Qdrant Watcher")
print(f"📍 Qdrant: {QDRANT_URL}/{QDRANT_COLLECTION}")
print(f"🧠 Ollama: {OLLAMA_URL}/{EMBEDDING_MODEL}")
print(f"👤 User: {USER_ID}")
print(f"📝 Sessions: {SESSIONS_DIR}")
print()
if args.once:
print("Running once...")
session_file = get_current_session_file()
if session_file:
watch_session(session_file, args.dry_run)
else:
print("No session found")
else:
print("Running as daemon (Ctrl+C to stop)...")
watch_loop(args.dry_run)
if __name__ == "__main__":
main()