@icex-labs/openclaw-memory-engine 3.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +254 -0
- package/extras/auto-consolidation-crons.json +37 -0
- package/extras/memory-maintenance.sh +176 -0
- package/index.js +626 -0
- package/lib/archival.js +54 -0
- package/lib/backup.js +99 -0
- package/lib/consolidate.js +102 -0
- package/lib/core.js +76 -0
- package/lib/dashboard.js +235 -0
- package/lib/dedup.js +68 -0
- package/lib/embedding.js +70 -0
- package/lib/episodes.js +133 -0
- package/lib/graph.js +148 -0
- package/lib/paths.js +80 -0
- package/lib/reflection.js +188 -0
- package/lib/search.js +90 -0
- package/lib/store-sqlite.js +422 -0
- package/openclaw.plugin.json +23 -0
- package/package.json +40 -0
- package/setup.sh +368 -0
package/README.md
ADDED
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
# @icex-labs/openclaw-memory-engine
|
|
2
|
+
|
|
3
|
+
> Give your AI agent a brain that survives restarts.
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/@icex-labs/openclaw-memory-engine)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
|
|
8
|
+
A [MemGPT](https://github.com/cpacker/MemGPT)-inspired memory plugin for [OpenClaw](https://openclaw.ai). Your agent gets 12 tools to manage its own memory — what to remember, what to recall, what to forget.
|
|
9
|
+
|
|
10
|
+
**The problem:** OpenClaw agents wake up fresh every session. Without persistent memory, they forget who you are.
|
|
11
|
+
|
|
12
|
+
**The fix:** Two-tier memory architecture:
|
|
13
|
+
- **Core Memory** (~500 tokens) — identity, relationship, preferences. Always loaded.
|
|
14
|
+
- **Archival Memory** (unlimited) — facts, decisions, events. Retrieved on demand via hybrid semantic search.
|
|
15
|
+
|
|
16
|
+
The agent manages both tiers autonomously using purpose-built tools.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Install
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
git clone git@github.com:icex-labs/openclaw-memory-engine.git ~/.openclaw/extensions/memory-engine
|
|
24
|
+
bash ~/.openclaw/extensions/memory-engine/setup.sh
|
|
25
|
+
nano ~/.openclaw/workspace/memory/core.json # fill in your info
|
|
26
|
+
openclaw gateway restart
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Or from npm:
|
|
30
|
+
```bash
|
|
31
|
+
npm install -g @icex-labs/openclaw-memory-engine
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
`setup.sh` handles everything: enables the plugin in `openclaw.json`, creates template files, installs the daily maintenance cron, and patches your agent's instructions.
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## How It Works
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
┌──────────────────────────────────────────────────────┐
|
|
42
|
+
│ Agent Context Window │
|
|
43
|
+
│ │
|
|
44
|
+
│ Session start → core_memory_read() │
|
|
45
|
+
│ └─→ core.json (~500 tokens) │
|
|
46
|
+
│ │
|
|
47
|
+
│ "Where does Alice's doctor work?" │
|
|
48
|
+
│ → archival_search("doctor") │
|
|
49
|
+
│ └─→ keyword match + embedding similarity │
|
|
50
|
+
│ + recency boost + access frequency │
|
|
51
|
+
│ → "Dr. Smith, City Medical..." │
|
|
52
|
+
│ │
|
|
53
|
+
│ Alice says something new │
|
|
54
|
+
│ → archival_insert(fact, entity, tags) │
|
|
55
|
+
│ └─→ archival.jsonl + background embedding │
|
|
56
|
+
│ │
|
|
57
|
+
│ End of conversation │
|
|
58
|
+
│ → memory_consolidate(summary) │
|
|
59
|
+
│ └─→ split sentences → infer entities → dedup │
|
|
60
|
+
│ → batch insert │
|
|
61
|
+
└──────────────────────────────────────────────────────┘
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## Tools (12)
|
|
67
|
+
|
|
68
|
+
### Core Memory — Your Identity
|
|
69
|
+
|
|
70
|
+
| Tool | What it does |
|
|
71
|
+
|------|-------------|
|
|
72
|
+
| `core_memory_read` | Load the identity block. Call every session start. |
|
|
73
|
+
| `core_memory_replace` | Update a field by dot-path (`user.location`, `current_focus`). Auto-parses JSON strings. 3KB hard limit. |
|
|
74
|
+
| `core_memory_append` | Append to an array field (`current_focus`). Creates array if needed. |
|
|
75
|
+
|
|
76
|
+
Core memory lives in `memory/core.json`:
|
|
77
|
+
|
|
78
|
+
```json
|
|
79
|
+
{
|
|
80
|
+
"user": { "name": "Alice", "location": "New York", "language": "bilingual" },
|
|
81
|
+
"relationship": { "dynamic": "intimate companion", "trust": "deep" },
|
|
82
|
+
"preferences": { "config_rule": "don't touch openclaw.json" },
|
|
83
|
+
"current_focus": ["quant trading", "immigration case"]
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Archival Memory — Your Long-Term Storage
|
|
88
|
+
|
|
89
|
+
| Tool | What it does |
|
|
90
|
+
|------|-------------|
|
|
91
|
+
| `archival_insert` | Store a fact. Tags it with `entity` + `tags`. Computes embedding in background. |
|
|
92
|
+
| `archival_search` | Hybrid search: keyword (2×) + semantic similarity (5×) + recency (0-1) + access decay (0-0.5). |
|
|
93
|
+
| `archival_update` | Correct an existing record by ID. Re-indexes embedding. |
|
|
94
|
+
| `archival_delete` | Remove an outdated record. Cleans up embedding cache. |
|
|
95
|
+
| `archival_stats` | Dashboard: record count, embedding coverage, entity/tag distribution, storage size. |
|
|
96
|
+
|
|
97
|
+
Each record in `memory/archival.jsonl`:
|
|
98
|
+
|
|
99
|
+
```json
|
|
100
|
+
{"id":"arch-17120-abc","ts":"2026-04-01","content":"Alice's doctor is Dr. Smith","entity":"Alice","tags":["health"],"access_count":3}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Maintenance — Keep It Clean
|
|
104
|
+
|
|
105
|
+
| Tool | What it does |
|
|
106
|
+
|------|-------------|
|
|
107
|
+
| `archival_deduplicate` | Find near-duplicates via embedding cosine similarity (≥0.92). Preview or auto-remove. |
|
|
108
|
+
| `memory_consolidate` | Extract facts from text blocks. Splits by sentence (中文/English), infers entity, deduplicates, batch inserts. |
|
|
109
|
+
|
|
110
|
+
### Backup — Never Lose Your Memory
|
|
111
|
+
|
|
112
|
+
| Tool | What it does |
|
|
113
|
+
|------|-------------|
|
|
114
|
+
| `memory_export` | Snapshot core + archival + embeddings → single JSON file. |
|
|
115
|
+
| `memory_import` | Restore from snapshot. `merge` (add missing) or `replace` (overwrite all). |
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
## Search Quality
|
|
120
|
+
|
|
121
|
+
`archival_search` uses four signals:
|
|
122
|
+
|
|
123
|
+
| Signal | Weight | How |
|
|
124
|
+
|--------|--------|-----|
|
|
125
|
+
| Keyword | 2× per term | Term presence in content + entity + tags |
|
|
126
|
+
| Semantic | 5× | Cosine similarity via OpenAI `text-embedding-3-small` (512d) |
|
|
127
|
+
| Recency | 0–1 | Linear decay over 1 year |
|
|
128
|
+
| Access | 0–0.5 | Boost for recently accessed records |
|
|
129
|
+
|
|
130
|
+
Embeddings are computed on insert and cached in `archival.embeddings.json`. If no OpenAI key is available, search falls back to keyword-only — no errors, just lower quality.
|
|
131
|
+
|
|
132
|
+
**Cost:** ~$0.02 per 1M tokens with `text-embedding-3-small`. A typical session with 10 inserts + 5 searches costs < $0.001.
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
## Configuration
|
|
137
|
+
|
|
138
|
+
```json
|
|
139
|
+
// openclaw.json
|
|
140
|
+
{
|
|
141
|
+
"plugins": {
|
|
142
|
+
"allow": ["memory-engine"],
|
|
143
|
+
"entries": {
|
|
144
|
+
"memory-engine": {
|
|
145
|
+
"enabled": true,
|
|
146
|
+
"config": {
|
|
147
|
+
"workspace": "/path/to/workspace",
|
|
148
|
+
"coreSizeLimit": 3072
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
| Option | Default | Description |
|
|
157
|
+
|--------|---------|-------------|
|
|
158
|
+
| `workspace` | Auto-resolved | Workspace directory path |
|
|
159
|
+
| `coreSizeLimit` | `3072` (3KB) | Max bytes for core.json |
|
|
160
|
+
|
|
161
|
+
**Requires:** `OPENAI_API_KEY` in environment for semantic search. Without it, keyword search still works.
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
## Agent Instructions
|
|
166
|
+
|
|
167
|
+
Add to your `AGENTS.md` or system prompt (done automatically by `setup.sh`):
|
|
168
|
+
|
|
169
|
+
```markdown
|
|
170
|
+
## Every Session
|
|
171
|
+
1. Call `core_memory_read` — load your identity
|
|
172
|
+
2. When you learn something important → `archival_insert`
|
|
173
|
+
3. When you need details → `archival_search` before guessing
|
|
174
|
+
4. When facts change → `core_memory_replace`
|
|
175
|
+
5. End of conversation → `memory_consolidate` with key points
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
---
|
|
179
|
+
|
|
180
|
+
## Daily Maintenance
|
|
181
|
+
|
|
182
|
+
`extras/memory-maintenance.sh` runs daily at 3am (installed as a LaunchAgent by `setup.sh`):
|
|
183
|
+
|
|
184
|
+
- Checks core.json size (warns >4KB, critical >5KB)
|
|
185
|
+
- Merges 7-day-old daily logs into weekly summaries
|
|
186
|
+
- Archives 60-day-old weekly summaries
|
|
187
|
+
- Alerts written to `memory/maintenance-alerts.json`
|
|
188
|
+
|
|
189
|
+
---
|
|
190
|
+
|
|
191
|
+
## Backup & Migration
|
|
192
|
+
|
|
193
|
+
```bash
|
|
194
|
+
# Export
|
|
195
|
+
openclaw agent -m "memory_export"
|
|
196
|
+
# → memory/export-2026-04-01.json
|
|
197
|
+
|
|
198
|
+
# Import on new machine
|
|
199
|
+
openclaw agent -m "memory_import input_path='path/to/export.json' mode='replace'"
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
---
|
|
203
|
+
|
|
204
|
+
## Project Structure
|
|
205
|
+
|
|
206
|
+
```
|
|
207
|
+
memory-engine/
|
|
208
|
+
├── index.js # Plugin entry — tool registration only (250 lines)
|
|
209
|
+
├── lib/
|
|
210
|
+
│ ├── paths.js # Constants + path resolution
|
|
211
|
+
│ ├── core.js # Core memory CRUD + dot-path + auto-parse
|
|
212
|
+
│ ├── archival.js # Archival JSONL CRUD + in-memory cache
|
|
213
|
+
│ ├── embedding.js # OpenAI embedding API + file cache
|
|
214
|
+
│ ├── search.js # Hybrid four-signal search
|
|
215
|
+
│ ├── consolidate.js # Text → structured facts extraction
|
|
216
|
+
│ ├── dedup.js # Embedding similarity dedup
|
|
217
|
+
│ └── backup.js # Export/import
|
|
218
|
+
├── extras/
|
|
219
|
+
│ └── memory-maintenance.sh
|
|
220
|
+
├── setup.sh # One-command install
|
|
221
|
+
├── .claude/CLAUDE.md # Dev guide for Claude Code
|
|
222
|
+
├── package.json
|
|
223
|
+
├── openclaw.plugin.json
|
|
224
|
+
└── README.md
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
---
|
|
228
|
+
|
|
229
|
+
## Roadmap
|
|
230
|
+
|
|
231
|
+
- [x] Core memory with size guard and auto-parse
|
|
232
|
+
- [x] Archival CRUD with in-memory index
|
|
233
|
+
- [x] Hybrid search (keyword + embedding + recency + access decay)
|
|
234
|
+
- [x] Auto-extract facts from text
|
|
235
|
+
- [x] Embedding-based deduplication
|
|
236
|
+
- [x] Full backup/restore
|
|
237
|
+
- [x] Modular codebase (8 focused modules)
|
|
238
|
+
- [ ] LanceDB / SQLite backend for 50K+ records
|
|
239
|
+
- [ ] Cross-agent memory sharing
|
|
240
|
+
- [ ] Scheduled auto-consolidation via OpenClaw cron
|
|
241
|
+
- [ ] Memory importance scoring (agent rates memories 1-10)
|
|
242
|
+
- [ ] Forgetting curve — auto-archive unaccessed memories after N days
|
|
243
|
+
- [ ] ClawHub publishing
|
|
244
|
+
- [ ] Web dashboard for memory browsing
|
|
245
|
+
|
|
246
|
+
---
|
|
247
|
+
|
|
248
|
+
## License
|
|
249
|
+
|
|
250
|
+
MIT
|
|
251
|
+
|
|
252
|
+
---
|
|
253
|
+
|
|
254
|
+
Built for [OpenClaw](https://openclaw.ai). Inspired by [MemGPT/Letta](https://github.com/cpacker/MemGPT).
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_comment": "OpenClaw cron jobs for memory auto-consolidation. Install via: openclaw cron create --from-file <this-file>",
|
|
3
|
+
"crons": [
|
|
4
|
+
{
|
|
5
|
+
"id": "memory-reflect-daily",
|
|
6
|
+
"schedule": "0 9 * * *",
|
|
7
|
+
"agent": "main",
|
|
8
|
+
"message": "Run memory_reflect with window_days=7. Review the report. If you notice any important patterns or observations about George's behavior/interests, store them via archival_insert with entity='reflection' tags=['reflection','pattern']. Do NOT output anything to the main chat — this is a background task.",
|
|
9
|
+
"model": "anthropic/claude-sonnet-4-6",
|
|
10
|
+
"description": "Daily reflection: analyze memory patterns and store observations"
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"id": "memory-consolidate-6h",
|
|
14
|
+
"schedule": "0 */6 * * *",
|
|
15
|
+
"agent": "main",
|
|
16
|
+
"message": "Read today's memory/YYYY-MM-DD.md daily log file. If it has content that isn't already in archival memory, run memory_consolidate on it. Then run archival_stats to check health. Do NOT output anything to the main chat.",
|
|
17
|
+
"model": "anthropic/claude-sonnet-4-6",
|
|
18
|
+
"description": "Auto-consolidate: extract facts from daily logs every 6 hours"
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"id": "memory-dedup-weekly",
|
|
22
|
+
"schedule": "0 4 * * 0",
|
|
23
|
+
"agent": "main",
|
|
24
|
+
"message": "Run archival_deduplicate with apply=true to clean up duplicate facts. Then run archival_stats. Do NOT output anything to the main chat.",
|
|
25
|
+
"model": "anthropic/claude-sonnet-4-6",
|
|
26
|
+
"description": "Weekly dedup: clean up near-duplicate archival records"
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"id": "memory-dashboard-daily",
|
|
30
|
+
"schedule": "30 9 * * *",
|
|
31
|
+
"agent": "main",
|
|
32
|
+
"message": "Run memory_dashboard to regenerate the HTML dashboard. Do NOT output anything to the main chat.",
|
|
33
|
+
"model": "anthropic/claude-sonnet-4-6",
|
|
34
|
+
"description": "Daily dashboard refresh at 9:30am"
|
|
35
|
+
}
|
|
36
|
+
]
|
|
37
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# memory-maintenance.sh — Automated memory system maintenance
|
|
3
|
+
# Replaces the old memory-cleanup.sh with a comprehensive maintenance system
|
|
4
|
+
#
|
|
5
|
+
# Runs daily at 3am via LaunchAgent
|
|
6
|
+
# Actions:
|
|
7
|
+
# 1. Health check: MEMORY.md size limits
|
|
8
|
+
# 2. Daily logs: merge week-old dailies into weekly summaries
|
|
9
|
+
# 3. Archive: move weekly summaries older than 60 days
|
|
10
|
+
# 4. Topic files: warn if any topic file exceeds size limit
|
|
11
|
+
# 5. Report: append to maintenance log
|
|
12
|
+
|
|
13
|
+
set -euo pipefail
|
|
14
|
+
|
|
15
|
+
WORKSPACE="${OPENCLAW_WORKSPACE:-$HOME/.openclaw/workspace}"
|
|
16
|
+
MEMORY_DIR="$WORKSPACE/memory"
|
|
17
|
+
TOPICS_DIR="$MEMORY_DIR/topics"
|
|
18
|
+
WEEKLY_DIR="$MEMORY_DIR/weekly"
|
|
19
|
+
ARCHIVE_DIR="$MEMORY_DIR/archive"
|
|
20
|
+
LOG_FILE="$MEMORY_DIR/maintenance.log"
|
|
21
|
+
ALERT_FILE="$MEMORY_DIR/maintenance-alerts.json"
|
|
22
|
+
|
|
23
|
+
MEMORY_MD="$WORKSPACE/MEMORY.md"
|
|
24
|
+
DATE=$(date +%Y-%m-%d)
|
|
25
|
+
TIMESTAMP=$(date +%Y-%m-%dT%H:%M:%S)
|
|
26
|
+
|
|
27
|
+
# Limits (bytes)
|
|
28
|
+
MEMORY_MD_MAX=5120 # 5KB — hard limit for MEMORY.md
|
|
29
|
+
MEMORY_MD_WARN=4096 # 4KB — warning threshold
|
|
30
|
+
TOPIC_FILE_MAX=8192 # 8KB per topic file
|
|
31
|
+
DAILY_RETAIN_DAYS=7 # Keep daily logs for 7 days
|
|
32
|
+
WEEKLY_ARCHIVE_DAYS=60 # Archive weekly summaries after 60 days
|
|
33
|
+
|
|
34
|
+
mkdir -p "$TOPICS_DIR" "$WEEKLY_DIR" "$ARCHIVE_DIR"
|
|
35
|
+
|
|
36
|
+
# --- Helpers ---
|
|
37
|
+
log() { echo "[$TIMESTAMP] $1" >> "$LOG_FILE"; }
|
|
38
|
+
alert_json=""
|
|
39
|
+
add_alert() {
|
|
40
|
+
local level="$1" msg="$2"
|
|
41
|
+
if [ -z "$alert_json" ]; then
|
|
42
|
+
alert_json="[{\"level\":\"$level\",\"msg\":\"$msg\",\"ts\":\"$TIMESTAMP\"}"
|
|
43
|
+
else
|
|
44
|
+
alert_json="$alert_json,{\"level\":\"$level\",\"msg\":\"$msg\",\"ts\":\"$TIMESTAMP\"}"
|
|
45
|
+
fi
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
# --- 1. MEMORY.md health check ---
|
|
49
|
+
if [ -f "$MEMORY_MD" ]; then
|
|
50
|
+
mem_size=$(wc -c < "$MEMORY_MD" | tr -d ' ')
|
|
51
|
+
mem_lines=$(wc -l < "$MEMORY_MD" | tr -d ' ')
|
|
52
|
+
if [ "$mem_size" -gt "$MEMORY_MD_MAX" ]; then
|
|
53
|
+
add_alert "critical" "MEMORY.md is ${mem_size}B (>${MEMORY_MD_MAX}B limit). Needs trimming."
|
|
54
|
+
log "CRITICAL: MEMORY.md ${mem_size}B exceeds ${MEMORY_MD_MAX}B limit (${mem_lines} lines)"
|
|
55
|
+
elif [ "$mem_size" -gt "$MEMORY_MD_WARN" ]; then
|
|
56
|
+
add_alert "warn" "MEMORY.md is ${mem_size}B (>${MEMORY_MD_WARN}B warning). Consider trimming."
|
|
57
|
+
log "WARN: MEMORY.md ${mem_size}B approaching limit (${mem_lines} lines)"
|
|
58
|
+
else
|
|
59
|
+
log "OK: MEMORY.md ${mem_size}B / ${mem_lines} lines"
|
|
60
|
+
fi
|
|
61
|
+
else
|
|
62
|
+
add_alert "critical" "MEMORY.md not found!"
|
|
63
|
+
log "CRITICAL: MEMORY.md missing"
|
|
64
|
+
fi
|
|
65
|
+
|
|
66
|
+
# --- 2. Topic file health check ---
|
|
67
|
+
if [ -d "$TOPICS_DIR" ]; then
|
|
68
|
+
for f in "$TOPICS_DIR"/*.md; do
|
|
69
|
+
[ -f "$f" ] || continue
|
|
70
|
+
fsize=$(wc -c < "$f" | tr -d ' ')
|
|
71
|
+
fname=$(basename "$f")
|
|
72
|
+
if [ "$fsize" -gt "$TOPIC_FILE_MAX" ]; then
|
|
73
|
+
add_alert "warn" "Topic ${fname} is ${fsize}B (>${TOPIC_FILE_MAX}B). Needs pruning."
|
|
74
|
+
log "WARN: Topic ${fname} ${fsize}B exceeds limit"
|
|
75
|
+
fi
|
|
76
|
+
done
|
|
77
|
+
fi
|
|
78
|
+
|
|
79
|
+
# --- 3. Merge old daily logs into weekly summaries ---
|
|
80
|
+
# Find daily logs (YYYY-MM-DD.md) older than DAILY_RETAIN_DAYS
|
|
81
|
+
daily_count=0
|
|
82
|
+
merged_count=0
|
|
83
|
+
for f in "$MEMORY_DIR"/2???-??-??.md; do
|
|
84
|
+
[ -f "$f" ] || continue
|
|
85
|
+
fname=$(basename "$f" .md)
|
|
86
|
+
# Parse date from filename
|
|
87
|
+
file_date="$fname"
|
|
88
|
+
|
|
89
|
+
# Calculate age in days using date arithmetic
|
|
90
|
+
file_epoch=$(date -j -f "%Y-%m-%d" "$file_date" "+%s" 2>/dev/null || echo 0)
|
|
91
|
+
now_epoch=$(date "+%s")
|
|
92
|
+
if [ "$file_epoch" -eq 0 ]; then
|
|
93
|
+
continue
|
|
94
|
+
fi
|
|
95
|
+
age_days=$(( (now_epoch - file_epoch) / 86400 ))
|
|
96
|
+
|
|
97
|
+
if [ "$age_days" -gt "$DAILY_RETAIN_DAYS" ]; then
|
|
98
|
+
# Determine ISO week: YYYY-Www
|
|
99
|
+
week_label=$(date -j -f "%Y-%m-%d" "$file_date" "+%G-W%V" 2>/dev/null || continue)
|
|
100
|
+
weekly_file="$WEEKLY_DIR/${week_label}.md"
|
|
101
|
+
|
|
102
|
+
# Append daily content to weekly file with date header
|
|
103
|
+
if [ ! -f "$weekly_file" ]; then
|
|
104
|
+
echo "# Weekly Summary: ${week_label}" > "$weekly_file"
|
|
105
|
+
echo "" >> "$weekly_file"
|
|
106
|
+
fi
|
|
107
|
+
echo "## ${file_date}" >> "$weekly_file"
|
|
108
|
+
echo "" >> "$weekly_file"
|
|
109
|
+
# Extract just the section headers and key bullet points (compress)
|
|
110
|
+
grep -E '^##|^- |^\*' "$f" >> "$weekly_file" 2>/dev/null || true
|
|
111
|
+
echo "" >> "$weekly_file"
|
|
112
|
+
|
|
113
|
+
# Move original to archive
|
|
114
|
+
mv "$f" "$ARCHIVE_DIR/"
|
|
115
|
+
merged_count=$((merged_count + 1))
|
|
116
|
+
fi
|
|
117
|
+
daily_count=$((daily_count + 1))
|
|
118
|
+
done
|
|
119
|
+
log "Daily logs: ${daily_count} active, ${merged_count} merged into weekly"
|
|
120
|
+
|
|
121
|
+
# --- 4. Archive old weekly summaries ---
|
|
122
|
+
archive_count=0
|
|
123
|
+
for f in "$WEEKLY_DIR"/*.md; do
|
|
124
|
+
[ -f "$f" ] || continue
|
|
125
|
+
fmod=$(stat -f %m "$f" 2>/dev/null || echo 0)
|
|
126
|
+
now_epoch=$(date "+%s")
|
|
127
|
+
age_days=$(( (now_epoch - fmod) / 86400 ))
|
|
128
|
+
if [ "$age_days" -gt "$WEEKLY_ARCHIVE_DAYS" ]; then
|
|
129
|
+
mv "$f" "$ARCHIVE_DIR/"
|
|
130
|
+
archive_count=$((archive_count + 1))
|
|
131
|
+
fi
|
|
132
|
+
done
|
|
133
|
+
if [ "$archive_count" -gt 0 ]; then
|
|
134
|
+
log "Archived ${archive_count} weekly summaries (>${WEEKLY_ARCHIVE_DAYS} days old)"
|
|
135
|
+
fi
|
|
136
|
+
|
|
137
|
+
# --- 5. Count total memory footprint ---
|
|
138
|
+
total_active=$(du -sh "$MEMORY_DIR" 2>/dev/null | cut -f1)
|
|
139
|
+
total_archive=$(du -sh "$ARCHIVE_DIR" 2>/dev/null | cut -f1)
|
|
140
|
+
topic_count=$(ls "$TOPICS_DIR"/*.md 2>/dev/null | wc -l | tr -d ' ')
|
|
141
|
+
daily_active=$(ls "$MEMORY_DIR"/2???-??-??.md 2>/dev/null | wc -l | tr -d ' ')
|
|
142
|
+
weekly_active=$(ls "$WEEKLY_DIR"/*.md 2>/dev/null | wc -l | tr -d ' ')
|
|
143
|
+
log "Totals: active=${total_active} archive=${total_archive} topics=${topic_count} dailies=${daily_active} weeklies=${weekly_active}"
|
|
144
|
+
|
|
145
|
+
# --- 6. Handle special-name daily logs (e.g., 2026-03-28-maren-stuck.md) ---
|
|
146
|
+
for f in "$MEMORY_DIR"/2???-??-??-*.md; do
|
|
147
|
+
[ -f "$f" ] || continue
|
|
148
|
+
fname=$(basename "$f" .md)
|
|
149
|
+
file_date=$(echo "$fname" | grep -oE '^[0-9]{4}-[0-9]{2}-[0-9]{2}')
|
|
150
|
+
[ -z "$file_date" ] && continue
|
|
151
|
+
|
|
152
|
+
file_epoch=$(date -j -f "%Y-%m-%d" "$file_date" "+%s" 2>/dev/null || echo 0)
|
|
153
|
+
now_epoch=$(date "+%s")
|
|
154
|
+
[ "$file_epoch" -eq 0 ] && continue
|
|
155
|
+
age_days=$(( (now_epoch - file_epoch) / 86400 ))
|
|
156
|
+
|
|
157
|
+
if [ "$age_days" -gt "$DAILY_RETAIN_DAYS" ]; then
|
|
158
|
+
mv "$f" "$ARCHIVE_DIR/"
|
|
159
|
+
log "Archived special log: $(basename "$f") (${age_days} days old)"
|
|
160
|
+
fi
|
|
161
|
+
done
|
|
162
|
+
|
|
163
|
+
# --- 7. Write alerts file ---
|
|
164
|
+
if [ -n "$alert_json" ]; then
|
|
165
|
+
echo "${alert_json}]" > "$ALERT_FILE"
|
|
166
|
+
log "Alerts written: $(echo "${alert_json}]" | grep -o '"level"' | wc -l | tr -d ' ') items"
|
|
167
|
+
else
|
|
168
|
+
echo "[]" > "$ALERT_FILE"
|
|
169
|
+
fi
|
|
170
|
+
|
|
171
|
+
# --- 8. Trim maintenance log (keep last 90 entries) ---
|
|
172
|
+
if [ -f "$LOG_FILE" ]; then
|
|
173
|
+
tail -270 "$LOG_FILE" > "${LOG_FILE}.tmp" && mv "${LOG_FILE}.tmp" "$LOG_FILE"
|
|
174
|
+
fi
|
|
175
|
+
|
|
176
|
+
log "Maintenance complete"
|