claude-memory-hub 0.8.0 → 0.8.2

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/CHANGELOG.md CHANGED
@@ -5,6 +5,59 @@ Format follows [Keep a Changelog](https://keepachangelog.com/).
5
5
 
6
6
  ---
7
7
 
8
+ ## [0.8.2] - 2026-04-02
9
+
10
+ Increased context injection limits for richer cross-session memory.
11
+
12
+ ### Context Injection Limits
13
+
14
+ - **UserPromptSubmit cap doubled** — `MAX_CHARS` increased from 4,500 (~1,125 tokens) to 8,000 (~2,000 tokens). Session-start context injection now carries significantly more past knowledge
15
+ - **Proactive retrieval cap doubled** — `MAX_INJECTION_CHARS` increased from 1,500 (~375 tokens) to 3,000 (~750 tokens). Mid-session topic-shift injections now include fuller context from L3
16
+
17
+ ---
18
+
19
+ ## [0.8.1] - 2026-04-02
20
+
21
+ Token-budget-aware MCP tools + proactive mid-session memory retrieval.
22
+
23
+ ### Token Budget Management
24
+
25
+ - **`max_tokens` parameter** — added to `memory_recall`, `memory_search`, `memory_fetch` MCP tools. When set, output is truncated to fit within the specified token budget (~4 chars/token). Helps Claude manage context window when many tools compete for space
26
+ - **`truncateToTokenBudget()` utility** — shared truncation function with `[...truncated to fit ~N token budget]` suffix
27
+
28
+ ### Proactive Memory Retrieval
29
+
30
+ - **Topic-shift detection** — PostToolUse hook now monitors file activity and detects when conversation drifts to a new domain (e.g., auth → payment → migration). Detection uses directory clustering + keyword matching across recent files
31
+ - **Mid-session context injection** — when topic shift detected, hook searches L3 for relevant past context and returns `additionalContext` via stdout JSON. Claude Code injects this into the conversation automatically
32
+ - **Trigger conditions:** every 15 tool calls OR on Bash errors after warmup (5+ calls)
33
+ - **State tracking** — per-session state at `~/.claude-memory-hub/proactive/<session_id>.json`, cleaned up on session end
34
+ - **Injection cap:** ~375 tokens (1500 chars) per injection, deduplicated by topic
35
+
36
+ ### Session End Improvements
37
+
38
+ - **Batch queue flush on session end** — `tryFlush()` called during Stop hook to prevent data loss from unflushed batch events
39
+ - **Proactive state cleanup** — per-session state files removed on session end
40
+
41
+ ### Research Findings (documented, no code changes needed)
42
+
43
+ Based on deep Claude Code source analysis:
44
+ - **Resource filtering:** Claude Code already defers MCP tools automatically via `isDeferredTool()`. Skill listings have budget system (`SKILL_BUDGET_CONTEXT_PERCENT=1%`). No external filtering needed
45
+ - **Multi-agent sharing:** Subagents inherit parent MCP servers via `initializeAgentMcpServers()`. Memory sharing via `memory_recall` works out-of-box — zero implementation needed
46
+ - **Permission-aware:** PostToolUse hook only fires for approved tools. Denied tools fire separate `PermissionDenied` hook. memory-hub is already permission-aware by design
47
+ - **IDE context:** Available as attachments in conversation (ide_selection, ide_opened_file) but not in hook inputs directly. Entity extraction captures file activity indirectly
48
+
49
+ ### Modified Files
50
+
51
+ ```
52
+ src/mcp/tool-definitions.ts — max_tokens param on 3 tools
53
+ src/mcp/tool-handlers.ts — truncateToTokenBudget() utility
54
+ src/retrieval/proactive-retrieval.ts — NEW: topic detection + injection
55
+ src/hooks-entry/post-tool-use.ts — proactive retrieval integration
56
+ src/hooks-entry/session-end.ts — batch flush + proactive cleanup
57
+ ```
58
+
59
+ ---
60
+
8
61
  ## [0.8.0] - 2026-04-02
9
62
 
10
63
  Major release: test infrastructure, architectural fixes, hook performance, data portability.
package/README.md CHANGED
@@ -17,9 +17,34 @@ Zero API key. Zero Python. Zero config. One install command.
17
17
 
18
18
  ---
19
19
 
20
+ ## Why memory-hub?
21
+
22
+ **Claude Code forgets everything.** Every session starts from zero. Auto-compact destroys 90% of your context. You lose files, decisions, errors — hours of work, gone.
23
+
24
+ **claude-memory-hub fixes this.** One install command. No API key. No Python. No Docker.
25
+
26
+ What makes it different? **The Compact Interceptor** — something no other memory tool has. When Claude Code auto-compacts at 200K tokens, memory-hub *tells the compact engine what matters*. PreCompact hook injects priority instructions. PostCompact hook saves the full summary. Result: 90% context salvage instead of vaporization.
27
+
28
+ But it doesn't stop there:
29
+ - **Cross-session memory** — past work auto-injected when you start a new session
30
+ - **3-engine hybrid search** — FTS5 + TF-IDF + semantic embeddings (384-dim, offline)
31
+ - **Proactive retrieval** — detects topic shifts mid-session, injects relevant context automatically
32
+ - **91 unit tests**, batch queue (75ms→3ms), JSONL export/import, browser UI
33
+ - **Multi-agent ready** — subagents share memory for free via MCP
34
+
35
+ Built for developers who use Claude Code daily and are tired of repeating themselves.
36
+
37
+ ```bash
38
+ bunx claude-memory-hub install
39
+ ```
40
+
41
+ That's it. Your Claude now remembers.
42
+
43
+ ---
44
+
20
45
  ## The Problem
21
46
 
22
- Claude Code forgets everything between sessions. Within long sessions, auto-compact destroys 90% of context. Every session wastes tokens loading resources that aren't needed. Search is keyword-only with no ranking.
47
+ Claude Code forgets everything between sessions. Within long sessions, auto-compact destroys 90% of context. Search is keyword-only with no ranking.
23
48
 
24
49
  ```
25
50
  Session 1: You spend 2 hours building auth system
@@ -29,37 +54,31 @@ Long session: Claude auto-compacts at 200K tokens
29
54
  → 180K tokens of context vaporized
30
55
  → Claude loses track of files, decisions, errors
31
56
 
32
- Every session: ALL skills + agents + rules loaded
33
- → 23-51K tokens consumed before you type anything
34
- → No external tool can prevent this (Claude Code limitation)
35
- → But you CAN identify and remove unused resources
36
-
37
57
  Search: Keyword-only, no semantic ranking
38
58
  → Irrelevant results, wasted tokens on full records
39
59
  ```
40
60
 
41
- **Four problems. memory-hub solves three directly and provides analysis for the fourth.**
42
-
43
61
  | Problem | Claude Code built-in | claude-mem | memory-hub |
44
62
  |---------|:-------------------:|:----------:|:----------:|
45
63
  | Cross-session memory | -- | Yes | **Yes** |
46
64
  | Influence what compact preserves | -- | -- | **Yes** |
47
- | Save compact output | -- | -- | **Yes** |
48
- | Token overhead analysis | -- | -- | **Yes** |
49
- | Semantic search (embeddings) | -- | Chroma (external) | **Yes (offline)** |
65
+ | Save compact output to L3 | -- | -- | **Yes** |
50
66
  | Hybrid search (FTS5 + TF-IDF + semantic) | -- | Partial | **Yes** |
51
67
  | 3-layer progressive search | -- | Yes | **Yes** |
52
68
  | Resource overhead analysis | -- | -- | **Yes** |
53
69
  | CLAUDE.md rule tracking | -- | -- | **Yes** |
54
- | Free-form observation capture | -- | Yes | **Yes** |
70
+ | Observation capture (14 patterns) | -- | Yes | **Yes** |
55
71
  | LLM summarization (3-tier) | -- | Yes (API) | **Yes (free)** |
72
+ | Token-budget-aware tools (`max_tokens`) | -- | -- | **Yes** |
73
+ | Proactive mid-session retrieval | -- | -- | **Yes** |
74
+ | Multi-agent memory sharing | -- | -- | **Yes (free)** |
75
+ | Permission-aware (approved only) | -- | -- | **Yes** |
76
+ | Data export/import (JSONL) | -- | -- | **Yes** |
77
+ | Hook batching (3ms vs 75ms) | -- | -- | **Yes** |
56
78
  | Browser UI | -- | Yes | **Yes** |
57
- | Health monitoring | -- | -- | **Yes** |
58
- | Migrate from claude-mem | N/A | N/A | **Yes** |
59
- | No API key needed | N/A | Yes | **Yes** |
60
- | No Python/Chroma needed | N/A | -- | **Yes** |
61
- | No XML format required | N/A | -- | **Yes** |
62
- | No HTTP server to manage | N/A | -- | **Yes** |
79
+ | Health monitoring + auto-cleanup | -- | -- | **Yes** |
80
+ | Unit tests (91 tests) | N/A | -- | **Yes** |
81
+ | No API key / Python / Chroma | N/A | Partial | **Yes** |
63
82
 
64
83
  ---
65
84
 
@@ -75,6 +94,8 @@ Claude makes a decision → memory-hub records: decision text + importance score
75
94
  ```
76
95
 
77
96
  No XML. No special format. Extracted directly from hook JSON metadata.
97
+ PostToolUse events are batched via write-through queue (~3ms per event vs ~75ms direct).
98
+ Mid-session topic shifts auto-inject relevant past context (proactive retrieval).
78
99
 
79
100
  ### Layer 2 — Compact Interceptor (the key innovation)
80
101
 
@@ -99,41 +120,21 @@ No XML. No special format. Extracted directly from hook JSON metadata.
99
120
  zero information loss
100
121
  ```
101
122
 
102
- **This is something no other memory tool does.** claude-mem never sees the compact. Built-in session memory is supplementary, not directive. memory-hub is the only system that **tells the compact what matters**.
123
+ **No other memory tool does this.** memory-hub is the only system that **tells the compact what matters**.
103
124
 
104
125
  ### Layer 3 — Cross-Session Memory
105
126
 
106
127
  ```
107
- Session N ends → rule-based summary from entities SQLite L3
108
- OR PostCompact summary (richer) SQLite L3
128
+ Session N ends → 3-tier summarization: PostCompact > CLI claude > rule-based
129
+ Summary saved to SQLite L3 with FTS5 indexing
109
130
 
110
131
  Session N+1 → UserPromptSubmit hook fires
111
- → FTS5 + TF-IDF hybrid search: match user prompt
112
- inject relevant context automatically
132
+ → FTS5 + TF-IDF + semantic search: match user prompt
133
+ Inject relevant past context automatically
113
134
  → Claude starts with history, not from zero
114
135
  ```
115
136
 
116
- ### Layer 4 — Resource Intelligence & Overhead Analysis
117
-
118
- ```
119
- ResourceRegistry scans your setup:
120
- 58 skills, 36 agents, 65 commands, 10 workflows, CLAUDE.md chain
121
-
122
- ResourceTracker records actual usage per session:
123
- "skill:mobile-development used 4/5 recent sessions"
124
- "agent:veo3-prompt-expert used 0/5 recent sessions"
125
-
126
- OverheadReport identifies waste:
127
- "42/58 skills never used → ~1500 listing tokens overhead"
128
- "CLAUDE.md chain is 8200 tokens → consider consolidating"
129
-
130
- UserPromptSubmit injects priority hints:
131
- "Frequently-used: skill:debugging, agent:planner, agent:tester"
132
- ```
133
-
134
- > **Transparency note:** Claude Code loads ALL resources into its system prompt — no external tool can prevent this. memory-hub provides **analysis and prioritization**, not filtering. To actually reduce token overhead, remove or relocate unused skills/agents based on the overhead report.
135
-
136
- ### Layer 5 — 3-Layer Progressive Search + Semantic (new in v0.5/v0.6)
137
+ ### Layer 4 — 3-Layer Progressive Search
137
138
 
138
139
  ```
139
140
  Traditional search: query → ALL full records → 5000+ tokens wasted
@@ -146,34 +147,33 @@ memory-hub search: query → Layer 1 (index) → ~50 tokens/result
146
147
  Token savings: ~80-90% vs. full context
147
148
  ```
148
149
 
149
- Hybrid ranking: FTS5 BM25 (keyword) + TF-IDF (term frequency) + **semantic cosine similarity** (384-dim embeddings, v0.6). "debugging tips" now matches "error fixing" even without shared keywords.
150
+ Hybrid ranking: FTS5 BM25 (keyword) + TF-IDF (term frequency) + semantic cosine similarity (384-dim embeddings). "debugging tips" matches "error fixing" even without shared keywords.
150
151
 
151
- ### Layer 6 — Resource Intelligence (new in v0.6)
152
+ ### Layer 5 — Resource Intelligence
152
153
 
153
154
  ```
154
155
  ResourceRegistry scans ALL .claude locations:
155
- ~/.claude/skills/ 58 skills listing + full + total tokens
156
- ~/.claude/agents/ 36 agents frontmatter name: resolution
157
- ~/.claude/agent_mobile/ ios-developer → agent_mobile/ios/AGENT.md
158
- ~/.claude/commands/ 65 commands → relative path naming
159
- ~/.claude/workflows/ 10 workflows
160
- ~/.claude/CLAUDE.md + project CLAUDE.md chain
156
+ skills, agents, commands, workflows, CLAUDE.md chain
157
+ 3-level token estimation: listing, full, total
161
158
 
162
- OverheadReport:
163
- "56/64 skills unused in last 10 sessions → ~1033 listing tokens wasted"
164
- "CLAUDE.md chain is 3222 tokens"
159
+ ResourceTracker records actual usage per session
160
+ OverheadReport identifies unused resources + token waste
165
161
  ```
166
162
 
167
- ### Layer 7Observation Capture (new in v0.6)
163
+ > **Transparency note:** Claude Code loads ALL resources into its system prompt no external tool can prevent this. memory-hub provides **analysis and prioritization**, not filtering. To reduce token overhead, remove or relocate unused skills/agents based on the overhead report.
164
+
165
+ ### Layer 6 — Observation Capture
168
166
 
169
167
  ```
170
168
  Tool output contains "IMPORTANT: always pool DB connections"
171
169
  → observation entity (importance=4) saved to L2
172
- → included in session summary
173
- → searchable across sessions
174
170
 
175
171
  User prompt contains "remember that we use TypeScript strict"
176
172
  → observation entity (importance=3) saved to L2
173
+
174
+ 14 heuristic patterns: IMPORTANT, CRITICAL, SECURITY, DEPRECATED,
175
+ decision:, discovered, root cause, switched to, TODO:, FIXME:,
176
+ HACK:, performance:, bottleneck, OOM, don't, never, prefer, etc.
177
177
  ```
178
178
 
179
179
  ---
@@ -181,58 +181,47 @@ User prompt contains "remember that we use TypeScript strict"
181
181
  ## Architecture
182
182
 
183
183
  ```
184
- ┌──────────────────────────────────────────────────────────────┐
185
- │ Claude Code
186
-
187
- │ 5 Lifecycle Hooks
184
+ ┌─────────────────────────────────────────────────────────────┐
185
+ │ Claude Code
186
+
187
+ │ 5 Lifecycle Hooks
188
188
  │ ┌───────────────┐ ┌──────────────┐ ┌──────────────┐ │
189
189
  │ │ PostToolUse │ │ PreCompact │ │ PostCompact │ │
190
- │ │ entity capture│ │ inject │ │ save summary │ │
190
+ │ │ batch queue │ │ inject │ │ save summary │ │
191
191
  │ └──────┬────────┘ │ priorities │ └──────┬───────┘ │
192
192
  │ │ └──────┬───────┘ │ │
193
193
  │ ┌──────┴───────┐ │ ┌──────┴───────┐ │
194
194
  │ │UserPrompt │ │ │ Stop │ │
195
195
  │ │Submit: inject│ │ │ session end │ │
196
196
  │ │past context │ │ │ summarize │ │
197
- │ └──────────────┘ │ └──────────────┘ │
198
- │ │ │
199
- │ MCP Server (stdio) │ Health Monitor │
200
- │ ┌─────────────────────┐ │ ┌────────────────────────┐ │
201
- │ │ memory_recall │ │ │ sqlite, fts5, disk, │ │
202
- │ │ memory_entities │ │ │ integrity checks │ │
203
- │ │ memory_session_notes│ │ └────────────────────────┘ │
204
- │ │ memory_store │ │ │
205
- │ │ memory_context_budget│ │ Smart Resource Loader │
206
- │ │ memory_search ←L1 │ │ ┌────────────────────────┐ │
207
- │ │ memory_timeline ←L2 │ │ │ track usage → predict │ │
208
- │ │ memory_fetch ←L3 │ │ │ → budget → recommend │ │
209
- │ │ memory_health │ │ └────────────────────────┘ │
210
- │ └─────────────────────┘ │ │
211
- │ │ Browser UI (:37888) │
212
- │ │ ┌────────────────────────┐ │
213
- │ │ │ search, browse, stats │ │
214
- │ │ └────────────────────────┘ │
197
+ │ └──────────────┘ │ └──────────────┘ │
215
198
  │ │ │
216
- └────────────────────────────┼────────────────────────────────┘
199
+ │ MCP Server (stdio, long-lived) │
200
+ │ ┌─────────────────────────────────────────────────────┐ │
201
+ │ │ memory_recall memory_search (L1 index) │ │
202
+ │ │ memory_entities memory_timeline (L2 context) │ │
203
+ │ │ memory_session_notes memory_fetch (L3 full) │ │
204
+ │ │ memory_store memory_context_budget │ │
205
+ │ │ memory_health │ │
206
+ │ │ │ │
207
+ │ │ L1 WorkingMemory: read-through cache over L2 │ │
208
+ │ └─────────────────────────────────────────────────────┘ │
209
+ │ │
210
+ │ Resource Intelligence Browser UI (:37888) │
211
+ │ ┌──────────────────┐ ┌──────────────────┐ │
212
+ │ │ scan → track → │ │ search, browse, │ │
213
+ │ │ analyze overhead │ │ stats, health │ │
214
+ │ └──────────────────┘ └──────────────────┘ │
215
+ └─────────────────────────────────────────────────────────────┘
217
216
 
218
217
  ┌─────────┴──────────┐
219
218
  │ SQLite + FTS5 │
220
219
  │ ~/.claude- │
221
220
  │ memory-hub/ │
222
- │ memory.db │
223
221
  │ │
224
- sessions
225
- entities
226
- session_notes
227
- │ long_term_ │
228
- │ summaries │
229
- │ resource_usage │
230
- │ fts_memories │
231
- │ tfidf_index │
232
- │ embeddings │
233
- │ claude_md_ │
234
- │ registry │
235
- │ health_checks │
222
+ memory.db
223
+ batch/queue.jsonl
224
+ logs/
236
225
  └────────────────────┘
237
226
  ```
238
227
 
@@ -242,19 +231,20 @@ User prompt contains "remember that we use TypeScript strict"
242
231
 
243
232
  ```
244
233
  ┌─────────────────────────────────────────────────────┐
245
- │ L1: WorkingMemory in-process Map
246
- Current session only <1ms access
247
- Lives in MCP server FIFO 50 entries/session
234
+ │ L1: WorkingMemory Read-through cache
235
+ Lives in MCP server <1ms (cache hit)
236
+ Backed by SessionStore Auto-refresh on miss
237
+ │ TTL: 5 minutes Max 50 entries/session │
248
238
  ├─────────────────────────────────────────────────────┤
249
239
  │ L2: SessionStore SQLite │
250
240
  │ Entities + notes <10ms access │
251
- files_read, file_modified Per-session scope │
252
- errors, decisions Importance scored
241
+ files, errors, decisions Per-session scope │
242
+ observations (14 patterns) Importance scored 1-5
253
243
  ├─────────────────────────────────────────────────────┤
254
- │ L3: LongTermStore SQLite + FTS5 + TF-IDF
244
+ │ L3: LongTermStore SQLite + FTS5 + TF-IDF
255
245
  │ Cross-session summaries <100ms access │
256
246
  │ Hybrid ranked search Persistent forever │
257
- Auto-injected on start 3-layer progressive │
247
+ Semantic embeddings 3-layer progressive │
258
248
  └─────────────────────────────────────────────────────┘
259
249
  ```
260
250
 
@@ -270,7 +260,7 @@ bunx claude-memory-hub install
270
260
 
271
261
  One command. Registers MCP server + 5 hooks globally. Works on CLI, VS Code, JetBrains.
272
262
 
273
- **Coming from claude-mem?** The installer auto-detects `~/.claude-mem/claude-mem.db` and migrates your data automatically. No manual steps needed.
263
+ **Coming from claude-mem?** The installer auto-detects `~/.claude-mem/claude-mem.db` and migrates your data automatically.
274
264
 
275
265
  ### Update
276
266
 
@@ -278,24 +268,8 @@ One command. Registers MCP server + 5 hooks globally. Works on CLI, VS Code, Jet
278
268
  bunx claude-memory-hub@latest install
279
269
  ```
280
270
 
281
- Or if installed globally:
282
-
283
- ```bash
284
- bun install -g claude-memory-hub@latest
285
- claude-memory-hub install
286
- ```
287
-
288
271
  Your data at `~/.claude-memory-hub/` is preserved across updates. Schema migrations run automatically.
289
272
 
290
- ### From source
291
-
292
- ```bash
293
- git clone https://github.com/TranHoaiHung/claude-memory-hub.git ~/.claude-memory-hub
294
- cd ~/.claude-memory-hub
295
- bun install && bun run build:all
296
- bunx . install
297
- ```
298
-
299
273
  ### All CLI commands
300
274
 
301
275
  ```bash
@@ -305,10 +279,10 @@ bunx claude-memory-hub status # Check installation
305
279
  bunx claude-memory-hub migrate # Import data from claude-mem
306
280
  bunx claude-memory-hub viewer # Open browser UI at localhost:37888
307
281
  bunx claude-memory-hub health # Run health diagnostics
308
- bunx claude-memory-hub reindex # Rebuild TF-IDF search index
282
+ bunx claude-memory-hub reindex # Rebuild TF-IDF + embedding indexes
309
283
  bunx claude-memory-hub export # Export data as JSONL to stdout
310
- bunx claude-memory-hub import # Import JSONL from stdin
311
- bunx claude-memory-hub cleanup # Remove old data (default: 90 days)
284
+ bunx claude-memory-hub import # Import JSONL from stdin (--dry-run)
285
+ bunx claude-memory-hub cleanup # Remove old data (--days N, default 90)
312
286
  ```
313
287
 
314
288
  ### Requirements
@@ -327,13 +301,13 @@ Claude can call these tools directly during conversation:
327
301
 
328
302
  | Tool | What it does | When to use |
329
303
  |------|-------------|-------------|
330
- | `memory_recall` | FTS5 search past session summaries | Starting a task, looking for prior work |
304
+ | `memory_recall` | FTS5 search past sessions (supports `max_tokens`) | Starting a task, looking for prior work |
331
305
  | `memory_entities` | Find all sessions that touched a file | Before editing a file, understanding history |
332
- | `memory_session_notes` | Current session activity summary | Mid-session, checking what's been done |
306
+ | `memory_session_notes` | Current session activity (L1 cache) | Mid-session, checking what's been done |
333
307
  | `memory_store` | Manually save a note or decision | Preserving important context |
334
- | `memory_context_budget` | Analyze token costs + recommendations | Optimizing which resources to load |
308
+ | `memory_context_budget` | Analyze token costs + overhead report | Understanding resource usage |
335
309
 
336
- ### 3-Layer Search (new in v0.5)
310
+ ### 3-Layer Search
337
311
 
338
312
  | Tool | Layer | Tokens/result | When to use |
339
313
  |------|-------|---------------|-------------|
@@ -345,7 +319,46 @@ Claude can call these tools directly during conversation:
345
319
 
346
320
  | Tool | What it does |
347
321
  |------|-------------|
348
- | `memory_health` | Check database, FTS5, disk, integrity status |
322
+ | `memory_health` | Check database, FTS5, disk, embeddings, integrity status |
323
+
324
+ ---
325
+
326
+ ## Data Export/Import
327
+
328
+ ### Export
329
+
330
+ ```bash
331
+ # Full export
332
+ bunx claude-memory-hub export > backup.jsonl
333
+
334
+ # Incremental (since timestamp)
335
+ bunx claude-memory-hub export --since 1743580800000 > incremental.jsonl
336
+
337
+ # Single table
338
+ bunx claude-memory-hub export --table sessions > sessions.jsonl
339
+ ```
340
+
341
+ ### Import
342
+
343
+ ```bash
344
+ # Import from file
345
+ bunx claude-memory-hub import < backup.jsonl
346
+
347
+ # Validate without writing
348
+ bunx claude-memory-hub import --dry-run < backup.jsonl
349
+ ```
350
+
351
+ ### Cleanup
352
+
353
+ ```bash
354
+ # Remove data older than 90 days (default)
355
+ bunx claude-memory-hub cleanup
356
+
357
+ # Custom retention
358
+ bunx claude-memory-hub cleanup --days 30
359
+ ```
360
+
361
+ Format: JSONL (one JSON object per line). Embedding BLOBs encoded as base64. Import uses UPSERT — safe to re-run.
349
362
 
350
363
  ---
351
364
 
@@ -366,20 +379,14 @@ Opens a dark-themed dashboard at `http://localhost:37888` with:
366
379
 
367
380
  ## Migrating from claude-mem
368
381
 
369
- If you're already using [claude-mem](https://github.com/nicobailey-llc/claude-mem), migration is seamless:
370
-
371
382
  ```bash
372
383
  # Automatic (during install)
373
384
  bunx claude-memory-hub install
374
- # → Detects ~/.claude-mem/claude-mem.db automatically
375
- # → Migrates sessions, observations, summaries
376
385
 
377
386
  # Manual
378
387
  bunx claude-memory-hub migrate
379
388
  ```
380
389
 
381
- ### What gets migrated
382
-
383
390
  | claude-mem | → | memory-hub |
384
391
  |------------|---|------------|
385
392
  | `sdk_sessions` | → | `sessions` |
@@ -397,13 +404,14 @@ Migration is idempotent — safe to run multiple times with zero duplicates.
397
404
  | Version | What it solved |
398
405
  |---------|---------------|
399
406
  | **v0.1.0** | Cross-session memory, entity tracking, FTS5 search |
400
- | **v0.2.0** | Compact interceptor (PreCompact/PostCompact hooks), context enrichment, importance scoring |
407
+ | **v0.2.0** | Compact interceptor (PreCompact/PostCompact), context enrichment, importance scoring |
401
408
  | **v0.3.0** | Removed API key requirement, 1-command install |
402
- | **v0.4.0** | Smart resource loading, token budget optimization |
409
+ | **v0.4.0** | Resource usage tracking, token overhead analysis |
403
410
  | **v0.5.0** | Production hardening, hybrid search, 3-layer progressive search, browser UI, health monitoring, claude-mem migration |
404
- | **v0.6.0** | ResourceRegistry (170 resources), semantic search (384-dim embeddings), observation capture, CLAUDE.md tracking, 3-tier LLM summarization, overhead analysis |
411
+ | **v0.6.0** | ResourceRegistry (170 resources), semantic search (384-dim embeddings), observation capture, CLAUDE.md tracking, 3-tier LLM summarization |
405
412
  | **v0.7.0** | Honest resource analysis, semantic search scaling, batch embeddings, 14 observation patterns, DB auto-cleanup, summarizer retry |
406
- | **v0.8.0** | 91 unit tests (was 0%), L1 WorkingMemory read-through cache, PostToolUse batch queue (75ms→3ms), JSONL export/import CLI, data cleanup command |
413
+ | **v0.8.0** | 91 unit tests (was 0%), L1 read-through cache, PostToolUse batch queue (75ms→3ms), JSONL export/import, data cleanup CLI, CI/CD auto-publish |
414
+ | **v0.8.1** | Token-budget-aware MCP tools (`max_tokens`), proactive mid-session memory retrieval (topic-shift detection), session-end batch flush |
407
415
 
408
416
  See [CHANGELOG.md](CHANGELOG.md) for full details.
409
417
 
@@ -437,7 +445,11 @@ All data stored locally at `~/.claude-memory-hub/`.
437
445
 
438
446
  ```
439
447
  ~/.claude-memory-hub/
440
- ├── memory.db # SQLite database (sessions, entities, summaries)
448
+ ├── memory.db # SQLite database (all memory data)
449
+ ├── batch/
450
+ │ └── queue.jsonl # PostToolUse batch queue (auto-flushed)
451
+ ├── proactive/
452
+ │ └── <session>.json # Topic tracking state (auto-cleaned)
441
453
  └── logs/
442
454
  └── memory-hub.log # Structured JSON logs (auto-rotated at 5MB)
443
455
  ```
@@ -1717,7 +1717,7 @@ function safeJson(text, fallback) {
1717
1717
 
1718
1718
  // src/context/injection-validator.ts
1719
1719
  var log5 = createLogger("injection-validator");
1720
- var MAX_CHARS = 4500;
1720
+ var MAX_CHARS = 8000;
1721
1721
 
1722
1722
  class InjectionValidator {
1723
1723
  registry;
@@ -1424,7 +1424,7 @@ function safeJson(text, fallback) {
1424
1424
 
1425
1425
  // src/context/injection-validator.ts
1426
1426
  var log3 = createLogger("injection-validator");
1427
- var MAX_CHARS = 4500;
1427
+ var MAX_CHARS = 8000;
1428
1428
 
1429
1429
  class InjectionValidator {
1430
1430
  registry;
@@ -1833,6 +1833,132 @@ function isBatchEnabled() {
1833
1833
  return mode !== "disabled";
1834
1834
  }
1835
1835
 
1836
+ // src/retrieval/proactive-retrieval.ts
1837
+ import { existsSync as existsSync6, readFileSync as readFileSync4, writeFileSync as writeFileSync2, mkdirSync as mkdirSync4 } from "fs";
1838
+ import { join as join6 } from "path";
1839
+ import { homedir as homedir5 } from "os";
1840
+ var log6 = createLogger("proactive-retrieval");
1841
+ var DATA_DIR2 = join6(homedir5(), ".claude-memory-hub");
1842
+ var PROACTIVE_DIR = join6(DATA_DIR2, "proactive");
1843
+ var TOOL_CALL_INTERVAL = 15;
1844
+ var MAX_INJECTION_CHARS = 3000;
1845
+ function evaluateProactiveInjection(sessionId, toolName, toolInput, toolResponse) {
1846
+ const state = loadState(sessionId);
1847
+ state.toolCallCount++;
1848
+ const filePath = extractFilePath(toolName, toolInput);
1849
+ if (filePath) {
1850
+ state.recentFiles = [...new Set([filePath, ...state.recentFiles])].slice(0, 20);
1851
+ }
1852
+ const shouldTrigger = state.toolCallCount % TOOL_CALL_INTERVAL === 0 || toolName === "Bash" && typeof toolResponse.exit_code === "number" && toolResponse.exit_code !== 0 && state.toolCallCount > 5;
1853
+ if (!shouldTrigger) {
1854
+ saveState(sessionId, state);
1855
+ return { shouldInject: false };
1856
+ }
1857
+ const currentTopic = detectTopic(state.recentFiles);
1858
+ if (!currentTopic || state.injectedTopics.includes(currentTopic)) {
1859
+ saveState(sessionId, state);
1860
+ return { shouldInject: false };
1861
+ }
1862
+ const ltStore = new LongTermStore;
1863
+ const results = ltStore.search(currentTopic, 2);
1864
+ if (results.length === 0) {
1865
+ state.injectedTopics.push(currentTopic);
1866
+ saveState(sessionId, state);
1867
+ return { shouldInject: false };
1868
+ }
1869
+ const lines = [`**Relevant past context** (topic: ${currentTopic}):`];
1870
+ for (const r of results) {
1871
+ const date = new Date(r.created_at).toLocaleDateString();
1872
+ lines.push(`- [${date}] ${r.summary.slice(0, 200)}`);
1873
+ const files = safeJson4(r.files_touched, []);
1874
+ if (files.length > 0)
1875
+ lines.push(` Files: ${files.slice(0, 3).join(", ")}`);
1876
+ }
1877
+ let context = lines.join(`
1878
+ `);
1879
+ if (context.length > MAX_INJECTION_CHARS) {
1880
+ context = context.slice(0, MAX_INJECTION_CHARS) + `
1881
+ [...truncated]`;
1882
+ }
1883
+ state.injectedTopics.push(currentTopic);
1884
+ state.lastInjectionAt = Date.now();
1885
+ saveState(sessionId, state);
1886
+ log6.info("proactive injection triggered", { sessionId, topic: currentTopic, results: results.length });
1887
+ return { shouldInject: true, additionalContext: context };
1888
+ }
1889
+ function cleanupProactiveState(sessionId) {
1890
+ const path = statePath(sessionId);
1891
+ try {
1892
+ if (existsSync6(path)) {
1893
+ const { unlinkSync: unlinkSync2 } = __require("fs");
1894
+ unlinkSync2(path);
1895
+ }
1896
+ } catch {}
1897
+ }
1898
+ function detectTopic(recentFiles) {
1899
+ if (recentFiles.length < 3)
1900
+ return null;
1901
+ const dirs = recentFiles.map((f) => f.split("/").slice(0, -1).join("/")).filter(Boolean);
1902
+ const dirCounts = new Map;
1903
+ for (const d of dirs) {
1904
+ const parts = d.split("/").filter(Boolean);
1905
+ const leaf = parts[parts.length - 1];
1906
+ if (leaf && leaf !== "src" && leaf !== "lib" && leaf !== "utils") {
1907
+ dirCounts.set(leaf, (dirCounts.get(leaf) ?? 0) + 1);
1908
+ }
1909
+ }
1910
+ let bestTopic = null;
1911
+ let bestCount = 0;
1912
+ for (const [topic, count] of dirCounts) {
1913
+ if (count > bestCount) {
1914
+ bestTopic = topic;
1915
+ bestCount = count;
1916
+ }
1917
+ }
1918
+ const fileNames = recentFiles.map((f) => f.split("/").pop() ?? "").filter(Boolean);
1919
+ const keywords = ["auth", "payment", "user", "api", "database", "config", "test", "migration", "deploy", "search"];
1920
+ for (const kw of keywords) {
1921
+ const matches = fileNames.filter((f) => f.toLowerCase().includes(kw));
1922
+ if (matches.length >= 2)
1923
+ return kw;
1924
+ }
1925
+ return bestTopic;
1926
+ }
1927
+ function statePath(sessionId) {
1928
+ return join6(PROACTIVE_DIR, `${sessionId.replace(/[^a-zA-Z0-9_-]/g, "_")}.json`);
1929
+ }
1930
+ function loadState(sessionId) {
1931
+ const path = statePath(sessionId);
1932
+ try {
1933
+ if (existsSync6(path)) {
1934
+ return JSON.parse(readFileSync4(path, "utf-8"));
1935
+ }
1936
+ } catch {}
1937
+ return { toolCallCount: 0, lastInjectionAt: 0, injectedTopics: [], recentFiles: [] };
1938
+ }
1939
+ function saveState(sessionId, state) {
1940
+ try {
1941
+ if (!existsSync6(PROACTIVE_DIR)) {
1942
+ mkdirSync4(PROACTIVE_DIR, { recursive: true, mode: 448 });
1943
+ }
1944
+ writeFileSync2(statePath(sessionId), JSON.stringify(state), "utf-8");
1945
+ } catch {}
1946
+ }
1947
+ function extractFilePath(toolName, toolInput) {
1948
+ if (toolName === "Read" || toolName === "Write" || toolName === "Edit" || toolName === "MultiEdit") {
1949
+ const fp = toolInput.file_path;
1950
+ return typeof fp === "string" ? fp : undefined;
1951
+ }
1952
+ return;
1953
+ }
1954
+ function safeJson4(text, fallback) {
1955
+ try {
1956
+ return JSON.parse(text);
1957
+ } catch {
1958
+ return fallback;
1959
+ }
1960
+ }
1961
+
1836
1962
  // src/hooks-entry/post-tool-use.ts
1837
1963
  async function main() {
1838
1964
  if (process.env["CLAUDE_MEMORY_HUB_SKIP_HOOKS"] === "1")
@@ -1871,9 +1997,18 @@ async function main() {
1871
1997
  timestamp: Date.now()
1872
1998
  });
1873
1999
  tryFlush();
1874
- return;
1875
- } catch {}
2000
+ } catch {
2001
+ await handlePostToolUse(hook, project);
2002
+ }
2003
+ } else {
2004
+ await handlePostToolUse(hook, project);
1876
2005
  }
1877
- await handlePostToolUse(hook, project);
2006
+ try {
2007
+ const result = evaluateProactiveInjection(hook.session_id, hook.tool_name, hook.tool_input ?? {}, hook.tool_response ?? {});
2008
+ if (result.shouldInject && result.additionalContext) {
2009
+ process.stdout.write(JSON.stringify({ additionalContext: result.additionalContext }) + `
2010
+ `);
2011
+ }
2012
+ } catch {}
1878
2013
  }
1879
2014
  main().catch(() => {}).finally(() => process.exit(0));
@@ -1717,7 +1717,7 @@ function safeJson(text, fallback) {
1717
1717
 
1718
1718
  // src/context/injection-validator.ts
1719
1719
  var log5 = createLogger("injection-validator");
1720
- var MAX_CHARS = 4500;
1720
+ var MAX_CHARS = 8000;
1721
1721
 
1722
1722
  class InjectionValidator {
1723
1723
  registry;
@@ -1424,7 +1424,7 @@ function safeJson(text, fallback) {
1424
1424
 
1425
1425
  // src/context/injection-validator.ts
1426
1426
  var log3 = createLogger("injection-validator");
1427
- var MAX_CHARS = 4500;
1427
+ var MAX_CHARS = 8000;
1428
1428
 
1429
1429
  class InjectionValidator {
1430
1430
  registry;
@@ -2021,6 +2021,250 @@ async function indexEmbedding(docType, docId, text, db) {
2021
2021
  created_at = excluded.created_at`, [docType, docId, blob, Date.now()]);
2022
2022
  }
2023
2023
 
2024
+ // src/retrieval/proactive-retrieval.ts
2025
+ import { existsSync as existsSync5, readFileSync as readFileSync3, writeFileSync, mkdirSync as mkdirSync3 } from "fs";
2026
+ import { join as join5 } from "path";
2027
+ import { homedir as homedir4 } from "os";
2028
+ var log9 = createLogger("proactive-retrieval");
2029
+ var DATA_DIR = join5(homedir4(), ".claude-memory-hub");
2030
+ var PROACTIVE_DIR = join5(DATA_DIR, "proactive");
2031
+ var TOOL_CALL_INTERVAL = 15;
2032
+ var MAX_INJECTION_CHARS = 3000;
2033
+ function evaluateProactiveInjection(sessionId, toolName, toolInput, toolResponse) {
2034
+ const state = loadState(sessionId);
2035
+ state.toolCallCount++;
2036
+ const filePath = extractFilePath(toolName, toolInput);
2037
+ if (filePath) {
2038
+ state.recentFiles = [...new Set([filePath, ...state.recentFiles])].slice(0, 20);
2039
+ }
2040
+ const shouldTrigger = state.toolCallCount % TOOL_CALL_INTERVAL === 0 || toolName === "Bash" && typeof toolResponse.exit_code === "number" && toolResponse.exit_code !== 0 && state.toolCallCount > 5;
2041
+ if (!shouldTrigger) {
2042
+ saveState(sessionId, state);
2043
+ return { shouldInject: false };
2044
+ }
2045
+ const currentTopic = detectTopic(state.recentFiles);
2046
+ if (!currentTopic || state.injectedTopics.includes(currentTopic)) {
2047
+ saveState(sessionId, state);
2048
+ return { shouldInject: false };
2049
+ }
2050
+ const ltStore = new LongTermStore;
2051
+ const results = ltStore.search(currentTopic, 2);
2052
+ if (results.length === 0) {
2053
+ state.injectedTopics.push(currentTopic);
2054
+ saveState(sessionId, state);
2055
+ return { shouldInject: false };
2056
+ }
2057
+ const lines = [`**Relevant past context** (topic: ${currentTopic}):`];
2058
+ for (const r of results) {
2059
+ const date = new Date(r.created_at).toLocaleDateString();
2060
+ lines.push(`- [${date}] ${r.summary.slice(0, 200)}`);
2061
+ const files = safeJson4(r.files_touched, []);
2062
+ if (files.length > 0)
2063
+ lines.push(` Files: ${files.slice(0, 3).join(", ")}`);
2064
+ }
2065
+ let context = lines.join(`
2066
+ `);
2067
+ if (context.length > MAX_INJECTION_CHARS) {
2068
+ context = context.slice(0, MAX_INJECTION_CHARS) + `
2069
+ [...truncated]`;
2070
+ }
2071
+ state.injectedTopics.push(currentTopic);
2072
+ state.lastInjectionAt = Date.now();
2073
+ saveState(sessionId, state);
2074
+ log9.info("proactive injection triggered", { sessionId, topic: currentTopic, results: results.length });
2075
+ return { shouldInject: true, additionalContext: context };
2076
+ }
2077
+ function cleanupProactiveState(sessionId) {
2078
+ const path = statePath(sessionId);
2079
+ try {
2080
+ if (existsSync5(path)) {
2081
+ const { unlinkSync } = __require("fs");
2082
+ unlinkSync(path);
2083
+ }
2084
+ } catch {}
2085
+ }
2086
+ function detectTopic(recentFiles) {
2087
+ if (recentFiles.length < 3)
2088
+ return null;
2089
+ const dirs = recentFiles.map((f) => f.split("/").slice(0, -1).join("/")).filter(Boolean);
2090
+ const dirCounts = new Map;
2091
+ for (const d of dirs) {
2092
+ const parts = d.split("/").filter(Boolean);
2093
+ const leaf = parts[parts.length - 1];
2094
+ if (leaf && leaf !== "src" && leaf !== "lib" && leaf !== "utils") {
2095
+ dirCounts.set(leaf, (dirCounts.get(leaf) ?? 0) + 1);
2096
+ }
2097
+ }
2098
+ let bestTopic = null;
2099
+ let bestCount = 0;
2100
+ for (const [topic, count] of dirCounts) {
2101
+ if (count > bestCount) {
2102
+ bestTopic = topic;
2103
+ bestCount = count;
2104
+ }
2105
+ }
2106
+ const fileNames = recentFiles.map((f) => f.split("/").pop() ?? "").filter(Boolean);
2107
+ const keywords = ["auth", "payment", "user", "api", "database", "config", "test", "migration", "deploy", "search"];
2108
+ for (const kw of keywords) {
2109
+ const matches = fileNames.filter((f) => f.toLowerCase().includes(kw));
2110
+ if (matches.length >= 2)
2111
+ return kw;
2112
+ }
2113
+ return bestTopic;
2114
+ }
2115
+ function statePath(sessionId) {
2116
+ return join5(PROACTIVE_DIR, `${sessionId.replace(/[^a-zA-Z0-9_-]/g, "_")}.json`);
2117
+ }
2118
+ function loadState(sessionId) {
2119
+ const path = statePath(sessionId);
2120
+ try {
2121
+ if (existsSync5(path)) {
2122
+ return JSON.parse(readFileSync3(path, "utf-8"));
2123
+ }
2124
+ } catch {}
2125
+ return { toolCallCount: 0, lastInjectionAt: 0, injectedTopics: [], recentFiles: [] };
2126
+ }
2127
+ function saveState(sessionId, state) {
2128
+ try {
2129
+ if (!existsSync5(PROACTIVE_DIR)) {
2130
+ mkdirSync3(PROACTIVE_DIR, { recursive: true, mode: 448 });
2131
+ }
2132
+ writeFileSync(statePath(sessionId), JSON.stringify(state), "utf-8");
2133
+ } catch {}
2134
+ }
2135
+ function extractFilePath(toolName, toolInput) {
2136
+ if (toolName === "Read" || toolName === "Write" || toolName === "Edit" || toolName === "MultiEdit") {
2137
+ const fp = toolInput.file_path;
2138
+ return typeof fp === "string" ? fp : undefined;
2139
+ }
2140
+ return;
2141
+ }
2142
+ function safeJson4(text, fallback) {
2143
+ try {
2144
+ return JSON.parse(text);
2145
+ } catch {
2146
+ return fallback;
2147
+ }
2148
+ }
2149
+
2150
+ // src/capture/batch-queue.ts
2151
+ import { existsSync as existsSync6, mkdirSync as mkdirSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync2, appendFileSync as appendFileSync2, unlinkSync, statSync as statSync2 } from "fs";
2152
+ import { join as join6 } from "path";
2153
+ import { homedir as homedir5 } from "os";
2154
+ var log10 = createLogger("batch-queue");
2155
+ var DATA_DIR2 = join6(homedir5(), ".claude-memory-hub");
2156
+ var BATCH_DIR = join6(DATA_DIR2, "batch");
2157
+ var QUEUE_PATH = join6(BATCH_DIR, "queue.jsonl");
2158
+ var LOCK_PATH = join6(BATCH_DIR, "queue.lock");
2159
+ var MAX_QUEUE_SIZE = 100 * 1024;
2160
+ var LOCK_STALE_MS = 30000;
2161
+ function enqueueEvent(event) {
2162
+ try {
2163
+ ensureBatchDir();
2164
+ const line = JSON.stringify(event) + `
2165
+ `;
2166
+ appendFileSync2(QUEUE_PATH, line, "utf-8");
2167
+ } catch (err) {
2168
+ log10.error("enqueue failed", { error: String(err) });
2169
+ throw err;
2170
+ }
2171
+ }
2172
+ function tryFlush() {
2173
+ try {
2174
+ if (!existsSync6(QUEUE_PATH))
2175
+ return false;
2176
+ const stat = statSync2(QUEUE_PATH);
2177
+ if (stat.size === 0)
2178
+ return false;
2179
+ if (!tryAcquireLock())
2180
+ return false;
2181
+ try {
2182
+ flushQueue();
2183
+ return true;
2184
+ } finally {
2185
+ releaseLock();
2186
+ }
2187
+ } catch (err) {
2188
+ log10.error("flush failed", { error: String(err) });
2189
+ return false;
2190
+ }
2191
+ }
2192
+ function flushQueue() {
2193
+ const content = readFileSync4(QUEUE_PATH, "utf-8").trim();
2194
+ if (!content)
2195
+ return;
2196
+ const events = [];
2197
+ for (const line of content.split(`
2198
+ `)) {
2199
+ try {
2200
+ events.push(JSON.parse(line));
2201
+ } catch {
2202
+ log10.warn("skipping malformed queue line");
2203
+ }
2204
+ }
2205
+ if (events.length === 0)
2206
+ return;
2207
+ const store = new SessionStore;
2208
+ const tracker = new ResourceTracker;
2209
+ const registry = getResourceRegistry();
2210
+ const db = store["db"];
2211
+ db.transaction(() => {
2212
+ for (const event of events) {
2213
+ store.upsertSession({
2214
+ id: event.session.id,
2215
+ project: event.session.project,
2216
+ started_at: event.session.started_at,
2217
+ status: "active"
2218
+ });
2219
+ for (const entity of event.entities) {
2220
+ store.insertEntity({ ...entity, project: event.session.project });
2221
+ }
2222
+ if (event.resources) {
2223
+ for (const r of event.resources) {
2224
+ const resource = registry.resolve(r.type, r.name);
2225
+ tracker.trackUsage(event.session.id, event.session.project, r.type, r.name, r.tokenCost ?? resource?.full_tokens ?? 0);
2226
+ }
2227
+ }
2228
+ }
2229
+ })();
2230
+ writeFileSync2(QUEUE_PATH, "", "utf-8");
2231
+ log10.info("batch flushed", { events: events.length });
2232
+ }
2233
+ function tryAcquireLock() {
2234
+ try {
2235
+ if (existsSync6(LOCK_PATH)) {
2236
+ const lockContent = readFileSync4(LOCK_PATH, "utf-8").trim();
2237
+ const [pidStr, timestampStr] = lockContent.split(":");
2238
+ const lockTime = Number(timestampStr);
2239
+ if (Date.now() - lockTime < LOCK_STALE_MS) {
2240
+ const pid = Number(pidStr);
2241
+ try {
2242
+ process.kill(pid, 0);
2243
+ return false;
2244
+ } catch {}
2245
+ }
2246
+ }
2247
+ writeFileSync2(LOCK_PATH, `${process.pid}:${Date.now()}`, "utf-8");
2248
+ return true;
2249
+ } catch {
2250
+ return false;
2251
+ }
2252
+ }
2253
+ function releaseLock() {
2254
+ try {
2255
+ unlinkSync(LOCK_PATH);
2256
+ } catch {}
2257
+ }
2258
+ function ensureBatchDir() {
2259
+ if (!existsSync6(BATCH_DIR)) {
2260
+ mkdirSync4(BATCH_DIR, { recursive: true, mode: 448 });
2261
+ }
2262
+ }
2263
+ function isBatchEnabled() {
2264
+ const mode = process.env["CLAUDE_MEMORY_HUB_BATCH"] ?? "auto";
2265
+ return mode !== "disabled";
2266
+ }
2267
+
2024
2268
  // src/hooks-entry/session-end.ts
2025
2269
  async function main() {
2026
2270
  if (process.env["CLAUDE_MEMORY_HUB_SKIP_HOOKS"] === "1")
@@ -2036,6 +2280,10 @@ async function main() {
2036
2280
  }
2037
2281
  const project = projectFromCwd(hook.cwd ?? process.env["CLAUDE_CWD"] ?? process.cwd());
2038
2282
  await handleSessionEnd(hook, project);
2283
+ try {
2284
+ tryFlush();
2285
+ } catch {}
2286
+ cleanupProactiveState(hook.session_id);
2039
2287
  const store = new SessionStore;
2040
2288
  if (store.getSession(hook.session_id)) {
2041
2289
  await new SessionSummarizer().summarize(hook.session_id, project).catch(() => {});
@@ -1424,7 +1424,7 @@ function safeJson(text, fallback) {
1424
1424
 
1425
1425
  // src/context/injection-validator.ts
1426
1426
  var log3 = createLogger("injection-validator");
1427
- var MAX_CHARS = 4500;
1427
+ var MAX_CHARS = 8000;
1428
1428
 
1429
1429
  class InjectionValidator {
1430
1430
  registry;
package/dist/index.js CHANGED
@@ -15593,8 +15593,17 @@ function sanitizeFtsQuery2(query) {
15593
15593
  }
15594
15594
 
15595
15595
  // src/mcp/tool-handlers.ts
15596
+ function truncateToTokenBudget(text, maxTokens) {
15597
+ if (!maxTokens || maxTokens <= 0)
15598
+ return text;
15599
+ const maxChars = maxTokens * 4;
15600
+ if (text.length <= maxChars)
15601
+ return text;
15602
+ return text.slice(0, maxChars) + `
15603
+ [...truncated to fit ~` + maxTokens + " token budget]";
15604
+ }
15596
15605
  async function handleMemoryRecall(args) {
15597
- const { query, limit = 5 } = args;
15606
+ const { query, limit = 5, max_tokens } = args;
15598
15607
  if (!query?.trim())
15599
15608
  return "No query provided.";
15600
15609
  const builder = new ContextBuilder;
@@ -15604,9 +15613,10 @@ async function handleMemoryRecall(args) {
15604
15613
 
15605
15614
  This appears to be a new topic with no prior history.`;
15606
15615
  }
15607
- return `Found ${ctx.resultCount} relevant memory(ies) (~${ctx.tokenEstimate} tokens):
15616
+ const output = `Found ${ctx.resultCount} relevant memory(ies) (~${ctx.tokenEstimate} tokens):
15608
15617
 
15609
15618
  ${ctx.text}`;
15619
+ return truncateToTokenBudget(output, max_tokens);
15610
15620
  }
15611
15621
  async function handleMemoryEntities(args) {
15612
15622
  const { file_path } = args;
@@ -15648,11 +15658,11 @@ async function handleMemoryStore(args) {
15648
15658
  return `Note saved to session ${session_id}.`;
15649
15659
  }
15650
15660
  async function handleMemorySearch(args) {
15651
- const { query, limit = 20, offset = 0, project } = args;
15661
+ const { query, limit = 20, offset = 0, project, max_tokens } = args;
15652
15662
  if (!query?.trim())
15653
15663
  return "No query provided.";
15654
15664
  const results = await searchIndex(query, { limit: Math.min(limit, 50), offset, ...project ? { project } : {} });
15655
- return formatSearchIndex(results);
15665
+ return truncateToTokenBudget(formatSearchIndex(results), max_tokens);
15656
15666
  }
15657
15667
  async function handleMemoryTimeline(args) {
15658
15668
  const { id, type, depth = 3 } = args;
@@ -15662,11 +15672,11 @@ async function handleMemoryTimeline(args) {
15662
15672
  return formatTimeline(entries);
15663
15673
  }
15664
15674
  async function handleMemoryFetch(args) {
15665
- const { ids } = args;
15675
+ const { ids, max_tokens } = args;
15666
15676
  if (!ids?.length)
15667
15677
  return "No IDs provided.";
15668
15678
  const records = fetchFullRecords(ids.slice(0, 20));
15669
- return formatFullRecords(records);
15679
+ return truncateToTokenBudget(formatFullRecords(records), max_tokens);
15670
15680
  }
15671
15681
  async function handleMemoryHealth() {
15672
15682
  const report = runHealthCheck();
@@ -15741,7 +15751,8 @@ var TOOL_DEFINITIONS = [
15741
15751
  type: "object",
15742
15752
  properties: {
15743
15753
  query: { type: "string", description: "Natural language search query" },
15744
- limit: { type: "number", description: "Max results (default 5, max 10)" }
15754
+ limit: { type: "number", description: "Max results (default 5, max 10)" },
15755
+ max_tokens: { type: "number", description: "Max output tokens. Results truncated to fit budget. Default: unlimited" }
15745
15756
  },
15746
15757
  required: ["query"]
15747
15758
  }
@@ -15800,7 +15811,8 @@ var TOOL_DEFINITIONS = [
15800
15811
  query: { type: "string", description: "Search query" },
15801
15812
  limit: { type: "number", description: "Max results (default 20)" },
15802
15813
  offset: { type: "number", description: "Pagination offset (default 0)" },
15803
- project: { type: "string", description: "Filter by project" }
15814
+ project: { type: "string", description: "Filter by project" },
15815
+ max_tokens: { type: "number", description: "Max output tokens. Results truncated to fit budget" }
15804
15816
  },
15805
15817
  required: ["query"]
15806
15818
  }
@@ -15835,7 +15847,8 @@ var TOOL_DEFINITIONS = [
15835
15847
  required: ["id", "type"]
15836
15848
  },
15837
15849
  description: "Array of {id, type} from search results"
15838
- }
15850
+ },
15851
+ max_tokens: { type: "number", description: "Max output tokens. Records truncated to fit budget" }
15839
15852
  },
15840
15853
  required: ["ids"]
15841
15854
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-memory-hub",
3
- "version": "0.8.0",
3
+ "version": "0.8.2",
4
4
  "description": "Persistent memory system for Claude Code. Zero API key. Zero Python. 5 hooks + MCP server + SQLite FTS5 + semantic search.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",