claude-memory-hub 0.8.2 → 0.9.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,14 +5,29 @@ Format follows [Keep a Changelog](https://keepachangelog.com/).
5
5
 
6
6
  ---
7
7
 
8
- ## [0.8.2] - 2026-04-02
8
+ ## [0.9.0] - 2026-04-02
9
9
 
10
- Increased context injection limits for richer cross-session memory.
10
+ Smart context budget allocation memory never gets pushed out by lower-priority content.
11
+
12
+ ### Smart Budget Allocation (breaking change in injection behavior)
13
+
14
+ - **Priority-based `fitWithinBudget()`** — replaces naive sequential concatenation. Four content sections now compete for 8,000 chars with explicit priorities: P1 memory (min 500 chars) > P2 CLAUDE.md (min 200 chars) > P3 resource advice (droppable) > P4 overhead warning (droppable). When total fits, everything is kept. When over budget, lowest priority sections are shrunk or dropped first
15
+ - **Memory context guaranteed** — past session context always gets first claim on budget. Previously could be truncated when CLAUDE.md + advice consumed too much space
16
+
17
+ ### CLAUDE.md Adaptive Compression
18
+
19
+ - **3-level `formatForInjection()`** — CLAUDE.md summary now adapts to available budget: Level 3 (full: headings + token cost, >500 chars), Level 2 (compact: file + token cost, >200 chars), Level 1 (minimal: file names only, <200 chars). Previously always used full format regardless of remaining space
20
+
21
+ ### Overhead Warning Injection
22
+
23
+ - **Auto-inject warning when unused resources > 10K tokens** — UserPromptSubmit hook now analyzes `OverheadReport` and injects a one-line note if unused skills/agents exceed 10,000 listing tokens. Points user to `memory_context_budget` tool for details. Zero-cost when overhead is acceptable
11
24
 
12
25
  ### Context Injection Limits
13
26
 
14
- - **UserPromptSubmit cap doubled** — `MAX_CHARS` increased from 4,500 (~1,125 tokens) to 8,000 (~2,000 tokens). Session-start context injection now carries significantly more past knowledge
15
- - **Proactive retrieval cap doubled** — `MAX_INJECTION_CHARS` increased from 1,500 (~375 tokens) to 3,000 (~750 tokens). Mid-session topic-shift injections now include fuller context from L3
27
+ - **UserPromptSubmit cap doubled** — `MAX_CHARS` increased from 4,500 (~1,125 tokens) to 8,000 (~2,000 tokens)
28
+ - **Proactive retrieval cap doubled** — `MAX_INJECTION_CHARS` increased from 1,500 (~375 tokens) to 3,000 (~750 tokens)
29
+ - **Proactive summary slice increased** — per-result summary increased from 200 to 400 chars for richer mid-session context
30
+ - **Memory result summary increased** — session-start per-result summary increased from 300 to 400 chars
16
31
 
17
32
  ---
18
33
 
package/README.md CHANGED
@@ -74,6 +74,8 @@ Search: Keyword-only, no semantic ranking
74
74
  | Multi-agent memory sharing | -- | -- | **Yes (free)** |
75
75
  | Permission-aware (approved only) | -- | -- | **Yes** |
76
76
  | Data export/import (JSONL) | -- | -- | **Yes** |
77
+ | Smart budget allocation (priority-based) | -- | -- | **Yes** |
78
+ | Overhead warning (unused resources) | -- | -- | **Yes** |
77
79
  | Hook batching (3ms vs 75ms) | -- | -- | **Yes** |
78
80
  | Browser UI | -- | Yes | **Yes** |
79
81
  | Health monitoring + auto-cleanup | -- | -- | **Yes** |
@@ -412,6 +414,7 @@ Migration is idempotent — safe to run multiple times with zero duplicates.
412
414
  | **v0.7.0** | Honest resource analysis, semantic search scaling, batch embeddings, 14 observation patterns, DB auto-cleanup, summarizer retry |
413
415
  | **v0.8.0** | 91 unit tests (was 0%), L1 read-through cache, PostToolUse batch queue (75ms→3ms), JSONL export/import, data cleanup CLI, CI/CD auto-publish |
414
416
  | **v0.8.1** | Token-budget-aware MCP tools (`max_tokens`), proactive mid-session memory retrieval (topic-shift detection), session-end batch flush |
417
+ | **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 |
415
418
 
416
419
  See [CHANGELOG.md](CHANGELOG.md) for full details.
417
420
 
@@ -1829,15 +1829,26 @@ class ClaudeMdTracker {
1829
1829
  tokenCost: r.token_cost
1830
1830
  }));
1831
1831
  }
1832
- formatForInjection(entries) {
1832
+ formatForInjection(entries, maxChars) {
1833
1833
  if (entries.length === 0)
1834
1834
  return "";
1835
- const lines = ["**Active CLAUDE.md rules:**"];
1835
+ const minimal = `CLAUDE.md: ${entries.map((e) => basename2(dirname(e.path)) + "/" + basename2(e.path)).join(", ")}`;
1836
+ if (maxChars !== undefined && maxChars < 200)
1837
+ return minimal;
1838
+ const compactLines = ["**Active CLAUDE.md rules:**"];
1839
+ for (const e of entries) {
1840
+ compactLines.push(`- ${basename2(dirname(e.path))}/${basename2(e.path)} (~${e.tokenCost} tok)`);
1841
+ }
1842
+ const compact = compactLines.join(`
1843
+ `);
1844
+ if (maxChars !== undefined && maxChars < 500)
1845
+ return compact;
1846
+ const fullLines = ["**Active CLAUDE.md rules:**"];
1836
1847
  for (const e of entries) {
1837
1848
  const headings = e.sections.map((s) => s.heading).join(", ");
1838
- lines.push(`- ${basename2(dirname(e.path))}/${basename2(e.path)} (~${e.tokenCost} tok): ${headings || "no sections"}`);
1849
+ fullLines.push(`- ${basename2(dirname(e.path))}/${basename2(e.path)} (~${e.tokenCost} tok): ${headings || "no sections"}`);
1839
1850
  }
1840
- return lines.join(`
1851
+ return fullLines.join(`
1841
1852
  `);
1842
1853
  }
1843
1854
  computeHash(content) {
@@ -1955,36 +1966,77 @@ async function handleUserPromptSubmit(hook, project) {
1955
1966
  plan.recommendations = validator.filterAliveRecommendations(plan.recommendations);
1956
1967
  plan.skipped = validator.filterAliveRecommendations(plan.skipped);
1957
1968
  const advice = loader.formatContextAdvice(plan);
1958
- const lines = [];
1959
- if (results.length > 0) {
1960
- lines.push("**Past session context:**");
1961
- for (const r of results) {
1962
- const date = new Date(r.created_at).toLocaleDateString();
1963
- const files = safeJson3(r.files_touched, []);
1964
- lines.push(`- [${date}, ${r.project}] ${r.summary.slice(0, 300)}`);
1965
- if (files.length > 0)
1966
- lines.push(` Files: ${files.slice(0, 5).join(", ")}`);
1967
- }
1968
- }
1969
- if (advice)
1970
- lines.push("", advice);
1969
+ let mdSummary = "";
1971
1970
  if (hook.cwd) {
1972
1971
  try {
1973
1972
  const mdTracker = new ClaudeMdTracker;
1974
1973
  const mdEntries = mdTracker.scanAndUpdate(hook.cwd, project);
1975
- const mdSummary = mdTracker.formatForInjection(mdEntries);
1976
- if (mdSummary)
1977
- lines.push("", mdSummary);
1974
+ mdSummary = mdTracker.formatForInjection(mdEntries);
1978
1975
  const tracker = new ResourceTracker;
1979
1976
  for (const entry of mdEntries) {
1980
1977
  tracker.trackUsage(hook.session_id, project, "claude_md", entry.path, entry.tokenCost);
1981
1978
  }
1982
1979
  } catch {}
1983
1980
  }
1984
- const safeContext = validator.validate(lines.join(`
1985
- `));
1981
+ let overheadWarning = "";
1982
+ try {
1983
+ const overhead = await registry.getOverheadReport(project);
1984
+ const unusedTokens = overhead.potential_savings.if_remove_unused_skills + overhead.potential_savings.if_remove_unused_agents;
1985
+ if (unusedTokens > 1e4) {
1986
+ const unusedCount = overhead.usage_analysis.skills_never_used.length + overhead.usage_analysis.agents_never_used.length;
1987
+ overheadWarning = `Note: ${unusedCount} unused resources (~${unusedTokens} listing tok overhead). Run \`memory_context_budget\` for details.`;
1988
+ }
1989
+ } catch {}
1990
+ const memorySection = buildMemorySection(results);
1991
+ const safeContext = validator.validate(fitWithinBudget(memorySection, mdSummary, advice, overheadWarning));
1986
1992
  return { additionalContext: safeContext };
1987
1993
  }
1994
+ function fitWithinBudget(memoryText, mdText, adviceText, overheadText) {
1995
+ const MAX_CHARS2 = 8000;
1996
+ const sections = [
1997
+ { text: memoryText, priority: 1, minChars: 500 },
1998
+ { text: mdText, priority: 2, minChars: 200 },
1999
+ { text: adviceText, priority: 3, minChars: 0 },
2000
+ { text: overheadText, priority: 4, minChars: 0 }
2001
+ ].filter((s) => s.text.length > 0);
2002
+ const totalNeeded = sections.reduce((sum, s) => sum + s.text.length, 0);
2003
+ if (totalNeeded <= MAX_CHARS2) {
2004
+ return sections.map((s) => s.text).join(`
2005
+
2006
+ `);
2007
+ }
2008
+ let remaining = MAX_CHARS2;
2009
+ const allocated = [];
2010
+ sections.sort((a, b) => a.priority - b.priority);
2011
+ for (const section of sections) {
2012
+ if (remaining <= 0)
2013
+ break;
2014
+ if (section.text.length <= remaining) {
2015
+ allocated.push(section.text);
2016
+ remaining -= section.text.length + 2;
2017
+ } else if (remaining >= section.minChars) {
2018
+ allocated.push(section.text.slice(0, remaining));
2019
+ remaining = 0;
2020
+ }
2021
+ }
2022
+ return allocated.join(`
2023
+
2024
+ `);
2025
+ }
2026
+ function buildMemorySection(results) {
2027
+ if (results.length === 0)
2028
+ return "";
2029
+ const lines = ["**Past session context:**"];
2030
+ for (const r of results) {
2031
+ const date = new Date(r.created_at).toLocaleDateString();
2032
+ const files = safeJson3(r.files_touched, []);
2033
+ lines.push(`- [${date}, ${r.project}] ${r.summary.slice(0, 400)}`);
2034
+ if (files.length > 0)
2035
+ lines.push(` Files: ${files.slice(0, 5).join(", ")}`);
2036
+ }
2037
+ return lines.join(`
2038
+ `);
2039
+ }
1988
2040
  async function handleSessionEnd(hook, project) {
1989
2041
  const store = new SessionStore;
1990
2042
  store.completeSession(hook.session_id);
@@ -1536,15 +1536,26 @@ class ClaudeMdTracker {
1536
1536
  tokenCost: r.token_cost
1537
1537
  }));
1538
1538
  }
1539
- formatForInjection(entries) {
1539
+ formatForInjection(entries, maxChars) {
1540
1540
  if (entries.length === 0)
1541
1541
  return "";
1542
- const lines = ["**Active CLAUDE.md rules:**"];
1542
+ const minimal = `CLAUDE.md: ${entries.map((e) => basename2(dirname(e.path)) + "/" + basename2(e.path)).join(", ")}`;
1543
+ if (maxChars !== undefined && maxChars < 200)
1544
+ return minimal;
1545
+ const compactLines = ["**Active CLAUDE.md rules:**"];
1546
+ for (const e of entries) {
1547
+ compactLines.push(`- ${basename2(dirname(e.path))}/${basename2(e.path)} (~${e.tokenCost} tok)`);
1548
+ }
1549
+ const compact = compactLines.join(`
1550
+ `);
1551
+ if (maxChars !== undefined && maxChars < 500)
1552
+ return compact;
1553
+ const fullLines = ["**Active CLAUDE.md rules:**"];
1543
1554
  for (const e of entries) {
1544
1555
  const headings = e.sections.map((s) => s.heading).join(", ");
1545
- lines.push(`- ${basename2(dirname(e.path))}/${basename2(e.path)} (~${e.tokenCost} tok): ${headings || "no sections"}`);
1556
+ fullLines.push(`- ${basename2(dirname(e.path))}/${basename2(e.path)} (~${e.tokenCost} tok): ${headings || "no sections"}`);
1546
1557
  }
1547
- return lines.join(`
1558
+ return fullLines.join(`
1548
1559
  `);
1549
1560
  }
1550
1561
  computeHash(content) {
@@ -1662,36 +1673,77 @@ async function handleUserPromptSubmit(hook, project) {
1662
1673
  plan.recommendations = validator.filterAliveRecommendations(plan.recommendations);
1663
1674
  plan.skipped = validator.filterAliveRecommendations(plan.skipped);
1664
1675
  const advice = loader.formatContextAdvice(plan);
1665
- const lines = [];
1666
- if (results.length > 0) {
1667
- lines.push("**Past session context:**");
1668
- for (const r of results) {
1669
- const date = new Date(r.created_at).toLocaleDateString();
1670
- const files = safeJson3(r.files_touched, []);
1671
- lines.push(`- [${date}, ${r.project}] ${r.summary.slice(0, 300)}`);
1672
- if (files.length > 0)
1673
- lines.push(` Files: ${files.slice(0, 5).join(", ")}`);
1674
- }
1675
- }
1676
- if (advice)
1677
- lines.push("", advice);
1676
+ let mdSummary = "";
1678
1677
  if (hook.cwd) {
1679
1678
  try {
1680
1679
  const mdTracker = new ClaudeMdTracker;
1681
1680
  const mdEntries = mdTracker.scanAndUpdate(hook.cwd, project);
1682
- const mdSummary = mdTracker.formatForInjection(mdEntries);
1683
- if (mdSummary)
1684
- lines.push("", mdSummary);
1681
+ mdSummary = mdTracker.formatForInjection(mdEntries);
1685
1682
  const tracker = new ResourceTracker;
1686
1683
  for (const entry of mdEntries) {
1687
1684
  tracker.trackUsage(hook.session_id, project, "claude_md", entry.path, entry.tokenCost);
1688
1685
  }
1689
1686
  } catch {}
1690
1687
  }
1691
- const safeContext = validator.validate(lines.join(`
1692
- `));
1688
+ let overheadWarning = "";
1689
+ try {
1690
+ const overhead = await registry.getOverheadReport(project);
1691
+ const unusedTokens = overhead.potential_savings.if_remove_unused_skills + overhead.potential_savings.if_remove_unused_agents;
1692
+ if (unusedTokens > 1e4) {
1693
+ const unusedCount = overhead.usage_analysis.skills_never_used.length + overhead.usage_analysis.agents_never_used.length;
1694
+ overheadWarning = `Note: ${unusedCount} unused resources (~${unusedTokens} listing tok overhead). Run \`memory_context_budget\` for details.`;
1695
+ }
1696
+ } catch {}
1697
+ const memorySection = buildMemorySection(results);
1698
+ const safeContext = validator.validate(fitWithinBudget(memorySection, mdSummary, advice, overheadWarning));
1693
1699
  return { additionalContext: safeContext };
1694
1700
  }
1701
+ function fitWithinBudget(memoryText, mdText, adviceText, overheadText) {
1702
+ const MAX_CHARS2 = 8000;
1703
+ const sections = [
1704
+ { text: memoryText, priority: 1, minChars: 500 },
1705
+ { text: mdText, priority: 2, minChars: 200 },
1706
+ { text: adviceText, priority: 3, minChars: 0 },
1707
+ { text: overheadText, priority: 4, minChars: 0 }
1708
+ ].filter((s) => s.text.length > 0);
1709
+ const totalNeeded = sections.reduce((sum, s) => sum + s.text.length, 0);
1710
+ if (totalNeeded <= MAX_CHARS2) {
1711
+ return sections.map((s) => s.text).join(`
1712
+
1713
+ `);
1714
+ }
1715
+ let remaining = MAX_CHARS2;
1716
+ const allocated = [];
1717
+ sections.sort((a, b) => a.priority - b.priority);
1718
+ for (const section of sections) {
1719
+ if (remaining <= 0)
1720
+ break;
1721
+ if (section.text.length <= remaining) {
1722
+ allocated.push(section.text);
1723
+ remaining -= section.text.length + 2;
1724
+ } else if (remaining >= section.minChars) {
1725
+ allocated.push(section.text.slice(0, remaining));
1726
+ remaining = 0;
1727
+ }
1728
+ }
1729
+ return allocated.join(`
1730
+
1731
+ `);
1732
+ }
1733
+ function buildMemorySection(results) {
1734
+ if (results.length === 0)
1735
+ return "";
1736
+ const lines = ["**Past session context:**"];
1737
+ for (const r of results) {
1738
+ const date = new Date(r.created_at).toLocaleDateString();
1739
+ const files = safeJson3(r.files_touched, []);
1740
+ lines.push(`- [${date}, ${r.project}] ${r.summary.slice(0, 400)}`);
1741
+ if (files.length > 0)
1742
+ lines.push(` Files: ${files.slice(0, 5).join(", ")}`);
1743
+ }
1744
+ return lines.join(`
1745
+ `);
1746
+ }
1695
1747
  async function handleSessionEnd(hook, project) {
1696
1748
  const store = new SessionStore;
1697
1749
  store.completeSession(hook.session_id);
@@ -1869,7 +1921,7 @@ function evaluateProactiveInjection(sessionId, toolName, toolInput, toolResponse
1869
1921
  const lines = [`**Relevant past context** (topic: ${currentTopic}):`];
1870
1922
  for (const r of results) {
1871
1923
  const date = new Date(r.created_at).toLocaleDateString();
1872
- lines.push(`- [${date}] ${r.summary.slice(0, 200)}`);
1924
+ lines.push(`- [${date}] ${r.summary.slice(0, 400)}`);
1873
1925
  const files = safeJson4(r.files_touched, []);
1874
1926
  if (files.length > 0)
1875
1927
  lines.push(` Files: ${files.slice(0, 3).join(", ")}`);
@@ -1829,15 +1829,26 @@ class ClaudeMdTracker {
1829
1829
  tokenCost: r.token_cost
1830
1830
  }));
1831
1831
  }
1832
- formatForInjection(entries) {
1832
+ formatForInjection(entries, maxChars) {
1833
1833
  if (entries.length === 0)
1834
1834
  return "";
1835
- const lines = ["**Active CLAUDE.md rules:**"];
1835
+ const minimal = `CLAUDE.md: ${entries.map((e) => basename2(dirname(e.path)) + "/" + basename2(e.path)).join(", ")}`;
1836
+ if (maxChars !== undefined && maxChars < 200)
1837
+ return minimal;
1838
+ const compactLines = ["**Active CLAUDE.md rules:**"];
1839
+ for (const e of entries) {
1840
+ compactLines.push(`- ${basename2(dirname(e.path))}/${basename2(e.path)} (~${e.tokenCost} tok)`);
1841
+ }
1842
+ const compact = compactLines.join(`
1843
+ `);
1844
+ if (maxChars !== undefined && maxChars < 500)
1845
+ return compact;
1846
+ const fullLines = ["**Active CLAUDE.md rules:**"];
1836
1847
  for (const e of entries) {
1837
1848
  const headings = e.sections.map((s) => s.heading).join(", ");
1838
- lines.push(`- ${basename2(dirname(e.path))}/${basename2(e.path)} (~${e.tokenCost} tok): ${headings || "no sections"}`);
1849
+ fullLines.push(`- ${basename2(dirname(e.path))}/${basename2(e.path)} (~${e.tokenCost} tok): ${headings || "no sections"}`);
1839
1850
  }
1840
- return lines.join(`
1851
+ return fullLines.join(`
1841
1852
  `);
1842
1853
  }
1843
1854
  computeHash(content) {
@@ -1955,36 +1966,77 @@ async function handleUserPromptSubmit(hook, project) {
1955
1966
  plan.recommendations = validator.filterAliveRecommendations(plan.recommendations);
1956
1967
  plan.skipped = validator.filterAliveRecommendations(plan.skipped);
1957
1968
  const advice = loader.formatContextAdvice(plan);
1958
- const lines = [];
1959
- if (results.length > 0) {
1960
- lines.push("**Past session context:**");
1961
- for (const r of results) {
1962
- const date = new Date(r.created_at).toLocaleDateString();
1963
- const files = safeJson3(r.files_touched, []);
1964
- lines.push(`- [${date}, ${r.project}] ${r.summary.slice(0, 300)}`);
1965
- if (files.length > 0)
1966
- lines.push(` Files: ${files.slice(0, 5).join(", ")}`);
1967
- }
1968
- }
1969
- if (advice)
1970
- lines.push("", advice);
1969
+ let mdSummary = "";
1971
1970
  if (hook.cwd) {
1972
1971
  try {
1973
1972
  const mdTracker = new ClaudeMdTracker;
1974
1973
  const mdEntries = mdTracker.scanAndUpdate(hook.cwd, project);
1975
- const mdSummary = mdTracker.formatForInjection(mdEntries);
1976
- if (mdSummary)
1977
- lines.push("", mdSummary);
1974
+ mdSummary = mdTracker.formatForInjection(mdEntries);
1978
1975
  const tracker = new ResourceTracker;
1979
1976
  for (const entry of mdEntries) {
1980
1977
  tracker.trackUsage(hook.session_id, project, "claude_md", entry.path, entry.tokenCost);
1981
1978
  }
1982
1979
  } catch {}
1983
1980
  }
1984
- const safeContext = validator.validate(lines.join(`
1985
- `));
1981
+ let overheadWarning = "";
1982
+ try {
1983
+ const overhead = await registry.getOverheadReport(project);
1984
+ const unusedTokens = overhead.potential_savings.if_remove_unused_skills + overhead.potential_savings.if_remove_unused_agents;
1985
+ if (unusedTokens > 1e4) {
1986
+ const unusedCount = overhead.usage_analysis.skills_never_used.length + overhead.usage_analysis.agents_never_used.length;
1987
+ overheadWarning = `Note: ${unusedCount} unused resources (~${unusedTokens} listing tok overhead). Run \`memory_context_budget\` for details.`;
1988
+ }
1989
+ } catch {}
1990
+ const memorySection = buildMemorySection(results);
1991
+ const safeContext = validator.validate(fitWithinBudget(memorySection, mdSummary, advice, overheadWarning));
1986
1992
  return { additionalContext: safeContext };
1987
1993
  }
1994
+ function fitWithinBudget(memoryText, mdText, adviceText, overheadText) {
1995
+ const MAX_CHARS2 = 8000;
1996
+ const sections = [
1997
+ { text: memoryText, priority: 1, minChars: 500 },
1998
+ { text: mdText, priority: 2, minChars: 200 },
1999
+ { text: adviceText, priority: 3, minChars: 0 },
2000
+ { text: overheadText, priority: 4, minChars: 0 }
2001
+ ].filter((s) => s.text.length > 0);
2002
+ const totalNeeded = sections.reduce((sum, s) => sum + s.text.length, 0);
2003
+ if (totalNeeded <= MAX_CHARS2) {
2004
+ return sections.map((s) => s.text).join(`
2005
+
2006
+ `);
2007
+ }
2008
+ let remaining = MAX_CHARS2;
2009
+ const allocated = [];
2010
+ sections.sort((a, b) => a.priority - b.priority);
2011
+ for (const section of sections) {
2012
+ if (remaining <= 0)
2013
+ break;
2014
+ if (section.text.length <= remaining) {
2015
+ allocated.push(section.text);
2016
+ remaining -= section.text.length + 2;
2017
+ } else if (remaining >= section.minChars) {
2018
+ allocated.push(section.text.slice(0, remaining));
2019
+ remaining = 0;
2020
+ }
2021
+ }
2022
+ return allocated.join(`
2023
+
2024
+ `);
2025
+ }
2026
+ function buildMemorySection(results) {
2027
+ if (results.length === 0)
2028
+ return "";
2029
+ const lines = ["**Past session context:**"];
2030
+ for (const r of results) {
2031
+ const date = new Date(r.created_at).toLocaleDateString();
2032
+ const files = safeJson3(r.files_touched, []);
2033
+ lines.push(`- [${date}, ${r.project}] ${r.summary.slice(0, 400)}`);
2034
+ if (files.length > 0)
2035
+ lines.push(` Files: ${files.slice(0, 5).join(", ")}`);
2036
+ }
2037
+ return lines.join(`
2038
+ `);
2039
+ }
1988
2040
  async function handleSessionEnd(hook, project) {
1989
2041
  const store = new SessionStore;
1990
2042
  store.completeSession(hook.session_id);
@@ -1536,15 +1536,26 @@ class ClaudeMdTracker {
1536
1536
  tokenCost: r.token_cost
1537
1537
  }));
1538
1538
  }
1539
- formatForInjection(entries) {
1539
+ formatForInjection(entries, maxChars) {
1540
1540
  if (entries.length === 0)
1541
1541
  return "";
1542
- const lines = ["**Active CLAUDE.md rules:**"];
1542
+ const minimal = `CLAUDE.md: ${entries.map((e) => basename2(dirname(e.path)) + "/" + basename2(e.path)).join(", ")}`;
1543
+ if (maxChars !== undefined && maxChars < 200)
1544
+ return minimal;
1545
+ const compactLines = ["**Active CLAUDE.md rules:**"];
1546
+ for (const e of entries) {
1547
+ compactLines.push(`- ${basename2(dirname(e.path))}/${basename2(e.path)} (~${e.tokenCost} tok)`);
1548
+ }
1549
+ const compact = compactLines.join(`
1550
+ `);
1551
+ if (maxChars !== undefined && maxChars < 500)
1552
+ return compact;
1553
+ const fullLines = ["**Active CLAUDE.md rules:**"];
1543
1554
  for (const e of entries) {
1544
1555
  const headings = e.sections.map((s) => s.heading).join(", ");
1545
- lines.push(`- ${basename2(dirname(e.path))}/${basename2(e.path)} (~${e.tokenCost} tok): ${headings || "no sections"}`);
1556
+ fullLines.push(`- ${basename2(dirname(e.path))}/${basename2(e.path)} (~${e.tokenCost} tok): ${headings || "no sections"}`);
1546
1557
  }
1547
- return lines.join(`
1558
+ return fullLines.join(`
1548
1559
  `);
1549
1560
  }
1550
1561
  computeHash(content) {
@@ -1662,36 +1673,77 @@ async function handleUserPromptSubmit(hook, project) {
1662
1673
  plan.recommendations = validator.filterAliveRecommendations(plan.recommendations);
1663
1674
  plan.skipped = validator.filterAliveRecommendations(plan.skipped);
1664
1675
  const advice = loader.formatContextAdvice(plan);
1665
- const lines = [];
1666
- if (results.length > 0) {
1667
- lines.push("**Past session context:**");
1668
- for (const r of results) {
1669
- const date = new Date(r.created_at).toLocaleDateString();
1670
- const files = safeJson3(r.files_touched, []);
1671
- lines.push(`- [${date}, ${r.project}] ${r.summary.slice(0, 300)}`);
1672
- if (files.length > 0)
1673
- lines.push(` Files: ${files.slice(0, 5).join(", ")}`);
1674
- }
1675
- }
1676
- if (advice)
1677
- lines.push("", advice);
1676
+ let mdSummary = "";
1678
1677
  if (hook.cwd) {
1679
1678
  try {
1680
1679
  const mdTracker = new ClaudeMdTracker;
1681
1680
  const mdEntries = mdTracker.scanAndUpdate(hook.cwd, project);
1682
- const mdSummary = mdTracker.formatForInjection(mdEntries);
1683
- if (mdSummary)
1684
- lines.push("", mdSummary);
1681
+ mdSummary = mdTracker.formatForInjection(mdEntries);
1685
1682
  const tracker = new ResourceTracker;
1686
1683
  for (const entry of mdEntries) {
1687
1684
  tracker.trackUsage(hook.session_id, project, "claude_md", entry.path, entry.tokenCost);
1688
1685
  }
1689
1686
  } catch {}
1690
1687
  }
1691
- const safeContext = validator.validate(lines.join(`
1692
- `));
1688
+ let overheadWarning = "";
1689
+ try {
1690
+ const overhead = await registry.getOverheadReport(project);
1691
+ const unusedTokens = overhead.potential_savings.if_remove_unused_skills + overhead.potential_savings.if_remove_unused_agents;
1692
+ if (unusedTokens > 1e4) {
1693
+ const unusedCount = overhead.usage_analysis.skills_never_used.length + overhead.usage_analysis.agents_never_used.length;
1694
+ overheadWarning = `Note: ${unusedCount} unused resources (~${unusedTokens} listing tok overhead). Run \`memory_context_budget\` for details.`;
1695
+ }
1696
+ } catch {}
1697
+ const memorySection = buildMemorySection(results);
1698
+ const safeContext = validator.validate(fitWithinBudget(memorySection, mdSummary, advice, overheadWarning));
1693
1699
  return { additionalContext: safeContext };
1694
1700
  }
1701
+ function fitWithinBudget(memoryText, mdText, adviceText, overheadText) {
1702
+ const MAX_CHARS2 = 8000;
1703
+ const sections = [
1704
+ { text: memoryText, priority: 1, minChars: 500 },
1705
+ { text: mdText, priority: 2, minChars: 200 },
1706
+ { text: adviceText, priority: 3, minChars: 0 },
1707
+ { text: overheadText, priority: 4, minChars: 0 }
1708
+ ].filter((s) => s.text.length > 0);
1709
+ const totalNeeded = sections.reduce((sum, s) => sum + s.text.length, 0);
1710
+ if (totalNeeded <= MAX_CHARS2) {
1711
+ return sections.map((s) => s.text).join(`
1712
+
1713
+ `);
1714
+ }
1715
+ let remaining = MAX_CHARS2;
1716
+ const allocated = [];
1717
+ sections.sort((a, b) => a.priority - b.priority);
1718
+ for (const section of sections) {
1719
+ if (remaining <= 0)
1720
+ break;
1721
+ if (section.text.length <= remaining) {
1722
+ allocated.push(section.text);
1723
+ remaining -= section.text.length + 2;
1724
+ } else if (remaining >= section.minChars) {
1725
+ allocated.push(section.text.slice(0, remaining));
1726
+ remaining = 0;
1727
+ }
1728
+ }
1729
+ return allocated.join(`
1730
+
1731
+ `);
1732
+ }
1733
+ function buildMemorySection(results) {
1734
+ if (results.length === 0)
1735
+ return "";
1736
+ const lines = ["**Past session context:**"];
1737
+ for (const r of results) {
1738
+ const date = new Date(r.created_at).toLocaleDateString();
1739
+ const files = safeJson3(r.files_touched, []);
1740
+ lines.push(`- [${date}, ${r.project}] ${r.summary.slice(0, 400)}`);
1741
+ if (files.length > 0)
1742
+ lines.push(` Files: ${files.slice(0, 5).join(", ")}`);
1743
+ }
1744
+ return lines.join(`
1745
+ `);
1746
+ }
1695
1747
  async function handleSessionEnd(hook, project) {
1696
1748
  const store = new SessionStore;
1697
1749
  store.completeSession(hook.session_id);
@@ -2057,7 +2109,7 @@ function evaluateProactiveInjection(sessionId, toolName, toolInput, toolResponse
2057
2109
  const lines = [`**Relevant past context** (topic: ${currentTopic}):`];
2058
2110
  for (const r of results) {
2059
2111
  const date = new Date(r.created_at).toLocaleDateString();
2060
- lines.push(`- [${date}] ${r.summary.slice(0, 200)}`);
2112
+ lines.push(`- [${date}] ${r.summary.slice(0, 400)}`);
2061
2113
  const files = safeJson4(r.files_touched, []);
2062
2114
  if (files.length > 0)
2063
2115
  lines.push(` Files: ${files.slice(0, 3).join(", ")}`);
@@ -1536,15 +1536,26 @@ class ClaudeMdTracker {
1536
1536
  tokenCost: r.token_cost
1537
1537
  }));
1538
1538
  }
1539
- formatForInjection(entries) {
1539
+ formatForInjection(entries, maxChars) {
1540
1540
  if (entries.length === 0)
1541
1541
  return "";
1542
- const lines = ["**Active CLAUDE.md rules:**"];
1542
+ const minimal = `CLAUDE.md: ${entries.map((e) => basename2(dirname(e.path)) + "/" + basename2(e.path)).join(", ")}`;
1543
+ if (maxChars !== undefined && maxChars < 200)
1544
+ return minimal;
1545
+ const compactLines = ["**Active CLAUDE.md rules:**"];
1546
+ for (const e of entries) {
1547
+ compactLines.push(`- ${basename2(dirname(e.path))}/${basename2(e.path)} (~${e.tokenCost} tok)`);
1548
+ }
1549
+ const compact = compactLines.join(`
1550
+ `);
1551
+ if (maxChars !== undefined && maxChars < 500)
1552
+ return compact;
1553
+ const fullLines = ["**Active CLAUDE.md rules:**"];
1543
1554
  for (const e of entries) {
1544
1555
  const headings = e.sections.map((s) => s.heading).join(", ");
1545
- lines.push(`- ${basename2(dirname(e.path))}/${basename2(e.path)} (~${e.tokenCost} tok): ${headings || "no sections"}`);
1556
+ fullLines.push(`- ${basename2(dirname(e.path))}/${basename2(e.path)} (~${e.tokenCost} tok): ${headings || "no sections"}`);
1546
1557
  }
1547
- return lines.join(`
1558
+ return fullLines.join(`
1548
1559
  `);
1549
1560
  }
1550
1561
  computeHash(content) {
@@ -1662,36 +1673,77 @@ async function handleUserPromptSubmit(hook, project) {
1662
1673
  plan.recommendations = validator.filterAliveRecommendations(plan.recommendations);
1663
1674
  plan.skipped = validator.filterAliveRecommendations(plan.skipped);
1664
1675
  const advice = loader.formatContextAdvice(plan);
1665
- const lines = [];
1666
- if (results.length > 0) {
1667
- lines.push("**Past session context:**");
1668
- for (const r of results) {
1669
- const date = new Date(r.created_at).toLocaleDateString();
1670
- const files = safeJson3(r.files_touched, []);
1671
- lines.push(`- [${date}, ${r.project}] ${r.summary.slice(0, 300)}`);
1672
- if (files.length > 0)
1673
- lines.push(` Files: ${files.slice(0, 5).join(", ")}`);
1674
- }
1675
- }
1676
- if (advice)
1677
- lines.push("", advice);
1676
+ let mdSummary = "";
1678
1677
  if (hook.cwd) {
1679
1678
  try {
1680
1679
  const mdTracker = new ClaudeMdTracker;
1681
1680
  const mdEntries = mdTracker.scanAndUpdate(hook.cwd, project);
1682
- const mdSummary = mdTracker.formatForInjection(mdEntries);
1683
- if (mdSummary)
1684
- lines.push("", mdSummary);
1681
+ mdSummary = mdTracker.formatForInjection(mdEntries);
1685
1682
  const tracker = new ResourceTracker;
1686
1683
  for (const entry of mdEntries) {
1687
1684
  tracker.trackUsage(hook.session_id, project, "claude_md", entry.path, entry.tokenCost);
1688
1685
  }
1689
1686
  } catch {}
1690
1687
  }
1691
- const safeContext = validator.validate(lines.join(`
1692
- `));
1688
+ let overheadWarning = "";
1689
+ try {
1690
+ const overhead = await registry.getOverheadReport(project);
1691
+ const unusedTokens = overhead.potential_savings.if_remove_unused_skills + overhead.potential_savings.if_remove_unused_agents;
1692
+ if (unusedTokens > 1e4) {
1693
+ const unusedCount = overhead.usage_analysis.skills_never_used.length + overhead.usage_analysis.agents_never_used.length;
1694
+ overheadWarning = `Note: ${unusedCount} unused resources (~${unusedTokens} listing tok overhead). Run \`memory_context_budget\` for details.`;
1695
+ }
1696
+ } catch {}
1697
+ const memorySection = buildMemorySection(results);
1698
+ const safeContext = validator.validate(fitWithinBudget(memorySection, mdSummary, advice, overheadWarning));
1693
1699
  return { additionalContext: safeContext };
1694
1700
  }
1701
+ function fitWithinBudget(memoryText, mdText, adviceText, overheadText) {
1702
+ const MAX_CHARS2 = 8000;
1703
+ const sections = [
1704
+ { text: memoryText, priority: 1, minChars: 500 },
1705
+ { text: mdText, priority: 2, minChars: 200 },
1706
+ { text: adviceText, priority: 3, minChars: 0 },
1707
+ { text: overheadText, priority: 4, minChars: 0 }
1708
+ ].filter((s) => s.text.length > 0);
1709
+ const totalNeeded = sections.reduce((sum, s) => sum + s.text.length, 0);
1710
+ if (totalNeeded <= MAX_CHARS2) {
1711
+ return sections.map((s) => s.text).join(`
1712
+
1713
+ `);
1714
+ }
1715
+ let remaining = MAX_CHARS2;
1716
+ const allocated = [];
1717
+ sections.sort((a, b) => a.priority - b.priority);
1718
+ for (const section of sections) {
1719
+ if (remaining <= 0)
1720
+ break;
1721
+ if (section.text.length <= remaining) {
1722
+ allocated.push(section.text);
1723
+ remaining -= section.text.length + 2;
1724
+ } else if (remaining >= section.minChars) {
1725
+ allocated.push(section.text.slice(0, remaining));
1726
+ remaining = 0;
1727
+ }
1728
+ }
1729
+ return allocated.join(`
1730
+
1731
+ `);
1732
+ }
1733
+ function buildMemorySection(results) {
1734
+ if (results.length === 0)
1735
+ return "";
1736
+ const lines = ["**Past session context:**"];
1737
+ for (const r of results) {
1738
+ const date = new Date(r.created_at).toLocaleDateString();
1739
+ const files = safeJson3(r.files_touched, []);
1740
+ lines.push(`- [${date}, ${r.project}] ${r.summary.slice(0, 400)}`);
1741
+ if (files.length > 0)
1742
+ lines.push(` Files: ${files.slice(0, 5).join(", ")}`);
1743
+ }
1744
+ return lines.join(`
1745
+ `);
1746
+ }
1695
1747
  async function handleSessionEnd(hook, project) {
1696
1748
  const store = new SessionStore;
1697
1749
  store.completeSession(hook.session_id);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-memory-hub",
3
- "version": "0.8.2",
3
+ "version": "0.9.0",
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",