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 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
- - **91 unit tests**, batch queue (75ms→3ms), JSONL export/import, browser UI
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 (14 patterns) | -- | Yes | **Yes** |
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 (91 tests) | N/A | -- | **Yes** |
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 → 3-tier summarization: PostCompact > CLI claude > rule-based
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
- 14 heuristic patterns: IMPORTANT, CRITICAL, SECURITY, DEPRECATED,
177
- decision:, discovered, root cause, switched to, TODO:, FIXME:,
178
- HACK:, performance:, bottleneck, OOM, don't, never, prefer, etc.
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│ │ │ session end
198
- │ │past context │ │ summarize
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
- observations (14 patterns) Importance scored 1-5 │
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
- parts.push(`Task: ${session.user_prompt.slice(0, 200)}.`);
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, 10).join(", ");
510
- parts.push(`Files (${files.length}): ${listed}${files.length > 10 ? ` (+${files.length - 10} more)` : ""}.`);
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, 3).map((d) => d.entity_value.slice(0, 100)).join("; ");
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 first = errors[0];
518
- const ctx = first.context ? ` (${first.context.slice(0, 60)})` : "";
519
- parts.push(`Errors (${errors.length}): ${first.entity_value.slice(0, 100)}${ctx}.`);
520
- if (errors.length > 1) {
521
- parts.push(`Also: ${errors[1].entity_value.slice(0, 80)}.`);
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(-2).join("; ").slice(0, 200)}.`);
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 = 4000;
533
- var MAX_OUTPUT_CHARS = 1000;
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 2-3 plain sentences. No markdown, no headers, no code blocks.",
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, 10).join(", ")}`);
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, 5).join("; ")}`);
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, 3).join("; ")}`);
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, 3).join("; ")}`);
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
- if (files.length === 0 && errors.length === 0 && notes.length === 0)
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 obsValues = observations.slice(0, 5).map((o) => o.entity_value);
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, 100)),
684
- decisions: decisions.slice(0, 5).map((d) => d.entity_value.slice(0, 100)),
685
- notes: notes.slice(0, 3),
686
- observations: obsValues.slice(0, 3)
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: hook.compact_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(prefer|always use|convention is|pattern is)\b/i, importance: 2, label: "user-preference" }
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
- raw.push(makeEntity(session_id, project, tool_name, "decision", `agent:${subagentType}: ${prompt.slice(0, 100)}`, 3, now, promptNumber));
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
- raw.push(makeEntity(session_id, project, tool_name, "decision", `skill:${skillName} ${args.slice(0, 80)}`.trim(), 2, now, promptNumber));
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 });