claude-memory-hub 0.9.4 → 0.9.6

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,54 @@ Format follows [Keep a Changelog](https://keepachangelog.com/).
5
5
 
6
6
  ---
7
7
 
8
+ ## [0.9.6] - 2026-04-03
9
+
10
+ Richer session capture — Agent results, higher limits, cleaner summaries.
11
+
12
+ ### Enhancements
13
+
14
+ - **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
15
+ - **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
16
+ - **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
17
+ - **PostCompact summary cap** — compact summaries exceeding 5,000 chars are truncated (was unbounded — seen 22K in production). Reduces DB bloat and improves search relevance
18
+ - **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
19
+
20
+ ### Before vs After
21
+
22
+ ```
23
+ Before: "Task: <ide_opened_file>...</ide_opened_file>. Files (49): ..." (1165 chars, noisy)
24
+ After: "Task: fix broken hooks in memory-hub. Files (15): ... Decisions: agent:debugger: investigated... → Found temp path issue..." (richer, clean)
25
+ ```
26
+
27
+ ---
28
+
29
+ ## [0.9.5] - 2026-04-03
30
+
31
+ Stable install path — hooks no longer break after reboot or bunx cache cleanup.
32
+
33
+ ### Bug Fixes
34
+
35
+ - **Hooks pointing to temp `bunx` path** — `bunx claude-memory-hub install` registered hooks at `/private/tmp/bunx-*/...` (macOS) or `%TEMP%/bunx-*/...` (Windows). These paths are ephemeral and get deleted on reboot or cache cleanup, causing **all hooks to silently fail** — sessions stop being captured with no error visible to the user
36
+ - **Install now copies `dist/` to `~/.claude-memory-hub/dist/`** — a stable, persistent location under the user's home directory. Both hooks and MCP server reference this path instead of the package install location
37
+ - **Old hook entries auto-replaced** — `install` removes previous claude-memory-hub hook entries before registering new ones, fixing stale paths from prior installs without manual cleanup
38
+ - **`install.sh` updated** — shell-based installer uses the same stable path strategy with full bun binary resolution
39
+
40
+ ### How It Works
41
+
42
+ ```
43
+ bunx claude-memory-hub install
44
+ 1. Downloads package to temp dir (bunx behavior)
45
+ 2. Copies dist/*.js + dist/hooks/*.js → ~/.claude-memory-hub/dist/ ← NEW
46
+ 3. Registers hooks pointing to ~/.claude-memory-hub/dist/hooks/ ← STABLE
47
+ 4. Registers MCP server pointing to ~/.claude-memory-hub/dist/index.js
48
+ ```
49
+
50
+ ### Upgrade Note
51
+
52
+ Run `bunx claude-memory-hub@latest install` to fix broken hooks. No data loss — only hook paths are updated.
53
+
54
+ ---
55
+
8
56
  ## [0.9.4] - 2026-04-02
9
57
 
10
58
  Windows path fix — backslashes no longer eaten by bash.
package/dist/cli.js CHANGED
@@ -1669,7 +1669,7 @@ var init_importer = __esm(() => {
1669
1669
  });
1670
1670
 
1671
1671
  // src/cli/main.ts
1672
- import { existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync, writeFileSync } from "fs";
1672
+ import { existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync, writeFileSync, readdirSync } from "fs";
1673
1673
  import { homedir as homedir5 } from "os";
1674
1674
  import { join as join5, resolve, dirname } from "path";
1675
1675
 
@@ -1967,6 +1967,7 @@ import { spawnSync } from "child_process";
1967
1967
  var CLAUDE_DIR = join5(homedir5(), ".claude");
1968
1968
  var SETTINGS_PATH = join5(CLAUDE_DIR, "settings.json");
1969
1969
  var PKG_DIR = resolve(dirname(import.meta.dir));
1970
+ var STABLE_DIR = join5(homedir5(), ".claude-memory-hub");
1970
1971
  function shellPath(p) {
1971
1972
  const normalized = p.replace(/\\/g, "/");
1972
1973
  return normalized.includes(" ") ? `"${normalized}"` : normalized;
@@ -1988,11 +1989,37 @@ function getBunPath() {
1988
1989
  }
1989
1990
  return "bun";
1990
1991
  }
1992
+ function copyDistToStableDir() {
1993
+ const srcDist = join5(PKG_DIR, "dist");
1994
+ const destDist = join5(STABLE_DIR, "dist");
1995
+ if (!existsSync5(srcDist)) {
1996
+ throw new Error(`dist/ not found at ${srcDist}. Run 'bun run build:all' first.`);
1997
+ }
1998
+ const destHooks = join5(destDist, "hooks");
1999
+ mkdirSync3(destHooks, { recursive: true });
2000
+ for (const file of readdirSync(srcDist)) {
2001
+ if (file.endsWith(".js")) {
2002
+ const src = join5(srcDist, file);
2003
+ const dest = join5(destDist, file);
2004
+ writeFileSync(dest, readFileSync(src));
2005
+ }
2006
+ }
2007
+ const srcHooks = join5(srcDist, "hooks");
2008
+ if (existsSync5(srcHooks)) {
2009
+ for (const file of readdirSync(srcHooks)) {
2010
+ if (file.endsWith(".js")) {
2011
+ const src = join5(srcHooks, file);
2012
+ const dest = join5(destHooks, file);
2013
+ writeFileSync(dest, readFileSync(src));
2014
+ }
2015
+ }
2016
+ }
2017
+ }
1991
2018
  function getHookPath(hookName) {
1992
- return shellPath(join5(PKG_DIR, "dist", "hooks", `${hookName}.js`));
2019
+ return shellPath(join5(STABLE_DIR, "dist", "hooks", `${hookName}.js`));
1993
2020
  }
1994
2021
  function getMcpServerPath() {
1995
- return shellPath(join5(PKG_DIR, "dist", "index.js"));
2022
+ return shellPath(join5(STABLE_DIR, "dist", "index.js"));
1996
2023
  }
1997
2024
  function loadSettings() {
1998
2025
  if (!existsSync5(SETTINGS_PATH))
@@ -2012,7 +2039,16 @@ function saveSettings(settings) {
2012
2039
  function install() {
2013
2040
  console.log(`claude-memory-hub \u2014 install
2014
2041
  `);
2015
- console.log("1. Registering MCP server...");
2042
+ console.log("0. Copying dist/ to ~/.claude-memory-hub/dist/...");
2043
+ try {
2044
+ copyDistToStableDir();
2045
+ console.log(" Files copied to stable location.");
2046
+ } catch (e) {
2047
+ console.error(` Failed to copy dist/: ${e}`);
2048
+ console.error(" Hooks will reference package location (may break after bunx cleanup).");
2049
+ }
2050
+ console.log(`
2051
+ 1. Registering MCP server...`);
2016
2052
  const mcpPath = getMcpServerPath();
2017
2053
  const bunBin = getBunPath();
2018
2054
  const result = spawnSync("claude", ["mcp", "add", "claude-memory-hub", "-s", "user", "--", bunBin, "run", mcpPath], {
@@ -2044,14 +2080,12 @@ function install() {
2044
2080
  for (const [event, scriptPath] of hookEntries) {
2045
2081
  const hooks = settings.hooks;
2046
2082
  hooks[event] ??= [];
2047
- const exists = hooks[event].some((e) => JSON.stringify(e).includes("claude-memory-hub"));
2048
- if (!exists) {
2049
- hooks[event].push({
2050
- matcher: "",
2051
- hooks: [{ type: "command", command: `${bunBin} run ${scriptPath}` }]
2052
- });
2053
- registered++;
2054
- }
2083
+ hooks[event] = hooks[event].filter((e) => !JSON.stringify(e).includes("claude-memory-hub"));
2084
+ hooks[event].push({
2085
+ matcher: "",
2086
+ hooks: [{ type: "command", command: `${bunBin} run ${scriptPath}` }]
2087
+ });
2088
+ registered++;
2055
2089
  }
2056
2090
  saveSettings(settings);
2057
2091
  console.log(` ${registered} hook(s) registered. (${5 - registered} already existed)`);
@@ -376,6 +376,11 @@ class SessionStore {
376
376
  this.db.run("UPDATE sessions SET status = 'completed', ended_at = ? WHERE id = ?", [Date.now(), id]);
377
377
  }
378
378
  insertEntity(entity) {
379
+ if (entity.entity_type === "decision" || entity.entity_type === "observation") {
380
+ const existing = this.db.query("SELECT COUNT(*) as c FROM entities WHERE session_id = ? AND entity_type = ? AND entity_value = ?").get(entity.session_id, entity.entity_type, entity.entity_value);
381
+ if (existing && existing.c > 0)
382
+ return -1;
383
+ }
379
384
  const result = this.db.run(`INSERT INTO entities(session_id, project, tool_name, entity_type, entity_value, context, importance, created_at, prompt_number)
380
385
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
381
386
  entity.session_id,
@@ -495,37 +500,46 @@ function sanitizeFtsQuery(query) {
495
500
  }
496
501
 
497
502
  // src/summarizer/summarizer-prompts.ts
503
+ function stripNoiseTags(text) {
504
+ 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();
505
+ }
498
506
  function buildRuleBasedSummary(session, files, errors, decisions, notes = []) {
499
507
  const parts = [];
500
508
  if (session.user_prompt) {
501
- parts.push(`Task: ${session.user_prompt.slice(0, 200)}.`);
509
+ const cleanPrompt = stripNoiseTags(session.user_prompt);
510
+ if (cleanPrompt) {
511
+ parts.push(`Task: ${cleanPrompt.slice(0, 500)}.`);
512
+ }
502
513
  }
503
514
  if (files.length > 0) {
504
- const listed = files.slice(0, 10).join(", ");
505
- parts.push(`Files (${files.length}): ${listed}${files.length > 10 ? ` (+${files.length - 10} more)` : ""}.`);
515
+ const listed = files.slice(0, 15).join(", ");
516
+ parts.push(`Files (${files.length}): ${listed}${files.length > 15 ? ` (+${files.length - 15} more)` : ""}.`);
506
517
  }
507
518
  if (decisions.length > 0) {
508
- const listed = decisions.slice(0, 3).map((d) => d.entity_value.slice(0, 100)).join("; ");
519
+ const listed = decisions.slice(0, 5).map((d) => {
520
+ const base = d.entity_value.slice(0, 150);
521
+ const ctx = d.context ? ` \u2192 ${d.context.slice(0, 200)}` : "";
522
+ return base + ctx;
523
+ }).join("; ");
509
524
  parts.push(`Decisions: ${listed}.`);
510
525
  }
511
526
  if (errors.length > 0) {
512
- const first = errors[0];
513
- const ctx = first.context ? ` (${first.context.slice(0, 60)})` : "";
514
- parts.push(`Errors (${errors.length}): ${first.entity_value.slice(0, 100)}${ctx}.`);
515
- if (errors.length > 1) {
516
- parts.push(`Also: ${errors[1].entity_value.slice(0, 80)}.`);
517
- }
527
+ const errorLines = errors.slice(0, 5).map((e) => {
528
+ const ctx = e.context ? ` (${e.context.slice(0, 100)})` : "";
529
+ return `${e.entity_value.slice(0, 150)}${ctx}`;
530
+ });
531
+ parts.push(`Errors (${errors.length}): ${errorLines.join("; ")}.`);
518
532
  }
519
533
  if (notes.length > 0) {
520
- parts.push(`Notes: ${notes.slice(-2).join("; ").slice(0, 200)}.`);
534
+ parts.push(`Notes: ${notes.slice(-5).join("; ").slice(0, 500)}.`);
521
535
  }
522
536
  return parts.join(" ") || `Session in project ${session.project}.`;
523
537
  }
524
538
 
525
539
  // src/summarizer/cli-summarizer.ts
526
540
  var log2 = createLogger("cli-summarizer");
527
- var MAX_PROMPT_CHARS = 4000;
528
- var MAX_OUTPUT_CHARS = 1000;
541
+ var MAX_PROMPT_CHARS = 6000;
542
+ var MAX_OUTPUT_CHARS = 2000;
529
543
  var DEFAULT_TIMEOUT_MS = 30000;
530
544
  var _cliAvailable;
531
545
  var _cliCheckedAt = 0;
@@ -545,27 +559,30 @@ function isClaudeCliAvailable() {
545
559
  _cliCheckedAt = Date.now();
546
560
  return _cliAvailable;
547
561
  }
562
+ function stripNoise(text) {
563
+ 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();
564
+ }
548
565
  function buildCliPrompt(ctx) {
549
566
  const sections = [
550
- "Summarize this coding session in 2-3 plain sentences. No markdown, no headers, no code blocks.",
551
- "Focus on: what was accomplished, key decisions, important findings.",
567
+ "Summarize this coding session in 3-5 plain sentences. No markdown, no headers, no code blocks.",
568
+ "Focus on: what was accomplished, key decisions, important findings, tools/agents used.",
552
569
  "",
553
570
  `Project: ${ctx.project}`
554
571
  ];
555
572
  if (ctx.files.length > 0) {
556
- sections.push(`Files modified: ${ctx.files.slice(0, 10).join(", ")}`);
573
+ sections.push(`Files modified: ${ctx.files.slice(0, 15).join(", ")}`);
557
574
  }
558
575
  if (ctx.errors.length > 0) {
559
- sections.push(`Errors resolved: ${ctx.errors.slice(0, 5).join("; ")}`);
576
+ sections.push(`Errors resolved: ${ctx.errors.slice(0, 5).map(stripNoise).join("; ")}`);
560
577
  }
561
578
  if (ctx.decisions.length > 0) {
562
- sections.push(`Decisions: ${ctx.decisions.slice(0, 5).join("; ")}`);
579
+ sections.push(`Decisions: ${ctx.decisions.slice(0, 8).map(stripNoise).join("; ")}`);
563
580
  }
564
581
  if (ctx.notes.length > 0) {
565
- sections.push(`Notes: ${ctx.notes.slice(0, 3).join("; ")}`);
582
+ sections.push(`Notes: ${ctx.notes.slice(0, 5).map(stripNoise).join("; ")}`);
566
583
  }
567
584
  if (ctx.observations.length > 0) {
568
- sections.push(`Key observations: ${ctx.observations.slice(0, 3).join("; ")}`);
585
+ sections.push(`Key observations: ${ctx.observations.slice(0, 5).map(stripNoise).join("; ")}`);
569
586
  }
570
587
  let prompt = sections.join(`
571
588
  `);
@@ -666,19 +683,23 @@ class SessionSummarizer {
666
683
  const hasModified = this.sessionStore.hasModifiedFiles(session_id);
667
684
  if (!hasModified && errors.length === 0 && decisions.length === 0 && notes.length === 0 && observations.length === 0)
668
685
  return;
669
- const obsValues = observations.slice(0, 5).map((o) => o.entity_value);
686
+ const obsValues = observations.slice(0, 8).map((o) => o.entity_value);
670
687
  let summaryText;
671
688
  let tier = "rule-based";
672
689
  const llmMode = process.env["CLAUDE_MEMORY_HUB_LLM"] ?? "auto";
673
690
  if (llmMode !== "rule-based") {
691
+ const decisionDetails = decisions.slice(0, 8).map((d) => {
692
+ const ctx2 = d.context ? ` \u2192 ${d.context.slice(0, 200)}` : "";
693
+ return d.entity_value.slice(0, 150) + ctx2;
694
+ });
674
695
  const ctx = {
675
696
  sessionId: session_id,
676
697
  project,
677
698
  files,
678
- errors: errors.slice(0, 5).map((e) => e.entity_value.slice(0, 100)),
679
- decisions: decisions.slice(0, 5).map((d) => d.entity_value.slice(0, 100)),
680
- notes: notes.slice(0, 3),
681
- observations: obsValues.slice(0, 3)
699
+ errors: errors.slice(0, 5).map((e) => e.entity_value.slice(0, 150)),
700
+ decisions: decisionDetails,
701
+ notes: notes.slice(0, 5),
702
+ observations: obsValues.slice(0, 5)
682
703
  };
683
704
  summaryText = await tryCliSummary(ctx);
684
705
  if (summaryText)
@@ -769,10 +790,14 @@ async function handlePostCompact(hook, project) {
769
790
  const files = store.getSessionFiles(hook.session_id);
770
791
  const decisions = store.getSessionDecisions(hook.session_id);
771
792
  const errors = store.getSessionErrors(hook.session_id);
793
+ const MAX_COMPACT_SUMMARY = 5000;
794
+ const summary = hook.compact_summary.length > MAX_COMPACT_SUMMARY ? hook.compact_summary.slice(0, MAX_COMPACT_SUMMARY - 20) + `
795
+
796
+ [truncated]` : hook.compact_summary;
772
797
  ltStore.upsertSummary({
773
798
  session_id: hook.session_id,
774
799
  project,
775
- summary: hook.compact_summary,
800
+ summary,
776
801
  files_touched: JSON.stringify(files.slice(0, 50)),
777
802
  decisions: JSON.stringify(decisions.slice(0, 20).map((d) => d.entity_value)),
778
803
  errors_fixed: JSON.stringify(errors.slice(0, 10).map((e) => e.entity_value.slice(0, 100))),
@@ -893,18 +918,26 @@ function extractCodePatterns(content) {
893
918
  var TOOL_OUTPUT_HEURISTICS = [
894
919
  { pattern: /\b(IMPORTANT|CRITICAL|WARNING|BREAKING)\b/i, importance: 4, label: "important" },
895
920
  { pattern: /\b(DEPRECATED|SECURITY|VULNERABILITY)\b/i, importance: 4, label: "security" },
921
+ { pattern: /\b(migration failed|data loss|corrupt)/i, importance: 4, label: "data-risk" },
896
922
  { pattern: /\b(decision:|decided to|NOTE:|conclusion:)/i, importance: 3, label: "decision-note" },
897
923
  { pattern: /\b(discovered|found that|learned|realized|root cause)\b/i, importance: 3, label: "discovery" },
898
924
  { pattern: /\b(workaround:|alternative:|instead of|switched to)/i, importance: 3, label: "approach-change" },
925
+ { pattern: /\b(refactored?|migrated?|upgraded?|replaced)\b/i, importance: 3, label: "refactor" },
926
+ { pattern: /\b(installed|added dependency|npm install|bun add)\b/i, importance: 2, label: "dependency" },
899
927
  { pattern: /\b(TODO:|FIXME:|HACK:|WORKAROUND:)/i, importance: 2, label: "todo-note" },
900
928
  { pattern: /\b(performance:|bottleneck|slow|timeout|OOM)/i, importance: 2, label: "performance" },
929
+ { pattern: /\b(created|scaffolded|initialized|bootstrapped)\b/i, importance: 2, label: "creation" },
930
+ { pattern: /\b(tests? (?:pass|fail)|coverage|assertion)/i, importance: 2, label: "test-result" },
931
+ { pattern: /\b(deployed|published|released|pushed to)\b/i, importance: 2, label: "deployment" },
901
932
  { pattern: /^>\s+.{10,}/m, importance: 2, label: "quoted" }
902
933
  ];
903
934
  var PROMPT_HEURISTICS = [
904
935
  { pattern: /\b(IMPORTANT|CRITICAL|MUST)\b/i, importance: 4, label: "user-important" },
905
936
  { pattern: /\b(remember that|note that|I decided|we should|keep in mind)\b/i, importance: 3, label: "user-note" },
906
937
  { pattern: /\b(don't|do not|never|avoid|stop)\b/i, importance: 3, label: "user-constraint" },
907
- { pattern: /\b(prefer|always use|convention is|pattern is)\b/i, importance: 2, label: "user-preference" }
938
+ { pattern: /\b(fix|debug|investigate|analyze|resolve)\b/i, importance: 2, label: "user-task" },
939
+ { pattern: /\b(prefer|always use|convention is|pattern is)\b/i, importance: 2, label: "user-preference" },
940
+ { pattern: /\b(implement|build|create|add feature|integrate)\b/i, importance: 2, label: "user-feature" }
908
941
  ];
909
942
  var MAX_VALUE_LENGTH = 500;
910
943
  var MIN_INPUT_LENGTH = 20;
@@ -1013,13 +1046,15 @@ function extractEntities(hook, promptNumber = 0) {
1013
1046
  case "Agent": {
1014
1047
  const subagentType = stringField2(tool_input, "subagent_type") ?? "general-purpose";
1015
1048
  const prompt = stringField2(tool_input, "prompt") ?? "";
1016
- raw.push(makeEntity(session_id, project, tool_name, "decision", `agent:${subagentType}: ${prompt.slice(0, 100)}`, 3, now, promptNumber));
1049
+ const agentResult = extractAgentResult(tool_response);
1050
+ raw.push(makeEntity(session_id, project, tool_name, "decision", `agent:${subagentType}: ${prompt.slice(0, 200)}`, 3, now, promptNumber, agentResult || undefined));
1017
1051
  break;
1018
1052
  }
1019
1053
  case "Skill": {
1020
1054
  const skillName = stringField2(tool_input, "skill") ?? "unknown";
1021
1055
  const args = stringField2(tool_input, "args") ?? "";
1022
- raw.push(makeEntity(session_id, project, tool_name, "decision", `skill:${skillName} ${args.slice(0, 80)}`.trim(), 2, now, promptNumber));
1056
+ const skillResult = extractAgentResult(tool_response);
1057
+ raw.push(makeEntity(session_id, project, tool_name, "decision", `skill:${skillName} ${args.slice(0, 120)}`.trim(), 2, now, promptNumber, skillResult || undefined));
1023
1058
  break;
1024
1059
  }
1025
1060
  default:
@@ -1050,6 +1085,15 @@ function stringField2(obj, key) {
1050
1085
  function deriveProject(hook) {
1051
1086
  return "unknown";
1052
1087
  }
1088
+ function extractAgentResult(response) {
1089
+ if (!response)
1090
+ return;
1091
+ const r = response;
1092
+ const text = typeof r === "string" ? r : stringField2(r, "result") ?? stringField2(r, "output") ?? stringField2(r, "content") ?? stringField2(r, "text");
1093
+ if (!text)
1094
+ return;
1095
+ return text.length > 800 ? text.slice(0, 797) + "..." : text;
1096
+ }
1053
1097
  function extractFileFromBashCmd(cmd) {
1054
1098
  const patterns = [
1055
1099
  /(?:cp|mv)\s+\S+\s+(\S+\.[\w]+)/,
@@ -376,6 +376,11 @@ class SessionStore {
376
376
  this.db.run("UPDATE sessions SET status = 'completed', ended_at = ? WHERE id = ?", [Date.now(), id]);
377
377
  }
378
378
  insertEntity(entity) {
379
+ if (entity.entity_type === "decision" || entity.entity_type === "observation") {
380
+ const existing = this.db.query("SELECT COUNT(*) as c FROM entities WHERE session_id = ? AND entity_type = ? AND entity_value = ?").get(entity.session_id, entity.entity_type, entity.entity_value);
381
+ if (existing && existing.c > 0)
382
+ return -1;
383
+ }
379
384
  const result = this.db.run(`INSERT INTO entities(session_id, project, tool_name, entity_type, entity_value, context, importance, created_at, prompt_number)
380
385
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
381
386
  entity.session_id,
@@ -597,18 +602,26 @@ function extractCodePatterns(content) {
597
602
  var TOOL_OUTPUT_HEURISTICS = [
598
603
  { pattern: /\b(IMPORTANT|CRITICAL|WARNING|BREAKING)\b/i, importance: 4, label: "important" },
599
604
  { pattern: /\b(DEPRECATED|SECURITY|VULNERABILITY)\b/i, importance: 4, label: "security" },
605
+ { pattern: /\b(migration failed|data loss|corrupt)/i, importance: 4, label: "data-risk" },
600
606
  { pattern: /\b(decision:|decided to|NOTE:|conclusion:)/i, importance: 3, label: "decision-note" },
601
607
  { pattern: /\b(discovered|found that|learned|realized|root cause)\b/i, importance: 3, label: "discovery" },
602
608
  { pattern: /\b(workaround:|alternative:|instead of|switched to)/i, importance: 3, label: "approach-change" },
609
+ { pattern: /\b(refactored?|migrated?|upgraded?|replaced)\b/i, importance: 3, label: "refactor" },
610
+ { pattern: /\b(installed|added dependency|npm install|bun add)\b/i, importance: 2, label: "dependency" },
603
611
  { pattern: /\b(TODO:|FIXME:|HACK:|WORKAROUND:)/i, importance: 2, label: "todo-note" },
604
612
  { pattern: /\b(performance:|bottleneck|slow|timeout|OOM)/i, importance: 2, label: "performance" },
613
+ { pattern: /\b(created|scaffolded|initialized|bootstrapped)\b/i, importance: 2, label: "creation" },
614
+ { pattern: /\b(tests? (?:pass|fail)|coverage|assertion)/i, importance: 2, label: "test-result" },
615
+ { pattern: /\b(deployed|published|released|pushed to)\b/i, importance: 2, label: "deployment" },
605
616
  { pattern: /^>\s+.{10,}/m, importance: 2, label: "quoted" }
606
617
  ];
607
618
  var PROMPT_HEURISTICS = [
608
619
  { pattern: /\b(IMPORTANT|CRITICAL|MUST)\b/i, importance: 4, label: "user-important" },
609
620
  { pattern: /\b(remember that|note that|I decided|we should|keep in mind)\b/i, importance: 3, label: "user-note" },
610
621
  { pattern: /\b(don't|do not|never|avoid|stop)\b/i, importance: 3, label: "user-constraint" },
611
- { pattern: /\b(prefer|always use|convention is|pattern is)\b/i, importance: 2, label: "user-preference" }
622
+ { pattern: /\b(fix|debug|investigate|analyze|resolve)\b/i, importance: 2, label: "user-task" },
623
+ { pattern: /\b(prefer|always use|convention is|pattern is)\b/i, importance: 2, label: "user-preference" },
624
+ { pattern: /\b(implement|build|create|add feature|integrate)\b/i, importance: 2, label: "user-feature" }
612
625
  ];
613
626
  var MAX_VALUE_LENGTH = 500;
614
627
  var MIN_INPUT_LENGTH = 20;
@@ -717,13 +730,15 @@ function extractEntities(hook, promptNumber = 0) {
717
730
  case "Agent": {
718
731
  const subagentType = stringField2(tool_input, "subagent_type") ?? "general-purpose";
719
732
  const prompt = stringField2(tool_input, "prompt") ?? "";
720
- raw.push(makeEntity(session_id, project, tool_name, "decision", `agent:${subagentType}: ${prompt.slice(0, 100)}`, 3, now, promptNumber));
733
+ const agentResult = extractAgentResult(tool_response);
734
+ raw.push(makeEntity(session_id, project, tool_name, "decision", `agent:${subagentType}: ${prompt.slice(0, 200)}`, 3, now, promptNumber, agentResult || undefined));
721
735
  break;
722
736
  }
723
737
  case "Skill": {
724
738
  const skillName = stringField2(tool_input, "skill") ?? "unknown";
725
739
  const args = stringField2(tool_input, "args") ?? "";
726
- raw.push(makeEntity(session_id, project, tool_name, "decision", `skill:${skillName} ${args.slice(0, 80)}`.trim(), 2, now, promptNumber));
740
+ const skillResult = extractAgentResult(tool_response);
741
+ raw.push(makeEntity(session_id, project, tool_name, "decision", `skill:${skillName} ${args.slice(0, 120)}`.trim(), 2, now, promptNumber, skillResult || undefined));
727
742
  break;
728
743
  }
729
744
  default:
@@ -754,6 +769,15 @@ function stringField2(obj, key) {
754
769
  function deriveProject(hook) {
755
770
  return "unknown";
756
771
  }
772
+ function extractAgentResult(response) {
773
+ if (!response)
774
+ return;
775
+ const r = response;
776
+ const text = typeof r === "string" ? r : stringField2(r, "result") ?? stringField2(r, "output") ?? stringField2(r, "content") ?? stringField2(r, "text");
777
+ if (!text)
778
+ return;
779
+ return text.length > 800 ? text.slice(0, 797) + "..." : text;
780
+ }
757
781
  function extractFileFromBashCmd(cmd) {
758
782
  const patterns = [
759
783
  /(?:cp|mv)\s+\S+\s+(\S+\.[\w]+)/,
@@ -376,6 +376,11 @@ class SessionStore {
376
376
  this.db.run("UPDATE sessions SET status = 'completed', ended_at = ? WHERE id = ?", [Date.now(), id]);
377
377
  }
378
378
  insertEntity(entity) {
379
+ if (entity.entity_type === "decision" || entity.entity_type === "observation") {
380
+ const existing = this.db.query("SELECT COUNT(*) as c FROM entities WHERE session_id = ? AND entity_type = ? AND entity_value = ?").get(entity.session_id, entity.entity_type, entity.entity_value);
381
+ if (existing && existing.c > 0)
382
+ return -1;
383
+ }
379
384
  const result = this.db.run(`INSERT INTO entities(session_id, project, tool_name, entity_type, entity_value, context, importance, created_at, prompt_number)
380
385
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
381
386
  entity.session_id,
@@ -495,37 +500,46 @@ function sanitizeFtsQuery(query) {
495
500
  }
496
501
 
497
502
  // src/summarizer/summarizer-prompts.ts
503
+ function stripNoiseTags(text) {
504
+ 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();
505
+ }
498
506
  function buildRuleBasedSummary(session, files, errors, decisions, notes = []) {
499
507
  const parts = [];
500
508
  if (session.user_prompt) {
501
- parts.push(`Task: ${session.user_prompt.slice(0, 200)}.`);
509
+ const cleanPrompt = stripNoiseTags(session.user_prompt);
510
+ if (cleanPrompt) {
511
+ parts.push(`Task: ${cleanPrompt.slice(0, 500)}.`);
512
+ }
502
513
  }
503
514
  if (files.length > 0) {
504
- const listed = files.slice(0, 10).join(", ");
505
- parts.push(`Files (${files.length}): ${listed}${files.length > 10 ? ` (+${files.length - 10} more)` : ""}.`);
515
+ const listed = files.slice(0, 15).join(", ");
516
+ parts.push(`Files (${files.length}): ${listed}${files.length > 15 ? ` (+${files.length - 15} more)` : ""}.`);
506
517
  }
507
518
  if (decisions.length > 0) {
508
- const listed = decisions.slice(0, 3).map((d) => d.entity_value.slice(0, 100)).join("; ");
519
+ const listed = decisions.slice(0, 5).map((d) => {
520
+ const base = d.entity_value.slice(0, 150);
521
+ const ctx = d.context ? ` \u2192 ${d.context.slice(0, 200)}` : "";
522
+ return base + ctx;
523
+ }).join("; ");
509
524
  parts.push(`Decisions: ${listed}.`);
510
525
  }
511
526
  if (errors.length > 0) {
512
- const first = errors[0];
513
- const ctx = first.context ? ` (${first.context.slice(0, 60)})` : "";
514
- parts.push(`Errors (${errors.length}): ${first.entity_value.slice(0, 100)}${ctx}.`);
515
- if (errors.length > 1) {
516
- parts.push(`Also: ${errors[1].entity_value.slice(0, 80)}.`);
517
- }
527
+ const errorLines = errors.slice(0, 5).map((e) => {
528
+ const ctx = e.context ? ` (${e.context.slice(0, 100)})` : "";
529
+ return `${e.entity_value.slice(0, 150)}${ctx}`;
530
+ });
531
+ parts.push(`Errors (${errors.length}): ${errorLines.join("; ")}.`);
518
532
  }
519
533
  if (notes.length > 0) {
520
- parts.push(`Notes: ${notes.slice(-2).join("; ").slice(0, 200)}.`);
534
+ parts.push(`Notes: ${notes.slice(-5).join("; ").slice(0, 500)}.`);
521
535
  }
522
536
  return parts.join(" ") || `Session in project ${session.project}.`;
523
537
  }
524
538
 
525
539
  // src/summarizer/cli-summarizer.ts
526
540
  var log2 = createLogger("cli-summarizer");
527
- var MAX_PROMPT_CHARS = 4000;
528
- var MAX_OUTPUT_CHARS = 1000;
541
+ var MAX_PROMPT_CHARS = 6000;
542
+ var MAX_OUTPUT_CHARS = 2000;
529
543
  var DEFAULT_TIMEOUT_MS = 30000;
530
544
  var _cliAvailable;
531
545
  var _cliCheckedAt = 0;
@@ -545,27 +559,30 @@ function isClaudeCliAvailable() {
545
559
  _cliCheckedAt = Date.now();
546
560
  return _cliAvailable;
547
561
  }
562
+ function stripNoise(text) {
563
+ 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();
564
+ }
548
565
  function buildCliPrompt(ctx) {
549
566
  const sections = [
550
- "Summarize this coding session in 2-3 plain sentences. No markdown, no headers, no code blocks.",
551
- "Focus on: what was accomplished, key decisions, important findings.",
567
+ "Summarize this coding session in 3-5 plain sentences. No markdown, no headers, no code blocks.",
568
+ "Focus on: what was accomplished, key decisions, important findings, tools/agents used.",
552
569
  "",
553
570
  `Project: ${ctx.project}`
554
571
  ];
555
572
  if (ctx.files.length > 0) {
556
- sections.push(`Files modified: ${ctx.files.slice(0, 10).join(", ")}`);
573
+ sections.push(`Files modified: ${ctx.files.slice(0, 15).join(", ")}`);
557
574
  }
558
575
  if (ctx.errors.length > 0) {
559
- sections.push(`Errors resolved: ${ctx.errors.slice(0, 5).join("; ")}`);
576
+ sections.push(`Errors resolved: ${ctx.errors.slice(0, 5).map(stripNoise).join("; ")}`);
560
577
  }
561
578
  if (ctx.decisions.length > 0) {
562
- sections.push(`Decisions: ${ctx.decisions.slice(0, 5).join("; ")}`);
579
+ sections.push(`Decisions: ${ctx.decisions.slice(0, 8).map(stripNoise).join("; ")}`);
563
580
  }
564
581
  if (ctx.notes.length > 0) {
565
- sections.push(`Notes: ${ctx.notes.slice(0, 3).join("; ")}`);
582
+ sections.push(`Notes: ${ctx.notes.slice(0, 5).map(stripNoise).join("; ")}`);
566
583
  }
567
584
  if (ctx.observations.length > 0) {
568
- sections.push(`Key observations: ${ctx.observations.slice(0, 3).join("; ")}`);
585
+ sections.push(`Key observations: ${ctx.observations.slice(0, 5).map(stripNoise).join("; ")}`);
569
586
  }
570
587
  let prompt = sections.join(`
571
588
  `);
@@ -666,19 +683,23 @@ class SessionSummarizer {
666
683
  const hasModified = this.sessionStore.hasModifiedFiles(session_id);
667
684
  if (!hasModified && errors.length === 0 && decisions.length === 0 && notes.length === 0 && observations.length === 0)
668
685
  return;
669
- const obsValues = observations.slice(0, 5).map((o) => o.entity_value);
686
+ const obsValues = observations.slice(0, 8).map((o) => o.entity_value);
670
687
  let summaryText;
671
688
  let tier = "rule-based";
672
689
  const llmMode = process.env["CLAUDE_MEMORY_HUB_LLM"] ?? "auto";
673
690
  if (llmMode !== "rule-based") {
691
+ const decisionDetails = decisions.slice(0, 8).map((d) => {
692
+ const ctx2 = d.context ? ` \u2192 ${d.context.slice(0, 200)}` : "";
693
+ return d.entity_value.slice(0, 150) + ctx2;
694
+ });
674
695
  const ctx = {
675
696
  sessionId: session_id,
676
697
  project,
677
698
  files,
678
- errors: errors.slice(0, 5).map((e) => e.entity_value.slice(0, 100)),
679
- decisions: decisions.slice(0, 5).map((d) => d.entity_value.slice(0, 100)),
680
- notes: notes.slice(0, 3),
681
- observations: obsValues.slice(0, 3)
699
+ errors: errors.slice(0, 5).map((e) => e.entity_value.slice(0, 150)),
700
+ decisions: decisionDetails,
701
+ notes: notes.slice(0, 5),
702
+ observations: obsValues.slice(0, 5)
682
703
  };
683
704
  summaryText = await tryCliSummary(ctx);
684
705
  if (summaryText)
@@ -769,10 +790,14 @@ async function handlePostCompact(hook, project) {
769
790
  const files = store.getSessionFiles(hook.session_id);
770
791
  const decisions = store.getSessionDecisions(hook.session_id);
771
792
  const errors = store.getSessionErrors(hook.session_id);
793
+ const MAX_COMPACT_SUMMARY = 5000;
794
+ const summary = hook.compact_summary.length > MAX_COMPACT_SUMMARY ? hook.compact_summary.slice(0, MAX_COMPACT_SUMMARY - 20) + `
795
+
796
+ [truncated]` : hook.compact_summary;
772
797
  ltStore.upsertSummary({
773
798
  session_id: hook.session_id,
774
799
  project,
775
- summary: hook.compact_summary,
800
+ summary,
776
801
  files_touched: JSON.stringify(files.slice(0, 50)),
777
802
  decisions: JSON.stringify(decisions.slice(0, 20).map((d) => d.entity_value)),
778
803
  errors_fixed: JSON.stringify(errors.slice(0, 10).map((e) => e.entity_value.slice(0, 100))),
@@ -893,18 +918,26 @@ function extractCodePatterns(content) {
893
918
  var TOOL_OUTPUT_HEURISTICS = [
894
919
  { pattern: /\b(IMPORTANT|CRITICAL|WARNING|BREAKING)\b/i, importance: 4, label: "important" },
895
920
  { pattern: /\b(DEPRECATED|SECURITY|VULNERABILITY)\b/i, importance: 4, label: "security" },
921
+ { pattern: /\b(migration failed|data loss|corrupt)/i, importance: 4, label: "data-risk" },
896
922
  { pattern: /\b(decision:|decided to|NOTE:|conclusion:)/i, importance: 3, label: "decision-note" },
897
923
  { pattern: /\b(discovered|found that|learned|realized|root cause)\b/i, importance: 3, label: "discovery" },
898
924
  { pattern: /\b(workaround:|alternative:|instead of|switched to)/i, importance: 3, label: "approach-change" },
925
+ { pattern: /\b(refactored?|migrated?|upgraded?|replaced)\b/i, importance: 3, label: "refactor" },
926
+ { pattern: /\b(installed|added dependency|npm install|bun add)\b/i, importance: 2, label: "dependency" },
899
927
  { pattern: /\b(TODO:|FIXME:|HACK:|WORKAROUND:)/i, importance: 2, label: "todo-note" },
900
928
  { pattern: /\b(performance:|bottleneck|slow|timeout|OOM)/i, importance: 2, label: "performance" },
929
+ { pattern: /\b(created|scaffolded|initialized|bootstrapped)\b/i, importance: 2, label: "creation" },
930
+ { pattern: /\b(tests? (?:pass|fail)|coverage|assertion)/i, importance: 2, label: "test-result" },
931
+ { pattern: /\b(deployed|published|released|pushed to)\b/i, importance: 2, label: "deployment" },
901
932
  { pattern: /^>\s+.{10,}/m, importance: 2, label: "quoted" }
902
933
  ];
903
934
  var PROMPT_HEURISTICS = [
904
935
  { pattern: /\b(IMPORTANT|CRITICAL|MUST)\b/i, importance: 4, label: "user-important" },
905
936
  { pattern: /\b(remember that|note that|I decided|we should|keep in mind)\b/i, importance: 3, label: "user-note" },
906
937
  { pattern: /\b(don't|do not|never|avoid|stop)\b/i, importance: 3, label: "user-constraint" },
907
- { pattern: /\b(prefer|always use|convention is|pattern is)\b/i, importance: 2, label: "user-preference" }
938
+ { pattern: /\b(fix|debug|investigate|analyze|resolve)\b/i, importance: 2, label: "user-task" },
939
+ { pattern: /\b(prefer|always use|convention is|pattern is)\b/i, importance: 2, label: "user-preference" },
940
+ { pattern: /\b(implement|build|create|add feature|integrate)\b/i, importance: 2, label: "user-feature" }
908
941
  ];
909
942
  var MAX_VALUE_LENGTH = 500;
910
943
  var MIN_INPUT_LENGTH = 20;
@@ -1013,13 +1046,15 @@ function extractEntities(hook, promptNumber = 0) {
1013
1046
  case "Agent": {
1014
1047
  const subagentType = stringField2(tool_input, "subagent_type") ?? "general-purpose";
1015
1048
  const prompt = stringField2(tool_input, "prompt") ?? "";
1016
- raw.push(makeEntity(session_id, project, tool_name, "decision", `agent:${subagentType}: ${prompt.slice(0, 100)}`, 3, now, promptNumber));
1049
+ const agentResult = extractAgentResult(tool_response);
1050
+ raw.push(makeEntity(session_id, project, tool_name, "decision", `agent:${subagentType}: ${prompt.slice(0, 200)}`, 3, now, promptNumber, agentResult || undefined));
1017
1051
  break;
1018
1052
  }
1019
1053
  case "Skill": {
1020
1054
  const skillName = stringField2(tool_input, "skill") ?? "unknown";
1021
1055
  const args = stringField2(tool_input, "args") ?? "";
1022
- raw.push(makeEntity(session_id, project, tool_name, "decision", `skill:${skillName} ${args.slice(0, 80)}`.trim(), 2, now, promptNumber));
1056
+ const skillResult = extractAgentResult(tool_response);
1057
+ raw.push(makeEntity(session_id, project, tool_name, "decision", `skill:${skillName} ${args.slice(0, 120)}`.trim(), 2, now, promptNumber, skillResult || undefined));
1023
1058
  break;
1024
1059
  }
1025
1060
  default:
@@ -1050,6 +1085,15 @@ function stringField2(obj, key) {
1050
1085
  function deriveProject(hook) {
1051
1086
  return "unknown";
1052
1087
  }
1088
+ function extractAgentResult(response) {
1089
+ if (!response)
1090
+ return;
1091
+ const r = response;
1092
+ const text = typeof r === "string" ? r : stringField2(r, "result") ?? stringField2(r, "output") ?? stringField2(r, "content") ?? stringField2(r, "text");
1093
+ if (!text)
1094
+ return;
1095
+ return text.length > 800 ? text.slice(0, 797) + "..." : text;
1096
+ }
1053
1097
  function extractFileFromBashCmd(cmd) {
1054
1098
  const patterns = [
1055
1099
  /(?:cp|mv)\s+\S+\s+(\S+\.[\w]+)/,
@@ -376,6 +376,11 @@ class SessionStore {
376
376
  this.db.run("UPDATE sessions SET status = 'completed', ended_at = ? WHERE id = ?", [Date.now(), id]);
377
377
  }
378
378
  insertEntity(entity) {
379
+ if (entity.entity_type === "decision" || entity.entity_type === "observation") {
380
+ const existing = this.db.query("SELECT COUNT(*) as c FROM entities WHERE session_id = ? AND entity_type = ? AND entity_value = ?").get(entity.session_id, entity.entity_type, entity.entity_value);
381
+ if (existing && existing.c > 0)
382
+ return -1;
383
+ }
379
384
  const result = this.db.run(`INSERT INTO entities(session_id, project, tool_name, entity_type, entity_value, context, importance, created_at, prompt_number)
380
385
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
381
386
  entity.session_id,
@@ -597,18 +602,26 @@ function extractCodePatterns(content) {
597
602
  var TOOL_OUTPUT_HEURISTICS = [
598
603
  { pattern: /\b(IMPORTANT|CRITICAL|WARNING|BREAKING)\b/i, importance: 4, label: "important" },
599
604
  { pattern: /\b(DEPRECATED|SECURITY|VULNERABILITY)\b/i, importance: 4, label: "security" },
605
+ { pattern: /\b(migration failed|data loss|corrupt)/i, importance: 4, label: "data-risk" },
600
606
  { pattern: /\b(decision:|decided to|NOTE:|conclusion:)/i, importance: 3, label: "decision-note" },
601
607
  { pattern: /\b(discovered|found that|learned|realized|root cause)\b/i, importance: 3, label: "discovery" },
602
608
  { pattern: /\b(workaround:|alternative:|instead of|switched to)/i, importance: 3, label: "approach-change" },
609
+ { pattern: /\b(refactored?|migrated?|upgraded?|replaced)\b/i, importance: 3, label: "refactor" },
610
+ { pattern: /\b(installed|added dependency|npm install|bun add)\b/i, importance: 2, label: "dependency" },
603
611
  { pattern: /\b(TODO:|FIXME:|HACK:|WORKAROUND:)/i, importance: 2, label: "todo-note" },
604
612
  { pattern: /\b(performance:|bottleneck|slow|timeout|OOM)/i, importance: 2, label: "performance" },
613
+ { pattern: /\b(created|scaffolded|initialized|bootstrapped)\b/i, importance: 2, label: "creation" },
614
+ { pattern: /\b(tests? (?:pass|fail)|coverage|assertion)/i, importance: 2, label: "test-result" },
615
+ { pattern: /\b(deployed|published|released|pushed to)\b/i, importance: 2, label: "deployment" },
605
616
  { pattern: /^>\s+.{10,}/m, importance: 2, label: "quoted" }
606
617
  ];
607
618
  var PROMPT_HEURISTICS = [
608
619
  { pattern: /\b(IMPORTANT|CRITICAL|MUST)\b/i, importance: 4, label: "user-important" },
609
620
  { pattern: /\b(remember that|note that|I decided|we should|keep in mind)\b/i, importance: 3, label: "user-note" },
610
621
  { pattern: /\b(don't|do not|never|avoid|stop)\b/i, importance: 3, label: "user-constraint" },
611
- { pattern: /\b(prefer|always use|convention is|pattern is)\b/i, importance: 2, label: "user-preference" }
622
+ { pattern: /\b(fix|debug|investigate|analyze|resolve)\b/i, importance: 2, label: "user-task" },
623
+ { pattern: /\b(prefer|always use|convention is|pattern is)\b/i, importance: 2, label: "user-preference" },
624
+ { pattern: /\b(implement|build|create|add feature|integrate)\b/i, importance: 2, label: "user-feature" }
612
625
  ];
613
626
  var MAX_VALUE_LENGTH = 500;
614
627
  var MIN_INPUT_LENGTH = 20;
@@ -717,13 +730,15 @@ function extractEntities(hook, promptNumber = 0) {
717
730
  case "Agent": {
718
731
  const subagentType = stringField2(tool_input, "subagent_type") ?? "general-purpose";
719
732
  const prompt = stringField2(tool_input, "prompt") ?? "";
720
- raw.push(makeEntity(session_id, project, tool_name, "decision", `agent:${subagentType}: ${prompt.slice(0, 100)}`, 3, now, promptNumber));
733
+ const agentResult = extractAgentResult(tool_response);
734
+ raw.push(makeEntity(session_id, project, tool_name, "decision", `agent:${subagentType}: ${prompt.slice(0, 200)}`, 3, now, promptNumber, agentResult || undefined));
721
735
  break;
722
736
  }
723
737
  case "Skill": {
724
738
  const skillName = stringField2(tool_input, "skill") ?? "unknown";
725
739
  const args = stringField2(tool_input, "args") ?? "";
726
- raw.push(makeEntity(session_id, project, tool_name, "decision", `skill:${skillName} ${args.slice(0, 80)}`.trim(), 2, now, promptNumber));
740
+ const skillResult = extractAgentResult(tool_response);
741
+ raw.push(makeEntity(session_id, project, tool_name, "decision", `skill:${skillName} ${args.slice(0, 120)}`.trim(), 2, now, promptNumber, skillResult || undefined));
727
742
  break;
728
743
  }
729
744
  default:
@@ -754,6 +769,15 @@ function stringField2(obj, key) {
754
769
  function deriveProject(hook) {
755
770
  return "unknown";
756
771
  }
772
+ function extractAgentResult(response) {
773
+ if (!response)
774
+ return;
775
+ const r = response;
776
+ const text = typeof r === "string" ? r : stringField2(r, "result") ?? stringField2(r, "output") ?? stringField2(r, "content") ?? stringField2(r, "text");
777
+ if (!text)
778
+ return;
779
+ return text.length > 800 ? text.slice(0, 797) + "..." : text;
780
+ }
757
781
  function extractFileFromBashCmd(cmd) {
758
782
  const patterns = [
759
783
  /(?:cp|mv)\s+\S+\s+(\S+\.[\w]+)/,
@@ -1777,37 +1801,46 @@ function projectFromCwd(cwd) {
1777
1801
  }
1778
1802
 
1779
1803
  // src/summarizer/summarizer-prompts.ts
1804
+ function stripNoiseTags(text) {
1805
+ 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();
1806
+ }
1780
1807
  function buildRuleBasedSummary(session, files, errors, decisions, notes = []) {
1781
1808
  const parts = [];
1782
1809
  if (session.user_prompt) {
1783
- parts.push(`Task: ${session.user_prompt.slice(0, 200)}.`);
1810
+ const cleanPrompt = stripNoiseTags(session.user_prompt);
1811
+ if (cleanPrompt) {
1812
+ parts.push(`Task: ${cleanPrompt.slice(0, 500)}.`);
1813
+ }
1784
1814
  }
1785
1815
  if (files.length > 0) {
1786
- const listed = files.slice(0, 10).join(", ");
1787
- parts.push(`Files (${files.length}): ${listed}${files.length > 10 ? ` (+${files.length - 10} more)` : ""}.`);
1816
+ const listed = files.slice(0, 15).join(", ");
1817
+ parts.push(`Files (${files.length}): ${listed}${files.length > 15 ? ` (+${files.length - 15} more)` : ""}.`);
1788
1818
  }
1789
1819
  if (decisions.length > 0) {
1790
- const listed = decisions.slice(0, 3).map((d) => d.entity_value.slice(0, 100)).join("; ");
1820
+ const listed = decisions.slice(0, 5).map((d) => {
1821
+ const base = d.entity_value.slice(0, 150);
1822
+ const ctx = d.context ? ` \u2192 ${d.context.slice(0, 200)}` : "";
1823
+ return base + ctx;
1824
+ }).join("; ");
1791
1825
  parts.push(`Decisions: ${listed}.`);
1792
1826
  }
1793
1827
  if (errors.length > 0) {
1794
- const first = errors[0];
1795
- const ctx = first.context ? ` (${first.context.slice(0, 60)})` : "";
1796
- parts.push(`Errors (${errors.length}): ${first.entity_value.slice(0, 100)}${ctx}.`);
1797
- if (errors.length > 1) {
1798
- parts.push(`Also: ${errors[1].entity_value.slice(0, 80)}.`);
1799
- }
1828
+ const errorLines = errors.slice(0, 5).map((e) => {
1829
+ const ctx = e.context ? ` (${e.context.slice(0, 100)})` : "";
1830
+ return `${e.entity_value.slice(0, 150)}${ctx}`;
1831
+ });
1832
+ parts.push(`Errors (${errors.length}): ${errorLines.join("; ")}.`);
1800
1833
  }
1801
1834
  if (notes.length > 0) {
1802
- parts.push(`Notes: ${notes.slice(-2).join("; ").slice(0, 200)}.`);
1835
+ parts.push(`Notes: ${notes.slice(-5).join("; ").slice(0, 500)}.`);
1803
1836
  }
1804
1837
  return parts.join(" ") || `Session in project ${session.project}.`;
1805
1838
  }
1806
1839
 
1807
1840
  // src/summarizer/cli-summarizer.ts
1808
1841
  var log5 = createLogger("cli-summarizer");
1809
- var MAX_PROMPT_CHARS = 4000;
1810
- var MAX_OUTPUT_CHARS = 1000;
1842
+ var MAX_PROMPT_CHARS = 6000;
1843
+ var MAX_OUTPUT_CHARS = 2000;
1811
1844
  var DEFAULT_TIMEOUT_MS = 30000;
1812
1845
  var _cliAvailable;
1813
1846
  var _cliCheckedAt = 0;
@@ -1827,27 +1860,30 @@ function isClaudeCliAvailable() {
1827
1860
  _cliCheckedAt = Date.now();
1828
1861
  return _cliAvailable;
1829
1862
  }
1863
+ function stripNoise(text) {
1864
+ 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();
1865
+ }
1830
1866
  function buildCliPrompt(ctx) {
1831
1867
  const sections = [
1832
- "Summarize this coding session in 2-3 plain sentences. No markdown, no headers, no code blocks.",
1833
- "Focus on: what was accomplished, key decisions, important findings.",
1868
+ "Summarize this coding session in 3-5 plain sentences. No markdown, no headers, no code blocks.",
1869
+ "Focus on: what was accomplished, key decisions, important findings, tools/agents used.",
1834
1870
  "",
1835
1871
  `Project: ${ctx.project}`
1836
1872
  ];
1837
1873
  if (ctx.files.length > 0) {
1838
- sections.push(`Files modified: ${ctx.files.slice(0, 10).join(", ")}`);
1874
+ sections.push(`Files modified: ${ctx.files.slice(0, 15).join(", ")}`);
1839
1875
  }
1840
1876
  if (ctx.errors.length > 0) {
1841
- sections.push(`Errors resolved: ${ctx.errors.slice(0, 5).join("; ")}`);
1877
+ sections.push(`Errors resolved: ${ctx.errors.slice(0, 5).map(stripNoise).join("; ")}`);
1842
1878
  }
1843
1879
  if (ctx.decisions.length > 0) {
1844
- sections.push(`Decisions: ${ctx.decisions.slice(0, 5).join("; ")}`);
1880
+ sections.push(`Decisions: ${ctx.decisions.slice(0, 8).map(stripNoise).join("; ")}`);
1845
1881
  }
1846
1882
  if (ctx.notes.length > 0) {
1847
- sections.push(`Notes: ${ctx.notes.slice(0, 3).join("; ")}`);
1883
+ sections.push(`Notes: ${ctx.notes.slice(0, 5).map(stripNoise).join("; ")}`);
1848
1884
  }
1849
1885
  if (ctx.observations.length > 0) {
1850
- sections.push(`Key observations: ${ctx.observations.slice(0, 3).join("; ")}`);
1886
+ sections.push(`Key observations: ${ctx.observations.slice(0, 5).map(stripNoise).join("; ")}`);
1851
1887
  }
1852
1888
  let prompt = sections.join(`
1853
1889
  `);
@@ -1948,19 +1984,23 @@ class SessionSummarizer {
1948
1984
  const hasModified = this.sessionStore.hasModifiedFiles(session_id);
1949
1985
  if (!hasModified && errors.length === 0 && decisions.length === 0 && notes.length === 0 && observations.length === 0)
1950
1986
  return;
1951
- const obsValues = observations.slice(0, 5).map((o) => o.entity_value);
1987
+ const obsValues = observations.slice(0, 8).map((o) => o.entity_value);
1952
1988
  let summaryText;
1953
1989
  let tier = "rule-based";
1954
1990
  const llmMode = process.env["CLAUDE_MEMORY_HUB_LLM"] ?? "auto";
1955
1991
  if (llmMode !== "rule-based") {
1992
+ const decisionDetails = decisions.slice(0, 8).map((d) => {
1993
+ const ctx2 = d.context ? ` \u2192 ${d.context.slice(0, 200)}` : "";
1994
+ return d.entity_value.slice(0, 150) + ctx2;
1995
+ });
1956
1996
  const ctx = {
1957
1997
  sessionId: session_id,
1958
1998
  project,
1959
1999
  files,
1960
- errors: errors.slice(0, 5).map((e) => e.entity_value.slice(0, 100)),
1961
- decisions: decisions.slice(0, 5).map((d) => d.entity_value.slice(0, 100)),
1962
- notes: notes.slice(0, 3),
1963
- observations: obsValues.slice(0, 3)
2000
+ errors: errors.slice(0, 5).map((e) => e.entity_value.slice(0, 150)),
2001
+ decisions: decisionDetails,
2002
+ notes: notes.slice(0, 5),
2003
+ observations: obsValues.slice(0, 5)
1964
2004
  };
1965
2005
  summaryText = await tryCliSummary(ctx);
1966
2006
  if (summaryText)
@@ -376,6 +376,11 @@ class SessionStore {
376
376
  this.db.run("UPDATE sessions SET status = 'completed', ended_at = ? WHERE id = ?", [Date.now(), id]);
377
377
  }
378
378
  insertEntity(entity) {
379
+ if (entity.entity_type === "decision" || entity.entity_type === "observation") {
380
+ const existing = this.db.query("SELECT COUNT(*) as c FROM entities WHERE session_id = ? AND entity_type = ? AND entity_value = ?").get(entity.session_id, entity.entity_type, entity.entity_value);
381
+ if (existing && existing.c > 0)
382
+ return -1;
383
+ }
379
384
  const result = this.db.run(`INSERT INTO entities(session_id, project, tool_name, entity_type, entity_value, context, importance, created_at, prompt_number)
380
385
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
381
386
  entity.session_id,
@@ -597,18 +602,26 @@ function extractCodePatterns(content) {
597
602
  var TOOL_OUTPUT_HEURISTICS = [
598
603
  { pattern: /\b(IMPORTANT|CRITICAL|WARNING|BREAKING)\b/i, importance: 4, label: "important" },
599
604
  { pattern: /\b(DEPRECATED|SECURITY|VULNERABILITY)\b/i, importance: 4, label: "security" },
605
+ { pattern: /\b(migration failed|data loss|corrupt)/i, importance: 4, label: "data-risk" },
600
606
  { pattern: /\b(decision:|decided to|NOTE:|conclusion:)/i, importance: 3, label: "decision-note" },
601
607
  { pattern: /\b(discovered|found that|learned|realized|root cause)\b/i, importance: 3, label: "discovery" },
602
608
  { pattern: /\b(workaround:|alternative:|instead of|switched to)/i, importance: 3, label: "approach-change" },
609
+ { pattern: /\b(refactored?|migrated?|upgraded?|replaced)\b/i, importance: 3, label: "refactor" },
610
+ { pattern: /\b(installed|added dependency|npm install|bun add)\b/i, importance: 2, label: "dependency" },
603
611
  { pattern: /\b(TODO:|FIXME:|HACK:|WORKAROUND:)/i, importance: 2, label: "todo-note" },
604
612
  { pattern: /\b(performance:|bottleneck|slow|timeout|OOM)/i, importance: 2, label: "performance" },
613
+ { pattern: /\b(created|scaffolded|initialized|bootstrapped)\b/i, importance: 2, label: "creation" },
614
+ { pattern: /\b(tests? (?:pass|fail)|coverage|assertion)/i, importance: 2, label: "test-result" },
615
+ { pattern: /\b(deployed|published|released|pushed to)\b/i, importance: 2, label: "deployment" },
605
616
  { pattern: /^>\s+.{10,}/m, importance: 2, label: "quoted" }
606
617
  ];
607
618
  var PROMPT_HEURISTICS = [
608
619
  { pattern: /\b(IMPORTANT|CRITICAL|MUST)\b/i, importance: 4, label: "user-important" },
609
620
  { pattern: /\b(remember that|note that|I decided|we should|keep in mind)\b/i, importance: 3, label: "user-note" },
610
621
  { pattern: /\b(don't|do not|never|avoid|stop)\b/i, importance: 3, label: "user-constraint" },
611
- { pattern: /\b(prefer|always use|convention is|pattern is)\b/i, importance: 2, label: "user-preference" }
622
+ { pattern: /\b(fix|debug|investigate|analyze|resolve)\b/i, importance: 2, label: "user-task" },
623
+ { pattern: /\b(prefer|always use|convention is|pattern is)\b/i, importance: 2, label: "user-preference" },
624
+ { pattern: /\b(implement|build|create|add feature|integrate)\b/i, importance: 2, label: "user-feature" }
612
625
  ];
613
626
  var MAX_VALUE_LENGTH = 500;
614
627
  var MIN_INPUT_LENGTH = 20;
@@ -717,13 +730,15 @@ function extractEntities(hook, promptNumber = 0) {
717
730
  case "Agent": {
718
731
  const subagentType = stringField2(tool_input, "subagent_type") ?? "general-purpose";
719
732
  const prompt = stringField2(tool_input, "prompt") ?? "";
720
- raw.push(makeEntity(session_id, project, tool_name, "decision", `agent:${subagentType}: ${prompt.slice(0, 100)}`, 3, now, promptNumber));
733
+ const agentResult = extractAgentResult(tool_response);
734
+ raw.push(makeEntity(session_id, project, tool_name, "decision", `agent:${subagentType}: ${prompt.slice(0, 200)}`, 3, now, promptNumber, agentResult || undefined));
721
735
  break;
722
736
  }
723
737
  case "Skill": {
724
738
  const skillName = stringField2(tool_input, "skill") ?? "unknown";
725
739
  const args = stringField2(tool_input, "args") ?? "";
726
- raw.push(makeEntity(session_id, project, tool_name, "decision", `skill:${skillName} ${args.slice(0, 80)}`.trim(), 2, now, promptNumber));
740
+ const skillResult = extractAgentResult(tool_response);
741
+ raw.push(makeEntity(session_id, project, tool_name, "decision", `skill:${skillName} ${args.slice(0, 120)}`.trim(), 2, now, promptNumber, skillResult || undefined));
727
742
  break;
728
743
  }
729
744
  default:
@@ -754,6 +769,15 @@ function stringField2(obj, key) {
754
769
  function deriveProject(hook) {
755
770
  return "unknown";
756
771
  }
772
+ function extractAgentResult(response) {
773
+ if (!response)
774
+ return;
775
+ const r = response;
776
+ const text = typeof r === "string" ? r : stringField2(r, "result") ?? stringField2(r, "output") ?? stringField2(r, "content") ?? stringField2(r, "text");
777
+ if (!text)
778
+ return;
779
+ return text.length > 800 ? text.slice(0, 797) + "..." : text;
780
+ }
757
781
  function extractFileFromBashCmd(cmd) {
758
782
  const patterns = [
759
783
  /(?:cp|mv)\s+\S+\s+(\S+\.[\w]+)/,
package/dist/index.js CHANGED
@@ -14140,6 +14140,11 @@ class SessionStore {
14140
14140
  this.db.run("UPDATE sessions SET status = 'completed', ended_at = ? WHERE id = ?", [Date.now(), id]);
14141
14141
  }
14142
14142
  insertEntity(entity) {
14143
+ if (entity.entity_type === "decision" || entity.entity_type === "observation") {
14144
+ const existing = this.db.query("SELECT COUNT(*) as c FROM entities WHERE session_id = ? AND entity_type = ? AND entity_value = ?").get(entity.session_id, entity.entity_type, entity.entity_value);
14145
+ if (existing && existing.c > 0)
14146
+ return -1;
14147
+ }
14143
14148
  const result = this.db.run(`INSERT INTO entities(session_id, project, tool_name, entity_type, entity_value, context, importance, created_at, prompt_number)
14144
14149
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
14145
14150
  entity.session_id,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-memory-hub",
3
- "version": "0.9.4",
3
+ "version": "0.9.6",
4
4
  "description": "Persistent memory system for Claude Code. Zero API key. Zero Python. 5 hooks + MCP server + SQLite FTS5 + semantic search.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",