claude-memory-hub 0.9.5 → 0.10.0
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 +76 -0
- package/README.md +37 -14
- package/dist/cli.js +43 -0
- package/dist/hooks/post-compact.js +195 -31
- package/dist/hooks/post-tool-use.js +139 -3
- package/dist/hooks/pre-compact.js +195 -31
- package/dist/hooks/session-end.js +328 -61
- package/dist/hooks/user-prompt-submit.js +139 -3
- package/dist/index.js +157 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,82 @@ Format follows [Keep a Changelog](https://keepachangelog.com/).
|
|
|
5
5
|
|
|
6
6
|
---
|
|
7
7
|
|
|
8
|
+
## [0.10.0] - 2026-04-03
|
|
9
|
+
|
|
10
|
+
Full conversation capture — memory-hub now remembers what you said AND what Claude said.
|
|
11
|
+
|
|
12
|
+
### New Feature: Conversation Capture
|
|
13
|
+
|
|
14
|
+
- **All user prompts saved** — every `UserPromptSubmit` hook now inserts the user's message into a new `messages` table (up to 2000 chars each). Previously only the first prompt was stored in `sessions.user_prompt` (500 chars)
|
|
15
|
+
- **Transcript parsing at session-end** — when session ends, the Stop hook reads Claude Code's JSONL transcript file (`transcript_path`) and extracts all user + assistant text messages. Tool blocks (tool_use/tool_result) are skipped since they're already captured as entities. Streaming parser handles files up to 10MB safely
|
|
16
|
+
- **`messages` table + FTS5 search** — new schema v5 migration adds `messages` table with full-text search index. Supports deduplication by UUID, conversation chain tracking via `parent_uuid`, and role-based filtering
|
|
17
|
+
- **`memory_conversation` MCP tool** — new tool to retrieve or search conversation history for any session. Supports: get all messages, filter by role, full-text search across all conversations
|
|
18
|
+
|
|
19
|
+
### Enriched Summaries
|
|
20
|
+
|
|
21
|
+
- **Conversation digest in summaries** — session summarizer (both Tier 2 CLI and Tier 3 rule-based) now includes a digest of user requests from the `messages` table. Summaries now show "User requests (3): [1] fix login bug; [2] add dark mode; [3] deploy to prod" instead of just the first prompt
|
|
22
|
+
- **Search across conversations** — `searchMessages()` provides FTS5 search across all stored messages with LIKE fallback
|
|
23
|
+
|
|
24
|
+
### Data Flow
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
Session Start:
|
|
28
|
+
UserPromptSubmit → save user prompt to messages table (real-time)
|
|
29
|
+
|
|
30
|
+
Mid-Session:
|
|
31
|
+
UserPromptSubmit → save each subsequent prompt (real-time)
|
|
32
|
+
PostToolUse → capture entities as before
|
|
33
|
+
|
|
34
|
+
Session End (Stop hook):
|
|
35
|
+
1. Parse transcript_path JSONL → extract user + assistant messages
|
|
36
|
+
2. Bulk insert to messages table (dedup by UUID)
|
|
37
|
+
3. Summarize with conversation digest
|
|
38
|
+
4. Generate embeddings
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### Database
|
|
42
|
+
|
|
43
|
+
- **Schema v5** — new `messages` table, `fts_messages` FTS5 virtual table, auto-sync triggers
|
|
44
|
+
- **Migration**: automatic on first use after upgrade
|
|
45
|
+
|
|
46
|
+
### Before vs After
|
|
47
|
+
|
|
48
|
+
```
|
|
49
|
+
Before (0.9.x):
|
|
50
|
+
Stored: first user prompt (500 chars) + file/error/decision entities
|
|
51
|
+
Missing: subsequent prompts, ALL assistant responses
|
|
52
|
+
Summary: "Task: fix login. Files: auth.ts."
|
|
53
|
+
|
|
54
|
+
After (0.10.0):
|
|
55
|
+
Stored: ALL user prompts + ALL assistant responses + entities
|
|
56
|
+
Summary: "User requests (3): fix login bug; add dark mode; deploy.
|
|
57
|
+
Files: auth.ts, theme.ts. Decisions: JWT refresh, CSS vars."
|
|
58
|
+
Searchable: "authentication login" → finds matching conversations
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## [0.9.6] - 2026-04-03
|
|
64
|
+
|
|
65
|
+
Richer session capture — Agent results, higher limits, cleaner summaries.
|
|
66
|
+
|
|
67
|
+
### Enhancements
|
|
68
|
+
|
|
69
|
+
- **Agent/Skill result capture** — `tool_response` from Agent and Skill tools is now saved into entity `context` field (up to 800 chars). Previously only the prompt was captured, losing all agent output. This is the biggest data quality improvement — multi-agent workflows now produce meaningful summaries
|
|
70
|
+
- **Higher summary limits** — `user_prompt` 200→500 chars, `decisions` 3→5 entries with context, `errors` 2→5 entries, `notes` 2→5 entries. CLI summarizer bumped to 6K prompt / 2K output (was 4K/1K). Decision entities now include agent/skill results in summary text
|
|
71
|
+
- **IDE/system tag stripping in summarizer** — `<ide_opened_file>`, `<ide_selection>`, `<system-reminder>`, `<local-command-*>`, `<command-*>` tags are now stripped at both rule-based and CLI summarizer stages. Prevents tag noise from polluting L3 summaries
|
|
72
|
+
- **PostCompact summary cap** — compact summaries exceeding 5,000 chars are truncated (was unbounded — seen 22K in production). Reduces DB bloat and improves search relevance
|
|
73
|
+
- **Broader observation heuristics** — added patterns for: refactoring, dependency changes, test results, deployments, scaffolding, data risks, user task/feature requests. Captures more meaningful observations from tool output and user prompts
|
|
74
|
+
|
|
75
|
+
### Before vs After
|
|
76
|
+
|
|
77
|
+
```
|
|
78
|
+
Before: "Task: <ide_opened_file>...</ide_opened_file>. Files (49): ..." (1165 chars, noisy)
|
|
79
|
+
After: "Task: fix broken hooks in memory-hub. Files (15): ... Decisions: agent:debugger: investigated... → Found temp path issue..." (richer, clean)
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
8
84
|
## [0.9.5] - 2026-04-03
|
|
9
85
|
|
|
10
86
|
Stable install path — hooks no longer break after reboot or bunx cache cleanup.
|
package/README.md
CHANGED
|
@@ -26,10 +26,11 @@ Zero API key. Zero Python. Zero config. One install command.
|
|
|
26
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
27
|
|
|
28
28
|
But it doesn't stop there:
|
|
29
|
+
- **Full conversation capture** — every user prompt + assistant response saved via transcript parsing
|
|
29
30
|
- **Cross-session memory** — past work auto-injected when you start a new session
|
|
30
31
|
- **3-engine hybrid search** — FTS5 + TF-IDF + semantic embeddings (384-dim, offline)
|
|
31
32
|
- **Proactive retrieval** — detects topic shifts mid-session, injects relevant context automatically
|
|
32
|
-
- **
|
|
33
|
+
- **100+ unit tests**, batch queue (75ms→3ms), JSONL export/import, browser UI
|
|
33
34
|
- **Multi-agent ready** — subagents share memory for free via MCP
|
|
34
35
|
|
|
35
36
|
Built for developers who use Claude Code daily and are tired of repeating themselves.
|
|
@@ -61,13 +62,15 @@ Search: Keyword-only, no semantic ranking
|
|
|
61
62
|
| Problem | Claude Code built-in | claude-mem | memory-hub |
|
|
62
63
|
|---------|:-------------------:|:----------:|:----------:|
|
|
63
64
|
| Cross-session memory | -- | Yes | **Yes** |
|
|
65
|
+
| Full conversation capture (user+assistant) | -- | -- | **Yes** |
|
|
66
|
+
| Conversation search (FTS5) | -- | -- | **Yes** |
|
|
64
67
|
| Influence what compact preserves | -- | -- | **Yes** |
|
|
65
68
|
| Save compact output to L3 | -- | -- | **Yes** |
|
|
66
69
|
| Hybrid search (FTS5 + TF-IDF + semantic) | -- | Partial | **Yes** |
|
|
67
70
|
| 3-layer progressive search | -- | Yes | **Yes** |
|
|
68
71
|
| Resource overhead analysis | -- | -- | **Yes** |
|
|
69
72
|
| CLAUDE.md rule tracking | -- | -- | **Yes** |
|
|
70
|
-
| Observation capture (
|
|
73
|
+
| Observation capture (20+ patterns) | -- | Yes | **Yes** |
|
|
71
74
|
| LLM summarization (3-tier) | -- | Yes (API) | **Yes (free)** |
|
|
72
75
|
| Token-budget-aware tools (`max_tokens`) | -- | -- | **Yes** |
|
|
73
76
|
| Proactive mid-session retrieval | -- | -- | **Yes** |
|
|
@@ -79,25 +82,29 @@ Search: Keyword-only, no semantic ranking
|
|
|
79
82
|
| Hook batching (3ms vs 75ms) | -- | -- | **Yes** |
|
|
80
83
|
| Browser UI | -- | Yes | **Yes** |
|
|
81
84
|
| Health monitoring + auto-cleanup | -- | -- | **Yes** |
|
|
82
|
-
| Unit tests (
|
|
85
|
+
| Unit tests (100+) | N/A | -- | **Yes** |
|
|
83
86
|
| No API key / Python / Chroma | N/A | Partial | **Yes** |
|
|
84
87
|
|
|
85
88
|
---
|
|
86
89
|
|
|
87
90
|
## How It Works
|
|
88
91
|
|
|
89
|
-
### Layer 1 — Entity Capture (every tool call)
|
|
92
|
+
### Layer 1 — Entity + Conversation Capture (every tool call + every prompt)
|
|
90
93
|
|
|
91
94
|
```
|
|
92
95
|
Claude reads a file → memory-hub records: which file, code patterns found
|
|
93
96
|
Claude edits a file → memory-hub records: what changed (old → new diff)
|
|
94
97
|
Claude runs a command → memory-hub records: command, exit code, stderr
|
|
95
98
|
Claude makes a decision → memory-hub records: decision text + importance score
|
|
99
|
+
Claude spawns an agent → memory-hub records: agent type, prompt, result summary
|
|
100
|
+
User sends a prompt → memory-hub records: full prompt text to messages table
|
|
101
|
+
Session ends → memory-hub parses transcript: ALL user + assistant messages
|
|
96
102
|
```
|
|
97
103
|
|
|
98
104
|
No XML. No special format. Extracted directly from hook JSON metadata.
|
|
99
105
|
PostToolUse events are batched via write-through queue (~3ms per event vs ~75ms direct).
|
|
100
106
|
Mid-session topic shifts auto-inject relevant past context (proactive retrieval).
|
|
107
|
+
Full conversation (user + assistant) captured from Claude Code's JSONL transcript at session end.
|
|
101
108
|
|
|
102
109
|
### Layer 2 — Compact Interceptor (the key innovation)
|
|
103
110
|
|
|
@@ -127,7 +134,9 @@ Mid-session topic shifts auto-inject relevant past context (proactive retrieval)
|
|
|
127
134
|
### Layer 3 — Cross-Session Memory
|
|
128
135
|
|
|
129
136
|
```
|
|
130
|
-
Session N ends →
|
|
137
|
+
Session N ends → Parse transcript: capture full conversation (user + assistant)
|
|
138
|
+
→ 3-tier summarization: PostCompact > CLI claude > rule-based
|
|
139
|
+
→ Summary enriched with conversation digest
|
|
131
140
|
→ Summary saved to SQLite L3 with FTS5 indexing
|
|
132
141
|
|
|
133
142
|
Session N+1 → UserPromptSubmit hook fires
|
|
@@ -173,9 +182,12 @@ Tool output contains "IMPORTANT: always pool DB connections"
|
|
|
173
182
|
User prompt contains "remember that we use TypeScript strict"
|
|
174
183
|
→ observation entity (importance=3) saved to L2
|
|
175
184
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
185
|
+
20+ heuristic patterns:
|
|
186
|
+
Tool output: IMPORTANT, CRITICAL, SECURITY, DEPRECATED, migration failed,
|
|
187
|
+
decision:, discovered, root cause, switched to, refactored, installed,
|
|
188
|
+
TODO:, FIXME:, performance:, bottleneck, tests pass/fail, deployed, etc.
|
|
189
|
+
User prompt: IMPORTANT, MUST, remember that, don't/never/avoid,
|
|
190
|
+
fix/debug/investigate, implement/build/create, prefer/always use, etc.
|
|
179
191
|
```
|
|
180
192
|
|
|
181
193
|
---
|
|
@@ -193,10 +205,11 @@ User prompt contains "remember that we use TypeScript strict"
|
|
|
193
205
|
│ └──────┬────────┘ │ priorities │ └──────┬───────┘ │
|
|
194
206
|
│ │ └──────┬───────┘ │ │
|
|
195
207
|
│ ┌──────┴───────┐ │ ┌──────┴───────┐ │
|
|
196
|
-
│ │UserPrompt │ │ │ Stop
|
|
197
|
-
│ │Submit: inject│ │ │
|
|
198
|
-
│ │past context
|
|
199
|
-
│
|
|
208
|
+
│ │UserPrompt │ │ │ Stop │ │
|
|
209
|
+
│ │Submit: inject│ │ │ parse transcript│ │
|
|
210
|
+
│ │past context +│ │ │ capture convo │ │
|
|
211
|
+
│ │save prompt │ │ │ summarize │ │
|
|
212
|
+
│ └──────────────┘ │ └────────────────┘ │
|
|
200
213
|
│ │ │
|
|
201
214
|
│ MCP Server (stdio, long-lived) │
|
|
202
215
|
│ ┌─────────────────────────────────────────────────────┐ │
|
|
@@ -204,7 +217,7 @@ User prompt contains "remember that we use TypeScript strict"
|
|
|
204
217
|
│ │ memory_entities memory_timeline (L2 context) │ │
|
|
205
218
|
│ │ memory_session_notes memory_fetch (L3 full) │ │
|
|
206
219
|
│ │ memory_store memory_context_budget │ │
|
|
207
|
-
│ │ memory_health
|
|
220
|
+
│ │ memory_conversation memory_health │ │
|
|
208
221
|
│ │ │ │
|
|
209
222
|
│ │ L1 WorkingMemory: read-through cache over L2 │ │
|
|
210
223
|
│ └─────────────────────────────────────────────────────┘ │
|
|
@@ -241,7 +254,8 @@ User prompt contains "remember that we use TypeScript strict"
|
|
|
241
254
|
│ L2: SessionStore SQLite │
|
|
242
255
|
│ Entities + notes <10ms access │
|
|
243
256
|
│ files, errors, decisions Per-session scope │
|
|
244
|
-
│
|
|
257
|
+
│ messages (user+assistant) Importance scored 1-5 │
|
|
258
|
+
│ observations (20+ patterns)FTS5 on conversations │
|
|
245
259
|
├─────────────────────────────────────────────────────┤
|
|
246
260
|
│ L3: LongTermStore SQLite + FTS5 + TF-IDF │
|
|
247
261
|
│ Cross-session summaries <100ms access │
|
|
@@ -318,6 +332,12 @@ Claude can call these tools directly during conversation:
|
|
|
318
332
|
| `memory_timeline` | 2 (context) | ~200 | Then: see what happened before/after a result |
|
|
319
333
|
| `memory_fetch` | 3 (full) | ~500 | Finally: get complete records for specific IDs |
|
|
320
334
|
|
|
335
|
+
### Conversation
|
|
336
|
+
|
|
337
|
+
| Tool | What it does | When to use |
|
|
338
|
+
|------|-------------|-------------|
|
|
339
|
+
| `memory_conversation` | Retrieve or search conversation messages | Reviewing what was discussed in a past session |
|
|
340
|
+
|
|
321
341
|
### Diagnostics
|
|
322
342
|
|
|
323
343
|
| Tool | What it does |
|
|
@@ -416,6 +436,9 @@ Migration is idempotent — safe to run multiple times with zero duplicates.
|
|
|
416
436
|
| **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 |
|
|
417
437
|
| **v0.8.1** | Token-budget-aware MCP tools (`max_tokens`), proactive mid-session memory retrieval (topic-shift detection), session-end batch flush |
|
|
418
438
|
| **v0.9.0** | Smart budget allocation (priority-based, memory never pushed out), CLAUDE.md adaptive compression (3 levels), overhead warning auto-injection, doubled injection limits |
|
|
439
|
+
| **v0.9.5** | Stable install path — hooks no longer break after reboot or bunx cache cleanup |
|
|
440
|
+
| **v0.9.6** | Agent/Skill result capture, higher summary limits, IDE tag stripping, PostCompact cap, broader observation patterns (20+) |
|
|
441
|
+
| **v0.10.0** | **Full conversation capture** — all user prompts + assistant responses via transcript parsing, `messages` table with FTS5, `memory_conversation` MCP tool, conversation-enriched summaries |
|
|
419
442
|
|
|
420
443
|
See [CHANGELOG.md](CHANGELOG.md) for full details.
|
|
421
444
|
|
package/dist/cli.js
CHANGED
|
@@ -240,6 +240,49 @@ function applyMigrations(db) {
|
|
|
240
240
|
db.run("INSERT OR IGNORE INTO schema_versions(version, applied_at) VALUES (4, ?)", [Date.now()]);
|
|
241
241
|
log.info("Migration v4 complete");
|
|
242
242
|
}
|
|
243
|
+
if (currentVersion < 5) {
|
|
244
|
+
log.info("Applying migration v5: messages table for conversation capture");
|
|
245
|
+
db.run(`
|
|
246
|
+
CREATE TABLE IF NOT EXISTS messages (
|
|
247
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
248
|
+
session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
|
|
249
|
+
project TEXT NOT NULL,
|
|
250
|
+
role TEXT NOT NULL CHECK(role IN ('user','assistant')),
|
|
251
|
+
content TEXT NOT NULL,
|
|
252
|
+
prompt_number INTEGER NOT NULL DEFAULT 0,
|
|
253
|
+
timestamp INTEGER NOT NULL,
|
|
254
|
+
uuid TEXT,
|
|
255
|
+
parent_uuid TEXT
|
|
256
|
+
)
|
|
257
|
+
`);
|
|
258
|
+
db.run(`CREATE INDEX IF NOT EXISTS idx_messages_session ON messages(session_id, prompt_number)`);
|
|
259
|
+
db.run(`CREATE INDEX IF NOT EXISTS idx_messages_role ON messages(session_id, role)`);
|
|
260
|
+
db.run(`CREATE UNIQUE INDEX IF NOT EXISTS idx_messages_uuid ON messages(uuid) WHERE uuid IS NOT NULL`);
|
|
261
|
+
db.run(`
|
|
262
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS fts_messages USING fts5(
|
|
263
|
+
session_id UNINDEXED,
|
|
264
|
+
role,
|
|
265
|
+
content,
|
|
266
|
+
tokenize = 'porter unicode61'
|
|
267
|
+
)
|
|
268
|
+
`);
|
|
269
|
+
db.run(`
|
|
270
|
+
CREATE TRIGGER IF NOT EXISTS fts_messages_insert
|
|
271
|
+
AFTER INSERT ON messages BEGIN
|
|
272
|
+
INSERT INTO fts_messages(rowid, session_id, role, content)
|
|
273
|
+
VALUES (new.id, new.session_id, new.role, new.content);
|
|
274
|
+
END
|
|
275
|
+
`);
|
|
276
|
+
db.run(`
|
|
277
|
+
CREATE TRIGGER IF NOT EXISTS fts_messages_delete
|
|
278
|
+
AFTER DELETE ON messages BEGIN
|
|
279
|
+
INSERT INTO fts_messages(fts_messages, rowid, session_id, role, content)
|
|
280
|
+
VALUES ('delete', old.id, old.session_id, old.role, old.content);
|
|
281
|
+
END
|
|
282
|
+
`);
|
|
283
|
+
db.run("INSERT OR IGNORE INTO schema_versions(version, applied_at) VALUES (5, ?)", [Date.now()]);
|
|
284
|
+
log.info("Migration v5 complete");
|
|
285
|
+
}
|
|
243
286
|
}
|
|
244
287
|
function getDatabase() {
|
|
245
288
|
if (!_db) {
|
|
@@ -337,6 +337,49 @@ function applyMigrations(db) {
|
|
|
337
337
|
db.run("INSERT OR IGNORE INTO schema_versions(version, applied_at) VALUES (4, ?)", [Date.now()]);
|
|
338
338
|
log.info("Migration v4 complete");
|
|
339
339
|
}
|
|
340
|
+
if (currentVersion < 5) {
|
|
341
|
+
log.info("Applying migration v5: messages table for conversation capture");
|
|
342
|
+
db.run(`
|
|
343
|
+
CREATE TABLE IF NOT EXISTS messages (
|
|
344
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
345
|
+
session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
|
|
346
|
+
project TEXT NOT NULL,
|
|
347
|
+
role TEXT NOT NULL CHECK(role IN ('user','assistant')),
|
|
348
|
+
content TEXT NOT NULL,
|
|
349
|
+
prompt_number INTEGER NOT NULL DEFAULT 0,
|
|
350
|
+
timestamp INTEGER NOT NULL,
|
|
351
|
+
uuid TEXT,
|
|
352
|
+
parent_uuid TEXT
|
|
353
|
+
)
|
|
354
|
+
`);
|
|
355
|
+
db.run(`CREATE INDEX IF NOT EXISTS idx_messages_session ON messages(session_id, prompt_number)`);
|
|
356
|
+
db.run(`CREATE INDEX IF NOT EXISTS idx_messages_role ON messages(session_id, role)`);
|
|
357
|
+
db.run(`CREATE UNIQUE INDEX IF NOT EXISTS idx_messages_uuid ON messages(uuid) WHERE uuid IS NOT NULL`);
|
|
358
|
+
db.run(`
|
|
359
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS fts_messages USING fts5(
|
|
360
|
+
session_id UNINDEXED,
|
|
361
|
+
role,
|
|
362
|
+
content,
|
|
363
|
+
tokenize = 'porter unicode61'
|
|
364
|
+
)
|
|
365
|
+
`);
|
|
366
|
+
db.run(`
|
|
367
|
+
CREATE TRIGGER IF NOT EXISTS fts_messages_insert
|
|
368
|
+
AFTER INSERT ON messages BEGIN
|
|
369
|
+
INSERT INTO fts_messages(rowid, session_id, role, content)
|
|
370
|
+
VALUES (new.id, new.session_id, new.role, new.content);
|
|
371
|
+
END
|
|
372
|
+
`);
|
|
373
|
+
db.run(`
|
|
374
|
+
CREATE TRIGGER IF NOT EXISTS fts_messages_delete
|
|
375
|
+
AFTER DELETE ON messages BEGIN
|
|
376
|
+
INSERT INTO fts_messages(fts_messages, rowid, session_id, role, content)
|
|
377
|
+
VALUES ('delete', old.id, old.session_id, old.role, old.content);
|
|
378
|
+
END
|
|
379
|
+
`);
|
|
380
|
+
db.run("INSERT OR IGNORE INTO schema_versions(version, applied_at) VALUES (5, ?)", [Date.now()]);
|
|
381
|
+
log.info("Migration v5 complete");
|
|
382
|
+
}
|
|
340
383
|
}
|
|
341
384
|
var _db = null;
|
|
342
385
|
function getDatabase() {
|
|
@@ -423,6 +466,68 @@ class SessionStore {
|
|
|
423
466
|
getSessionNotes(session_id) {
|
|
424
467
|
return this.db.query("SELECT * FROM session_notes WHERE session_id = ? ORDER BY created_at ASC").all(session_id);
|
|
425
468
|
}
|
|
469
|
+
insertMessage(msg) {
|
|
470
|
+
if (msg.uuid) {
|
|
471
|
+
const existing = this.db.query("SELECT COUNT(*) as c FROM messages WHERE uuid = ?").get(msg.uuid);
|
|
472
|
+
if (existing && existing.c > 0)
|
|
473
|
+
return -1;
|
|
474
|
+
}
|
|
475
|
+
const result = this.db.run(`INSERT INTO messages(session_id, project, role, content, prompt_number, timestamp, uuid, parent_uuid)
|
|
476
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
477
|
+
msg.session_id,
|
|
478
|
+
msg.project,
|
|
479
|
+
msg.role,
|
|
480
|
+
msg.content,
|
|
481
|
+
msg.prompt_number,
|
|
482
|
+
msg.timestamp,
|
|
483
|
+
msg.uuid ?? null,
|
|
484
|
+
msg.parent_uuid ?? null
|
|
485
|
+
]);
|
|
486
|
+
return Number(result.lastInsertRowid);
|
|
487
|
+
}
|
|
488
|
+
insertMessages(msgs) {
|
|
489
|
+
let count = 0;
|
|
490
|
+
const db = this.db;
|
|
491
|
+
db.transaction(() => {
|
|
492
|
+
for (const msg of msgs) {
|
|
493
|
+
const id = this.insertMessage(msg);
|
|
494
|
+
if (id !== -1)
|
|
495
|
+
count++;
|
|
496
|
+
}
|
|
497
|
+
})();
|
|
498
|
+
return count;
|
|
499
|
+
}
|
|
500
|
+
getSessionMessages(session_id, role) {
|
|
501
|
+
if (role) {
|
|
502
|
+
return this.db.query("SELECT * FROM messages WHERE session_id = ? AND role = ? ORDER BY prompt_number ASC, timestamp ASC").all(session_id, role);
|
|
503
|
+
}
|
|
504
|
+
return this.db.query("SELECT * FROM messages WHERE session_id = ? ORDER BY prompt_number ASC, timestamp ASC").all(session_id);
|
|
505
|
+
}
|
|
506
|
+
getMessageCount(session_id, role) {
|
|
507
|
+
if (role) {
|
|
508
|
+
const row2 = this.db.query("SELECT COUNT(*) as c FROM messages WHERE session_id = ? AND role = ?").get(session_id, role);
|
|
509
|
+
return row2?.c ?? 0;
|
|
510
|
+
}
|
|
511
|
+
const row = this.db.query("SELECT COUNT(*) as c FROM messages WHERE session_id = ?").get(session_id);
|
|
512
|
+
return row?.c ?? 0;
|
|
513
|
+
}
|
|
514
|
+
searchMessages(query, limit = 10) {
|
|
515
|
+
if (!query.trim())
|
|
516
|
+
return [];
|
|
517
|
+
const words = query.trim().split(/\s+/).filter((w) => w.length > 1).map((w) => `"${w.replace(/["*^()]/g, "")}"`);
|
|
518
|
+
if (words.length === 0)
|
|
519
|
+
return [];
|
|
520
|
+
const ftsQuery = words.join(" ");
|
|
521
|
+
try {
|
|
522
|
+
return this.db.query(`SELECT m.*, rank FROM fts_messages
|
|
523
|
+
JOIN messages m ON m.id = fts_messages.rowid
|
|
524
|
+
WHERE fts_messages MATCH ?
|
|
525
|
+
ORDER BY rank LIMIT ?`).all(ftsQuery, limit);
|
|
526
|
+
} catch {
|
|
527
|
+
const pattern = `%${query.replace(/[%_]/g, "\\$&")}%`;
|
|
528
|
+
return this.db.query("SELECT * FROM messages WHERE content LIKE ? ORDER BY timestamp DESC LIMIT ?").all(pattern, limit);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
426
531
|
}
|
|
427
532
|
|
|
428
533
|
// src/db/long-term-store.ts
|
|
@@ -500,37 +605,46 @@ function sanitizeFtsQuery(query) {
|
|
|
500
605
|
}
|
|
501
606
|
|
|
502
607
|
// src/summarizer/summarizer-prompts.ts
|
|
608
|
+
function stripNoiseTags(text) {
|
|
609
|
+
return text.replace(/<ide_opened_file>[\s\S]*?<\/ide_opened_file>\s*/g, "").replace(/<ide_selection>[\s\S]*?<\/ide_selection>\s*/g, "").replace(/<system-reminder>[\s\S]*?<\/system-reminder>\s*/g, "").replace(/<local-command-caveat>[\s\S]*?<\/local-command-caveat>\s*/g, "").replace(/<command-name>[\s\S]*?<\/command-name>\s*/g, "").replace(/<command-message>[\s\S]*?<\/command-message>\s*/g, "").replace(/<command-args>[\s\S]*?<\/command-args>\s*/g, "").replace(/<local-command-stdout>[\s\S]*?<\/local-command-stdout>\s*/g, "").trim();
|
|
610
|
+
}
|
|
503
611
|
function buildRuleBasedSummary(session, files, errors, decisions, notes = []) {
|
|
504
612
|
const parts = [];
|
|
505
613
|
if (session.user_prompt) {
|
|
506
|
-
|
|
614
|
+
const cleanPrompt = stripNoiseTags(session.user_prompt);
|
|
615
|
+
if (cleanPrompt) {
|
|
616
|
+
parts.push(`Task: ${cleanPrompt.slice(0, 500)}.`);
|
|
617
|
+
}
|
|
507
618
|
}
|
|
508
619
|
if (files.length > 0) {
|
|
509
|
-
const listed = files.slice(0,
|
|
510
|
-
parts.push(`Files (${files.length}): ${listed}${files.length >
|
|
620
|
+
const listed = files.slice(0, 15).join(", ");
|
|
621
|
+
parts.push(`Files (${files.length}): ${listed}${files.length > 15 ? ` (+${files.length - 15} more)` : ""}.`);
|
|
511
622
|
}
|
|
512
623
|
if (decisions.length > 0) {
|
|
513
|
-
const listed = decisions.slice(0,
|
|
624
|
+
const listed = decisions.slice(0, 5).map((d) => {
|
|
625
|
+
const base = d.entity_value.slice(0, 150);
|
|
626
|
+
const ctx = d.context ? ` \u2192 ${d.context.slice(0, 200)}` : "";
|
|
627
|
+
return base + ctx;
|
|
628
|
+
}).join("; ");
|
|
514
629
|
parts.push(`Decisions: ${listed}.`);
|
|
515
630
|
}
|
|
516
631
|
if (errors.length > 0) {
|
|
517
|
-
const
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
}
|
|
632
|
+
const errorLines = errors.slice(0, 5).map((e) => {
|
|
633
|
+
const ctx = e.context ? ` (${e.context.slice(0, 100)})` : "";
|
|
634
|
+
return `${e.entity_value.slice(0, 150)}${ctx}`;
|
|
635
|
+
});
|
|
636
|
+
parts.push(`Errors (${errors.length}): ${errorLines.join("; ")}.`);
|
|
523
637
|
}
|
|
524
638
|
if (notes.length > 0) {
|
|
525
|
-
parts.push(`Notes: ${notes.slice(-
|
|
639
|
+
parts.push(`Notes: ${notes.slice(-5).join("; ").slice(0, 500)}.`);
|
|
526
640
|
}
|
|
527
641
|
return parts.join(" ") || `Session in project ${session.project}.`;
|
|
528
642
|
}
|
|
529
643
|
|
|
530
644
|
// src/summarizer/cli-summarizer.ts
|
|
531
645
|
var log2 = createLogger("cli-summarizer");
|
|
532
|
-
var MAX_PROMPT_CHARS =
|
|
533
|
-
var MAX_OUTPUT_CHARS =
|
|
646
|
+
var MAX_PROMPT_CHARS = 6000;
|
|
647
|
+
var MAX_OUTPUT_CHARS = 2000;
|
|
534
648
|
var DEFAULT_TIMEOUT_MS = 30000;
|
|
535
649
|
var _cliAvailable;
|
|
536
650
|
var _cliCheckedAt = 0;
|
|
@@ -550,27 +664,30 @@ function isClaudeCliAvailable() {
|
|
|
550
664
|
_cliCheckedAt = Date.now();
|
|
551
665
|
return _cliAvailable;
|
|
552
666
|
}
|
|
667
|
+
function stripNoise(text) {
|
|
668
|
+
return text.replace(/<ide_opened_file>[\s\S]*?<\/ide_opened_file>\s*/g, "").replace(/<ide_selection>[\s\S]*?<\/ide_selection>\s*/g, "").replace(/<system-reminder>[\s\S]*?<\/system-reminder>\s*/g, "").replace(/<local-command-[\w-]+>[\s\S]*?<\/local-command-[\w-]+>\s*/g, "").replace(/<command-[\w-]+>[\s\S]*?<\/command-[\w-]+>\s*/g, "").trim();
|
|
669
|
+
}
|
|
553
670
|
function buildCliPrompt(ctx) {
|
|
554
671
|
const sections = [
|
|
555
|
-
"Summarize this coding session in
|
|
556
|
-
"Focus on: what was accomplished, key decisions, important findings.",
|
|
672
|
+
"Summarize this coding session in 3-5 plain sentences. No markdown, no headers, no code blocks.",
|
|
673
|
+
"Focus on: what was accomplished, key decisions, important findings, tools/agents used.",
|
|
557
674
|
"",
|
|
558
675
|
`Project: ${ctx.project}`
|
|
559
676
|
];
|
|
560
677
|
if (ctx.files.length > 0) {
|
|
561
|
-
sections.push(`Files modified: ${ctx.files.slice(0,
|
|
678
|
+
sections.push(`Files modified: ${ctx.files.slice(0, 15).join(", ")}`);
|
|
562
679
|
}
|
|
563
680
|
if (ctx.errors.length > 0) {
|
|
564
|
-
sections.push(`Errors resolved: ${ctx.errors.slice(0, 5).join("; ")}`);
|
|
681
|
+
sections.push(`Errors resolved: ${ctx.errors.slice(0, 5).map(stripNoise).join("; ")}`);
|
|
565
682
|
}
|
|
566
683
|
if (ctx.decisions.length > 0) {
|
|
567
|
-
sections.push(`Decisions: ${ctx.decisions.slice(0,
|
|
684
|
+
sections.push(`Decisions: ${ctx.decisions.slice(0, 8).map(stripNoise).join("; ")}`);
|
|
568
685
|
}
|
|
569
686
|
if (ctx.notes.length > 0) {
|
|
570
|
-
sections.push(`Notes: ${ctx.notes.slice(0,
|
|
687
|
+
sections.push(`Notes: ${ctx.notes.slice(0, 5).map(stripNoise).join("; ")}`);
|
|
571
688
|
}
|
|
572
689
|
if (ctx.observations.length > 0) {
|
|
573
|
-
sections.push(`Key observations: ${ctx.observations.slice(0,
|
|
690
|
+
sections.push(`Key observations: ${ctx.observations.slice(0, 5).map(stripNoise).join("; ")}`);
|
|
574
691
|
}
|
|
575
692
|
let prompt = sections.join(`
|
|
576
693
|
`);
|
|
@@ -666,24 +783,34 @@ class SessionSummarizer {
|
|
|
666
783
|
const decisions = this.sessionStore.getSessionDecisions(session_id);
|
|
667
784
|
const observations = this.sessionStore.getSessionObservations(session_id);
|
|
668
785
|
const notes = this.sessionStore.getSessionNotes(session_id).map((n) => n.content);
|
|
669
|
-
|
|
786
|
+
const messages = this.sessionStore.getSessionMessages(session_id);
|
|
787
|
+
if (files.length === 0 && errors.length === 0 && notes.length === 0 && messages.length === 0)
|
|
670
788
|
return;
|
|
671
789
|
const hasModified = this.sessionStore.hasModifiedFiles(session_id);
|
|
672
|
-
if (!hasModified && errors.length === 0 && decisions.length === 0 && notes.length === 0 && observations.length === 0)
|
|
790
|
+
if (!hasModified && errors.length === 0 && decisions.length === 0 && notes.length === 0 && observations.length === 0 && messages.length === 0)
|
|
673
791
|
return;
|
|
674
|
-
const
|
|
792
|
+
const userPrompts = messages.filter((m) => m.role === "user").slice(0, 10).map((m, i) => `[${i + 1}] ${m.content.slice(0, 150)}`);
|
|
793
|
+
const conversationDigest = userPrompts.length > 0 ? `User requests (${userPrompts.length}): ${userPrompts.join("; ")}` : "";
|
|
794
|
+
const obsValues = observations.slice(0, 8).map((o) => o.entity_value);
|
|
675
795
|
let summaryText;
|
|
676
796
|
let tier = "rule-based";
|
|
677
797
|
const llmMode = process.env["CLAUDE_MEMORY_HUB_LLM"] ?? "auto";
|
|
678
798
|
if (llmMode !== "rule-based") {
|
|
799
|
+
const decisionDetails = decisions.slice(0, 8).map((d) => {
|
|
800
|
+
const ctx2 = d.context ? ` \u2192 ${d.context.slice(0, 200)}` : "";
|
|
801
|
+
return d.entity_value.slice(0, 150) + ctx2;
|
|
802
|
+
});
|
|
803
|
+
const allNotes = [...notes.slice(0, 5)];
|
|
804
|
+
if (conversationDigest)
|
|
805
|
+
allNotes.push(conversationDigest);
|
|
679
806
|
const ctx = {
|
|
680
807
|
sessionId: session_id,
|
|
681
808
|
project,
|
|
682
809
|
files,
|
|
683
|
-
errors: errors.slice(0, 5).map((e) => e.entity_value.slice(0,
|
|
684
|
-
decisions:
|
|
685
|
-
notes:
|
|
686
|
-
observations: obsValues.slice(0,
|
|
810
|
+
errors: errors.slice(0, 5).map((e) => e.entity_value.slice(0, 150)),
|
|
811
|
+
decisions: decisionDetails,
|
|
812
|
+
notes: allNotes,
|
|
813
|
+
observations: obsValues.slice(0, 5)
|
|
687
814
|
};
|
|
688
815
|
summaryText = await tryCliSummary(ctx);
|
|
689
816
|
if (summaryText)
|
|
@@ -691,6 +818,8 @@ class SessionSummarizer {
|
|
|
691
818
|
}
|
|
692
819
|
if (!summaryText) {
|
|
693
820
|
const allNotes = [...notes, ...obsValues];
|
|
821
|
+
if (conversationDigest)
|
|
822
|
+
allNotes.push(conversationDigest);
|
|
694
823
|
summaryText = buildRuleBasedSummary(session, files, errors, decisions, allNotes);
|
|
695
824
|
}
|
|
696
825
|
log3.info("Summary generated", { session_id, tier, length: summaryText.length });
|
|
@@ -774,10 +903,14 @@ async function handlePostCompact(hook, project) {
|
|
|
774
903
|
const files = store.getSessionFiles(hook.session_id);
|
|
775
904
|
const decisions = store.getSessionDecisions(hook.session_id);
|
|
776
905
|
const errors = store.getSessionErrors(hook.session_id);
|
|
906
|
+
const MAX_COMPACT_SUMMARY = 5000;
|
|
907
|
+
const summary = hook.compact_summary.length > MAX_COMPACT_SUMMARY ? hook.compact_summary.slice(0, MAX_COMPACT_SUMMARY - 20) + `
|
|
908
|
+
|
|
909
|
+
[truncated]` : hook.compact_summary;
|
|
777
910
|
ltStore.upsertSummary({
|
|
778
911
|
session_id: hook.session_id,
|
|
779
912
|
project,
|
|
780
|
-
summary
|
|
913
|
+
summary,
|
|
781
914
|
files_touched: JSON.stringify(files.slice(0, 50)),
|
|
782
915
|
decisions: JSON.stringify(decisions.slice(0, 20).map((d) => d.entity_value)),
|
|
783
916
|
errors_fixed: JSON.stringify(errors.slice(0, 10).map((e) => e.entity_value.slice(0, 100))),
|
|
@@ -898,18 +1031,26 @@ function extractCodePatterns(content) {
|
|
|
898
1031
|
var TOOL_OUTPUT_HEURISTICS = [
|
|
899
1032
|
{ pattern: /\b(IMPORTANT|CRITICAL|WARNING|BREAKING)\b/i, importance: 4, label: "important" },
|
|
900
1033
|
{ pattern: /\b(DEPRECATED|SECURITY|VULNERABILITY)\b/i, importance: 4, label: "security" },
|
|
1034
|
+
{ pattern: /\b(migration failed|data loss|corrupt)/i, importance: 4, label: "data-risk" },
|
|
901
1035
|
{ pattern: /\b(decision:|decided to|NOTE:|conclusion:)/i, importance: 3, label: "decision-note" },
|
|
902
1036
|
{ pattern: /\b(discovered|found that|learned|realized|root cause)\b/i, importance: 3, label: "discovery" },
|
|
903
1037
|
{ pattern: /\b(workaround:|alternative:|instead of|switched to)/i, importance: 3, label: "approach-change" },
|
|
1038
|
+
{ pattern: /\b(refactored?|migrated?|upgraded?|replaced)\b/i, importance: 3, label: "refactor" },
|
|
1039
|
+
{ pattern: /\b(installed|added dependency|npm install|bun add)\b/i, importance: 2, label: "dependency" },
|
|
904
1040
|
{ pattern: /\b(TODO:|FIXME:|HACK:|WORKAROUND:)/i, importance: 2, label: "todo-note" },
|
|
905
1041
|
{ pattern: /\b(performance:|bottleneck|slow|timeout|OOM)/i, importance: 2, label: "performance" },
|
|
1042
|
+
{ pattern: /\b(created|scaffolded|initialized|bootstrapped)\b/i, importance: 2, label: "creation" },
|
|
1043
|
+
{ pattern: /\b(tests? (?:pass|fail)|coverage|assertion)/i, importance: 2, label: "test-result" },
|
|
1044
|
+
{ pattern: /\b(deployed|published|released|pushed to)\b/i, importance: 2, label: "deployment" },
|
|
906
1045
|
{ pattern: /^>\s+.{10,}/m, importance: 2, label: "quoted" }
|
|
907
1046
|
];
|
|
908
1047
|
var PROMPT_HEURISTICS = [
|
|
909
1048
|
{ pattern: /\b(IMPORTANT|CRITICAL|MUST)\b/i, importance: 4, label: "user-important" },
|
|
910
1049
|
{ pattern: /\b(remember that|note that|I decided|we should|keep in mind)\b/i, importance: 3, label: "user-note" },
|
|
911
1050
|
{ pattern: /\b(don't|do not|never|avoid|stop)\b/i, importance: 3, label: "user-constraint" },
|
|
912
|
-
{ pattern: /\b(
|
|
1051
|
+
{ pattern: /\b(fix|debug|investigate|analyze|resolve)\b/i, importance: 2, label: "user-task" },
|
|
1052
|
+
{ pattern: /\b(prefer|always use|convention is|pattern is)\b/i, importance: 2, label: "user-preference" },
|
|
1053
|
+
{ pattern: /\b(implement|build|create|add feature|integrate)\b/i, importance: 2, label: "user-feature" }
|
|
913
1054
|
];
|
|
914
1055
|
var MAX_VALUE_LENGTH = 500;
|
|
915
1056
|
var MIN_INPUT_LENGTH = 20;
|
|
@@ -1018,13 +1159,15 @@ function extractEntities(hook, promptNumber = 0) {
|
|
|
1018
1159
|
case "Agent": {
|
|
1019
1160
|
const subagentType = stringField2(tool_input, "subagent_type") ?? "general-purpose";
|
|
1020
1161
|
const prompt = stringField2(tool_input, "prompt") ?? "";
|
|
1021
|
-
|
|
1162
|
+
const agentResult = extractAgentResult(tool_response);
|
|
1163
|
+
raw.push(makeEntity(session_id, project, tool_name, "decision", `agent:${subagentType}: ${prompt.slice(0, 200)}`, 3, now, promptNumber, agentResult || undefined));
|
|
1022
1164
|
break;
|
|
1023
1165
|
}
|
|
1024
1166
|
case "Skill": {
|
|
1025
1167
|
const skillName = stringField2(tool_input, "skill") ?? "unknown";
|
|
1026
1168
|
const args = stringField2(tool_input, "args") ?? "";
|
|
1027
|
-
|
|
1169
|
+
const skillResult = extractAgentResult(tool_response);
|
|
1170
|
+
raw.push(makeEntity(session_id, project, tool_name, "decision", `skill:${skillName} ${args.slice(0, 120)}`.trim(), 2, now, promptNumber, skillResult || undefined));
|
|
1028
1171
|
break;
|
|
1029
1172
|
}
|
|
1030
1173
|
default:
|
|
@@ -1055,6 +1198,15 @@ function stringField2(obj, key) {
|
|
|
1055
1198
|
function deriveProject(hook) {
|
|
1056
1199
|
return "unknown";
|
|
1057
1200
|
}
|
|
1201
|
+
function extractAgentResult(response) {
|
|
1202
|
+
if (!response)
|
|
1203
|
+
return;
|
|
1204
|
+
const r = response;
|
|
1205
|
+
const text = typeof r === "string" ? r : stringField2(r, "result") ?? stringField2(r, "output") ?? stringField2(r, "content") ?? stringField2(r, "text");
|
|
1206
|
+
if (!text)
|
|
1207
|
+
return;
|
|
1208
|
+
return text.length > 800 ? text.slice(0, 797) + "..." : text;
|
|
1209
|
+
}
|
|
1058
1210
|
function extractFileFromBashCmd(cmd) {
|
|
1059
1211
|
const patterns = [
|
|
1060
1212
|
/(?:cp|mv)\s+\S+\s+(\S+\.[\w]+)/,
|
|
@@ -1971,6 +2123,18 @@ async function handleUserPromptSubmit(hook, project) {
|
|
|
1971
2123
|
user_prompt: cleanPrompt.slice(0, 500) || hook.prompt.slice(0, 500),
|
|
1972
2124
|
status: "active"
|
|
1973
2125
|
});
|
|
2126
|
+
const promptText = cleanPrompt || hook.prompt;
|
|
2127
|
+
if (promptText.length > 5) {
|
|
2128
|
+
const promptNum = store.getMessageCount(hook.session_id, "user");
|
|
2129
|
+
store.insertMessage({
|
|
2130
|
+
session_id: hook.session_id,
|
|
2131
|
+
project,
|
|
2132
|
+
role: "user",
|
|
2133
|
+
content: promptText.slice(0, 2000),
|
|
2134
|
+
prompt_number: promptNum,
|
|
2135
|
+
timestamp: Date.now()
|
|
2136
|
+
});
|
|
2137
|
+
}
|
|
1974
2138
|
const promptObs = extractObservationFromPrompt(cleanPrompt || hook.prompt, hook.session_id, project, 0);
|
|
1975
2139
|
if (promptObs)
|
|
1976
2140
|
store.insertEntity({ ...promptObs, project });
|