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 +19 -4
- package/README.md +3 -0
- package/dist/hooks/post-compact.js +74 -22
- package/dist/hooks/post-tool-use.js +75 -23
- package/dist/hooks/pre-compact.js +74 -22
- package/dist/hooks/session-end.js +75 -23
- package/dist/hooks/user-prompt-submit.js +74 -22
- package/package.json +1 -1
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
|
+
## [0.9.0] - 2026-04-02
|
|
9
9
|
|
|
10
|
-
|
|
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)
|
|
15
|
-
- **Proactive retrieval cap doubled** — `MAX_INJECTION_CHARS` increased from 1,500 (~375 tokens) to 3,000 (~750 tokens)
|
|
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
|
|
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
|
-
|
|
1849
|
+
fullLines.push(`- ${basename2(dirname(e.path))}/${basename2(e.path)} (~${e.tokenCost} tok): ${headings || "no sections"}`);
|
|
1839
1850
|
}
|
|
1840
|
-
return
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
1556
|
+
fullLines.push(`- ${basename2(dirname(e.path))}/${basename2(e.path)} (~${e.tokenCost} tok): ${headings || "no sections"}`);
|
|
1546
1557
|
}
|
|
1547
|
-
return
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
|
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
|
-
|
|
1849
|
+
fullLines.push(`- ${basename2(dirname(e.path))}/${basename2(e.path)} (~${e.tokenCost} tok): ${headings || "no sections"}`);
|
|
1839
1850
|
}
|
|
1840
|
-
return
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
1556
|
+
fullLines.push(`- ${basename2(dirname(e.path))}/${basename2(e.path)} (~${e.tokenCost} tok): ${headings || "no sections"}`);
|
|
1546
1557
|
}
|
|
1547
|
-
return
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
|
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
|
-
|
|
1556
|
+
fullLines.push(`- ${basename2(dirname(e.path))}/${basename2(e.path)} (~${e.tokenCost} tok): ${headings || "no sections"}`);
|
|
1546
1557
|
}
|
|
1547
|
-
return
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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