claude-memory-hub 0.8.0 → 0.8.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/CHANGELOG.md +42 -0
- package/README.md +120 -133
- package/dist/hooks/post-tool-use.js +138 -3
- package/dist/hooks/session-end.js +248 -0
- package/dist/index.js +22 -9
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,48 @@ Format follows [Keep a Changelog](https://keepachangelog.com/).
|
|
|
5
5
|
|
|
6
6
|
---
|
|
7
7
|
|
|
8
|
+
## [0.8.1] - 2026-04-02
|
|
9
|
+
|
|
10
|
+
Token-budget-aware MCP tools + proactive mid-session memory retrieval.
|
|
11
|
+
|
|
12
|
+
### Token Budget Management
|
|
13
|
+
|
|
14
|
+
- **`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
|
|
15
|
+
- **`truncateToTokenBudget()` utility** — shared truncation function with `[...truncated to fit ~N token budget]` suffix
|
|
16
|
+
|
|
17
|
+
### Proactive Memory Retrieval
|
|
18
|
+
|
|
19
|
+
- **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
|
|
20
|
+
- **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
|
|
21
|
+
- **Trigger conditions:** every 15 tool calls OR on Bash errors after warmup (5+ calls)
|
|
22
|
+
- **State tracking** — per-session state at `~/.claude-memory-hub/proactive/<session_id>.json`, cleaned up on session end
|
|
23
|
+
- **Injection cap:** ~375 tokens (1500 chars) per injection, deduplicated by topic
|
|
24
|
+
|
|
25
|
+
### Session End Improvements
|
|
26
|
+
|
|
27
|
+
- **Batch queue flush on session end** — `tryFlush()` called during Stop hook to prevent data loss from unflushed batch events
|
|
28
|
+
- **Proactive state cleanup** — per-session state files removed on session end
|
|
29
|
+
|
|
30
|
+
### Research Findings (documented, no code changes needed)
|
|
31
|
+
|
|
32
|
+
Based on deep Claude Code source analysis:
|
|
33
|
+
- **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
|
|
34
|
+
- **Multi-agent sharing:** Subagents inherit parent MCP servers via `initializeAgentMcpServers()`. Memory sharing via `memory_recall` works out-of-box — zero implementation needed
|
|
35
|
+
- **Permission-aware:** PostToolUse hook only fires for approved tools. Denied tools fire separate `PermissionDenied` hook. memory-hub is already permission-aware by design
|
|
36
|
+
- **IDE context:** Available as attachments in conversation (ide_selection, ide_opened_file) but not in hook inputs directly. Entity extraction captures file activity indirectly
|
|
37
|
+
|
|
38
|
+
### Modified Files
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
src/mcp/tool-definitions.ts — max_tokens param on 3 tools
|
|
42
|
+
src/mcp/tool-handlers.ts — truncateToTokenBudget() utility
|
|
43
|
+
src/retrieval/proactive-retrieval.ts — NEW: topic detection + injection
|
|
44
|
+
src/hooks-entry/post-tool-use.ts — proactive retrieval integration
|
|
45
|
+
src/hooks-entry/session-end.ts — batch flush + proactive cleanup
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
8
50
|
## [0.8.0] - 2026-04-02
|
|
9
51
|
|
|
10
52
|
Major release: test infrastructure, architectural fixes, hook performance, data portability.
|
package/README.md
CHANGED
|
@@ -19,7 +19,7 @@ Zero API key. Zero Python. Zero config. One install command.
|
|
|
19
19
|
|
|
20
20
|
## The Problem
|
|
21
21
|
|
|
22
|
-
Claude Code forgets everything between sessions. Within long sessions, auto-compact destroys 90% of context.
|
|
22
|
+
Claude Code forgets everything between sessions. Within long sessions, auto-compact destroys 90% of context. Search is keyword-only with no ranking.
|
|
23
23
|
|
|
24
24
|
```
|
|
25
25
|
Session 1: You spend 2 hours building auth system
|
|
@@ -29,37 +29,31 @@ Long session: Claude auto-compacts at 200K tokens
|
|
|
29
29
|
→ 180K tokens of context vaporized
|
|
30
30
|
→ Claude loses track of files, decisions, errors
|
|
31
31
|
|
|
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
32
|
Search: Keyword-only, no semantic ranking
|
|
38
33
|
→ Irrelevant results, wasted tokens on full records
|
|
39
34
|
```
|
|
40
35
|
|
|
41
|
-
**Four problems. memory-hub solves three directly and provides analysis for the fourth.**
|
|
42
|
-
|
|
43
36
|
| Problem | Claude Code built-in | claude-mem | memory-hub |
|
|
44
37
|
|---------|:-------------------:|:----------:|:----------:|
|
|
45
38
|
| Cross-session memory | -- | Yes | **Yes** |
|
|
46
39
|
| Influence what compact preserves | -- | -- | **Yes** |
|
|
47
|
-
| Save compact output | -- | -- | **Yes** |
|
|
48
|
-
| Token overhead analysis | -- | -- | **Yes** |
|
|
49
|
-
| Semantic search (embeddings) | -- | Chroma (external) | **Yes (offline)** |
|
|
40
|
+
| Save compact output to L3 | -- | -- | **Yes** |
|
|
50
41
|
| Hybrid search (FTS5 + TF-IDF + semantic) | -- | Partial | **Yes** |
|
|
51
42
|
| 3-layer progressive search | -- | Yes | **Yes** |
|
|
52
43
|
| Resource overhead analysis | -- | -- | **Yes** |
|
|
53
44
|
| CLAUDE.md rule tracking | -- | -- | **Yes** |
|
|
54
|
-
|
|
|
45
|
+
| Observation capture (14 patterns) | -- | Yes | **Yes** |
|
|
55
46
|
| LLM summarization (3-tier) | -- | Yes (API) | **Yes (free)** |
|
|
47
|
+
| Token-budget-aware tools (`max_tokens`) | -- | -- | **Yes** |
|
|
48
|
+
| Proactive mid-session retrieval | -- | -- | **Yes** |
|
|
49
|
+
| Multi-agent memory sharing | -- | -- | **Yes (free)** |
|
|
50
|
+
| Permission-aware (approved only) | -- | -- | **Yes** |
|
|
51
|
+
| Data export/import (JSONL) | -- | -- | **Yes** |
|
|
52
|
+
| Hook batching (3ms vs 75ms) | -- | -- | **Yes** |
|
|
56
53
|
| Browser UI | -- | Yes | **Yes** |
|
|
57
|
-
| Health monitoring | -- | -- | **Yes** |
|
|
58
|
-
|
|
|
59
|
-
| No API key
|
|
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** |
|
|
54
|
+
| Health monitoring + auto-cleanup | -- | -- | **Yes** |
|
|
55
|
+
| Unit tests (91 tests) | N/A | -- | **Yes** |
|
|
56
|
+
| No API key / Python / Chroma | N/A | Partial | **Yes** |
|
|
63
57
|
|
|
64
58
|
---
|
|
65
59
|
|
|
@@ -75,6 +69,8 @@ Claude makes a decision → memory-hub records: decision text + importance score
|
|
|
75
69
|
```
|
|
76
70
|
|
|
77
71
|
No XML. No special format. Extracted directly from hook JSON metadata.
|
|
72
|
+
PostToolUse events are batched via write-through queue (~3ms per event vs ~75ms direct).
|
|
73
|
+
Mid-session topic shifts auto-inject relevant past context (proactive retrieval).
|
|
78
74
|
|
|
79
75
|
### Layer 2 — Compact Interceptor (the key innovation)
|
|
80
76
|
|
|
@@ -99,41 +95,21 @@ No XML. No special format. Extracted directly from hook JSON metadata.
|
|
|
99
95
|
zero information loss
|
|
100
96
|
```
|
|
101
97
|
|
|
102
|
-
**
|
|
98
|
+
**No other memory tool does this.** memory-hub is the only system that **tells the compact what matters**.
|
|
103
99
|
|
|
104
100
|
### Layer 3 — Cross-Session Memory
|
|
105
101
|
|
|
106
102
|
```
|
|
107
|
-
Session N ends →
|
|
108
|
-
|
|
103
|
+
Session N ends → 3-tier summarization: PostCompact > CLI claude > rule-based
|
|
104
|
+
→ Summary saved to SQLite L3 with FTS5 indexing
|
|
109
105
|
|
|
110
106
|
Session N+1 → UserPromptSubmit hook fires
|
|
111
|
-
→ FTS5 + TF-IDF
|
|
112
|
-
→
|
|
107
|
+
→ FTS5 + TF-IDF + semantic search: match user prompt
|
|
108
|
+
→ Inject relevant past context automatically
|
|
113
109
|
→ Claude starts with history, not from zero
|
|
114
110
|
```
|
|
115
111
|
|
|
116
|
-
### Layer 4 —
|
|
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)
|
|
112
|
+
### Layer 4 — 3-Layer Progressive Search
|
|
137
113
|
|
|
138
114
|
```
|
|
139
115
|
Traditional search: query → ALL full records → 5000+ tokens wasted
|
|
@@ -146,34 +122,33 @@ memory-hub search: query → Layer 1 (index) → ~50 tokens/result
|
|
|
146
122
|
Token savings: ~80-90% vs. full context
|
|
147
123
|
```
|
|
148
124
|
|
|
149
|
-
Hybrid ranking: FTS5 BM25 (keyword) + TF-IDF (term frequency) +
|
|
125
|
+
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
126
|
|
|
151
|
-
### Layer
|
|
127
|
+
### Layer 5 — Resource Intelligence
|
|
152
128
|
|
|
153
129
|
```
|
|
154
130
|
ResourceRegistry scans ALL .claude locations:
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
~/.claude/CLAUDE.md + project CLAUDE.md chain
|
|
161
|
-
|
|
162
|
-
OverheadReport:
|
|
163
|
-
"56/64 skills unused in last 10 sessions → ~1033 listing tokens wasted"
|
|
164
|
-
"CLAUDE.md chain is 3222 tokens"
|
|
131
|
+
skills, agents, commands, workflows, CLAUDE.md chain
|
|
132
|
+
→ 3-level token estimation: listing, full, total
|
|
133
|
+
|
|
134
|
+
ResourceTracker records actual usage per session
|
|
135
|
+
OverheadReport identifies unused resources + token waste
|
|
165
136
|
```
|
|
166
137
|
|
|
167
|
-
|
|
138
|
+
> **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.
|
|
139
|
+
|
|
140
|
+
### Layer 6 — Observation Capture
|
|
168
141
|
|
|
169
142
|
```
|
|
170
143
|
Tool output contains "IMPORTANT: always pool DB connections"
|
|
171
144
|
→ observation entity (importance=4) saved to L2
|
|
172
|
-
→ included in session summary
|
|
173
|
-
→ searchable across sessions
|
|
174
145
|
|
|
175
146
|
User prompt contains "remember that we use TypeScript strict"
|
|
176
147
|
→ observation entity (importance=3) saved to L2
|
|
148
|
+
|
|
149
|
+
14 heuristic patterns: IMPORTANT, CRITICAL, SECURITY, DEPRECATED,
|
|
150
|
+
decision:, discovered, root cause, switched to, TODO:, FIXME:,
|
|
151
|
+
HACK:, performance:, bottleneck, OOM, don't, never, prefer, etc.
|
|
177
152
|
```
|
|
178
153
|
|
|
179
154
|
---
|
|
@@ -187,7 +162,7 @@ User prompt contains "remember that we use TypeScript strict"
|
|
|
187
162
|
│ 5 Lifecycle Hooks │
|
|
188
163
|
│ ┌───────────────┐ ┌──────────────┐ ┌──────────────┐ │
|
|
189
164
|
│ │ PostToolUse │ │ PreCompact │ │ PostCompact │ │
|
|
190
|
-
│ │
|
|
165
|
+
│ │ batch queue │ │ inject │ │ save summary │ │
|
|
191
166
|
│ └──────┬────────┘ │ priorities │ └──────┬───────┘ │
|
|
192
167
|
│ │ └──────┬───────┘ │ │
|
|
193
168
|
│ ┌──────┴───────┐ │ ┌──────┴───────┐ │
|
|
@@ -196,43 +171,32 @@ User prompt contains "remember that we use TypeScript strict"
|
|
|
196
171
|
│ │past context │ │ │ summarize │ │
|
|
197
172
|
│ └──────────────┘ │ └──────────────┘ │
|
|
198
173
|
│ │ │
|
|
199
|
-
│ MCP Server (stdio)
|
|
200
|
-
│
|
|
201
|
-
│ │ memory_recall
|
|
202
|
-
│ │ memory_entities
|
|
203
|
-
│ │ memory_session_notes
|
|
204
|
-
│ │ memory_store
|
|
205
|
-
│ │
|
|
206
|
-
│ │
|
|
207
|
-
│ │
|
|
208
|
-
│ │
|
|
209
|
-
│
|
|
210
|
-
│
|
|
211
|
-
│
|
|
212
|
-
│
|
|
213
|
-
│
|
|
214
|
-
│
|
|
215
|
-
|
|
216
|
-
└────────────────────────────┼────────────────────────────────┘
|
|
174
|
+
│ MCP Server (stdio, long-lived) │
|
|
175
|
+
│ ┌─────────────────────────────────────────────────────┐ │
|
|
176
|
+
│ │ memory_recall memory_search (L1 index) │ │
|
|
177
|
+
│ │ memory_entities memory_timeline (L2 context) │ │
|
|
178
|
+
│ │ memory_session_notes memory_fetch (L3 full) │ │
|
|
179
|
+
│ │ memory_store memory_context_budget │ │
|
|
180
|
+
│ │ memory_health │ │
|
|
181
|
+
│ │ │ │
|
|
182
|
+
│ │ L1 WorkingMemory: read-through cache over L2 │ │
|
|
183
|
+
│ └─────────────────────────────────────────────────────┘ │
|
|
184
|
+
│ │
|
|
185
|
+
│ Resource Intelligence Browser UI (:37888) │
|
|
186
|
+
│ ┌──────────────────┐ ┌──────────────────┐ │
|
|
187
|
+
│ │ scan → track → │ │ search, browse, │ │
|
|
188
|
+
│ │ analyze overhead │ │ stats, health │ │
|
|
189
|
+
│ └──────────────────┘ └──────────────────┘ │
|
|
190
|
+
└──────────────────────────────────────────────────────────────┘
|
|
217
191
|
│
|
|
218
192
|
┌─────────┴──────────┐
|
|
219
193
|
│ SQLite + FTS5 │
|
|
220
194
|
│ ~/.claude- │
|
|
221
195
|
│ memory-hub/ │
|
|
222
|
-
│ memory.db │
|
|
223
196
|
│ │
|
|
224
|
-
│
|
|
225
|
-
│
|
|
226
|
-
│
|
|
227
|
-
│ long_term_ │
|
|
228
|
-
│ summaries │
|
|
229
|
-
│ resource_usage │
|
|
230
|
-
│ fts_memories │
|
|
231
|
-
│ tfidf_index │
|
|
232
|
-
│ embeddings │
|
|
233
|
-
│ claude_md_ │
|
|
234
|
-
│ registry │
|
|
235
|
-
│ health_checks │
|
|
197
|
+
│ memory.db │
|
|
198
|
+
│ batch/queue.jsonl │
|
|
199
|
+
│ logs/ │
|
|
236
200
|
└────────────────────┘
|
|
237
201
|
```
|
|
238
202
|
|
|
@@ -242,19 +206,20 @@ User prompt contains "remember that we use TypeScript strict"
|
|
|
242
206
|
|
|
243
207
|
```
|
|
244
208
|
┌─────────────────────────────────────────────────────┐
|
|
245
|
-
│ L1: WorkingMemory
|
|
246
|
-
│
|
|
247
|
-
│
|
|
209
|
+
│ L1: WorkingMemory Read-through cache │
|
|
210
|
+
│ Lives in MCP server <1ms (cache hit) │
|
|
211
|
+
│ Backed by SessionStore Auto-refresh on miss │
|
|
212
|
+
│ TTL: 5 minutes Max 50 entries/session │
|
|
248
213
|
├─────────────────────────────────────────────────────┤
|
|
249
214
|
│ L2: SessionStore SQLite │
|
|
250
215
|
│ Entities + notes <10ms access │
|
|
251
|
-
│
|
|
252
|
-
│
|
|
216
|
+
│ files, errors, decisions Per-session scope │
|
|
217
|
+
│ observations (14 patterns) Importance scored 1-5 │
|
|
253
218
|
├─────────────────────────────────────────────────────┤
|
|
254
219
|
│ L3: LongTermStore SQLite + FTS5 + TF-IDF │
|
|
255
220
|
│ Cross-session summaries <100ms access │
|
|
256
221
|
│ Hybrid ranked search Persistent forever │
|
|
257
|
-
│
|
|
222
|
+
│ Semantic embeddings 3-layer progressive │
|
|
258
223
|
└─────────────────────────────────────────────────────┘
|
|
259
224
|
```
|
|
260
225
|
|
|
@@ -270,7 +235,7 @@ bunx claude-memory-hub install
|
|
|
270
235
|
|
|
271
236
|
One command. Registers MCP server + 5 hooks globally. Works on CLI, VS Code, JetBrains.
|
|
272
237
|
|
|
273
|
-
**Coming from claude-mem?** The installer auto-detects `~/.claude-mem/claude-mem.db` and migrates your data automatically.
|
|
238
|
+
**Coming from claude-mem?** The installer auto-detects `~/.claude-mem/claude-mem.db` and migrates your data automatically.
|
|
274
239
|
|
|
275
240
|
### Update
|
|
276
241
|
|
|
@@ -278,24 +243,8 @@ One command. Registers MCP server + 5 hooks globally. Works on CLI, VS Code, Jet
|
|
|
278
243
|
bunx claude-memory-hub@latest install
|
|
279
244
|
```
|
|
280
245
|
|
|
281
|
-
Or if installed globally:
|
|
282
|
-
|
|
283
|
-
```bash
|
|
284
|
-
bun install -g claude-memory-hub@latest
|
|
285
|
-
claude-memory-hub install
|
|
286
|
-
```
|
|
287
|
-
|
|
288
246
|
Your data at `~/.claude-memory-hub/` is preserved across updates. Schema migrations run automatically.
|
|
289
247
|
|
|
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
248
|
### All CLI commands
|
|
300
249
|
|
|
301
250
|
```bash
|
|
@@ -305,10 +254,10 @@ bunx claude-memory-hub status # Check installation
|
|
|
305
254
|
bunx claude-memory-hub migrate # Import data from claude-mem
|
|
306
255
|
bunx claude-memory-hub viewer # Open browser UI at localhost:37888
|
|
307
256
|
bunx claude-memory-hub health # Run health diagnostics
|
|
308
|
-
bunx claude-memory-hub reindex # Rebuild TF-IDF
|
|
257
|
+
bunx claude-memory-hub reindex # Rebuild TF-IDF + embedding indexes
|
|
309
258
|
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
|
|
259
|
+
bunx claude-memory-hub import # Import JSONL from stdin (--dry-run)
|
|
260
|
+
bunx claude-memory-hub cleanup # Remove old data (--days N, default 90)
|
|
312
261
|
```
|
|
313
262
|
|
|
314
263
|
### Requirements
|
|
@@ -327,13 +276,13 @@ Claude can call these tools directly during conversation:
|
|
|
327
276
|
|
|
328
277
|
| Tool | What it does | When to use |
|
|
329
278
|
|------|-------------|-------------|
|
|
330
|
-
| `memory_recall` | FTS5 search past
|
|
279
|
+
| `memory_recall` | FTS5 search past sessions (supports `max_tokens`) | Starting a task, looking for prior work |
|
|
331
280
|
| `memory_entities` | Find all sessions that touched a file | Before editing a file, understanding history |
|
|
332
|
-
| `memory_session_notes` | Current session activity
|
|
281
|
+
| `memory_session_notes` | Current session activity (L1 cache) | Mid-session, checking what's been done |
|
|
333
282
|
| `memory_store` | Manually save a note or decision | Preserving important context |
|
|
334
|
-
| `memory_context_budget` | Analyze token costs +
|
|
283
|
+
| `memory_context_budget` | Analyze token costs + overhead report | Understanding resource usage |
|
|
335
284
|
|
|
336
|
-
### 3-Layer Search
|
|
285
|
+
### 3-Layer Search
|
|
337
286
|
|
|
338
287
|
| Tool | Layer | Tokens/result | When to use |
|
|
339
288
|
|------|-------|---------------|-------------|
|
|
@@ -345,7 +294,46 @@ Claude can call these tools directly during conversation:
|
|
|
345
294
|
|
|
346
295
|
| Tool | What it does |
|
|
347
296
|
|------|-------------|
|
|
348
|
-
| `memory_health` | Check database, FTS5, disk, integrity status |
|
|
297
|
+
| `memory_health` | Check database, FTS5, disk, embeddings, integrity status |
|
|
298
|
+
|
|
299
|
+
---
|
|
300
|
+
|
|
301
|
+
## Data Export/Import
|
|
302
|
+
|
|
303
|
+
### Export
|
|
304
|
+
|
|
305
|
+
```bash
|
|
306
|
+
# Full export
|
|
307
|
+
bunx claude-memory-hub export > backup.jsonl
|
|
308
|
+
|
|
309
|
+
# Incremental (since timestamp)
|
|
310
|
+
bunx claude-memory-hub export --since 1743580800000 > incremental.jsonl
|
|
311
|
+
|
|
312
|
+
# Single table
|
|
313
|
+
bunx claude-memory-hub export --table sessions > sessions.jsonl
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
### Import
|
|
317
|
+
|
|
318
|
+
```bash
|
|
319
|
+
# Import from file
|
|
320
|
+
bunx claude-memory-hub import < backup.jsonl
|
|
321
|
+
|
|
322
|
+
# Validate without writing
|
|
323
|
+
bunx claude-memory-hub import --dry-run < backup.jsonl
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
### Cleanup
|
|
327
|
+
|
|
328
|
+
```bash
|
|
329
|
+
# Remove data older than 90 days (default)
|
|
330
|
+
bunx claude-memory-hub cleanup
|
|
331
|
+
|
|
332
|
+
# Custom retention
|
|
333
|
+
bunx claude-memory-hub cleanup --days 30
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
Format: JSONL (one JSON object per line). Embedding BLOBs encoded as base64. Import uses UPSERT — safe to re-run.
|
|
349
337
|
|
|
350
338
|
---
|
|
351
339
|
|
|
@@ -366,20 +354,14 @@ Opens a dark-themed dashboard at `http://localhost:37888` with:
|
|
|
366
354
|
|
|
367
355
|
## Migrating from claude-mem
|
|
368
356
|
|
|
369
|
-
If you're already using [claude-mem](https://github.com/nicobailey-llc/claude-mem), migration is seamless:
|
|
370
|
-
|
|
371
357
|
```bash
|
|
372
358
|
# Automatic (during install)
|
|
373
359
|
bunx claude-memory-hub install
|
|
374
|
-
# → Detects ~/.claude-mem/claude-mem.db automatically
|
|
375
|
-
# → Migrates sessions, observations, summaries
|
|
376
360
|
|
|
377
361
|
# Manual
|
|
378
362
|
bunx claude-memory-hub migrate
|
|
379
363
|
```
|
|
380
364
|
|
|
381
|
-
### What gets migrated
|
|
382
|
-
|
|
383
365
|
| claude-mem | → | memory-hub |
|
|
384
366
|
|------------|---|------------|
|
|
385
367
|
| `sdk_sessions` | → | `sessions` |
|
|
@@ -397,13 +379,14 @@ Migration is idempotent — safe to run multiple times with zero duplicates.
|
|
|
397
379
|
| Version | What it solved |
|
|
398
380
|
|---------|---------------|
|
|
399
381
|
| **v0.1.0** | Cross-session memory, entity tracking, FTS5 search |
|
|
400
|
-
| **v0.2.0** | Compact interceptor (PreCompact/PostCompact
|
|
382
|
+
| **v0.2.0** | Compact interceptor (PreCompact/PostCompact), context enrichment, importance scoring |
|
|
401
383
|
| **v0.3.0** | Removed API key requirement, 1-command install |
|
|
402
|
-
| **v0.4.0** |
|
|
384
|
+
| **v0.4.0** | Resource usage tracking, token overhead analysis |
|
|
403
385
|
| **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
|
|
386
|
+
| **v0.6.0** | ResourceRegistry (170 resources), semantic search (384-dim embeddings), observation capture, CLAUDE.md tracking, 3-tier LLM summarization |
|
|
405
387
|
| **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
|
|
388
|
+
| **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 |
|
|
389
|
+
| **v0.8.1** | Token-budget-aware MCP tools (`max_tokens`), proactive mid-session memory retrieval (topic-shift detection), session-end batch flush |
|
|
407
390
|
|
|
408
391
|
See [CHANGELOG.md](CHANGELOG.md) for full details.
|
|
409
392
|
|
|
@@ -437,7 +420,11 @@ All data stored locally at `~/.claude-memory-hub/`.
|
|
|
437
420
|
|
|
438
421
|
```
|
|
439
422
|
~/.claude-memory-hub/
|
|
440
|
-
├── memory.db
|
|
423
|
+
├── memory.db # SQLite database (all memory data)
|
|
424
|
+
├── batch/
|
|
425
|
+
│ └── queue.jsonl # PostToolUse batch queue (auto-flushed)
|
|
426
|
+
├── proactive/
|
|
427
|
+
│ └── <session>.json # Topic tracking state (auto-cleaned)
|
|
441
428
|
└── logs/
|
|
442
429
|
└── memory-hub.log # Structured JSON logs (auto-rotated at 5MB)
|
|
443
430
|
```
|
|
@@ -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 = 1500;
|
|
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
|
-
|
|
1875
|
-
|
|
2000
|
+
} catch {
|
|
2001
|
+
await handlePostToolUse(hook, project);
|
|
2002
|
+
}
|
|
2003
|
+
} else {
|
|
2004
|
+
await handlePostToolUse(hook, project);
|
|
1876
2005
|
}
|
|
1877
|
-
|
|
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));
|
|
@@ -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 = 1500;
|
|
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(() => {});
|
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
|
-
|
|
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