clawmem 0.2.4 → 0.2.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/AGENTS.md +3 -0
- package/CLAUDE.md +3 -0
- package/SKILL.md +3 -0
- package/package.json +1 -1
- package/src/clawmem.ts +1 -1
- package/src/mcp.ts +178 -25
package/AGENTS.md
CHANGED
|
@@ -636,6 +636,9 @@ Symptom: "UserPromptSubmit hook error" on context-surfacing hook (intermittent)
|
|
|
636
636
|
→ Default hook timeout is 8s (since v0.1.1). If you have an older install, re-run
|
|
637
637
|
`clawmem setup hooks`. If persistent, restart the watcher: `systemctl --user restart
|
|
638
638
|
clawmem-watcher.service`. Healthy memory is under 100MB — if 400MB+, restart clears it.
|
|
639
|
+
→ v0.2.4 fix: hook's SQLite busy_timeout was 500ms — too tight. During A-MEM enrichment
|
|
640
|
+
or heavy indexing, watcher write locks exceed 500ms, causing SQLITE_BUSY. Raised to
|
|
641
|
+
5000ms (matches MCP server). Still completes within the 8s outer timeout.
|
|
639
642
|
|
|
640
643
|
Symptom: WSL hangs or becomes unresponsive during long sessions / watcher has 100K+ FDs
|
|
641
644
|
→ Pre-v0.2.3: fs.watch(recursive: true) registered inotify watches on EVERY subdirectory,
|
package/CLAUDE.md
CHANGED
|
@@ -636,6 +636,9 @@ Symptom: "UserPromptSubmit hook error" on context-surfacing hook (intermittent)
|
|
|
636
636
|
→ Default hook timeout is 8s (since v0.1.1). If you have an older install, re-run
|
|
637
637
|
`clawmem setup hooks`. If persistent, restart the watcher: `systemctl --user restart
|
|
638
638
|
clawmem-watcher.service`. Healthy memory is under 100MB — if 400MB+, restart clears it.
|
|
639
|
+
→ v0.2.4 fix: hook's SQLite busy_timeout was 500ms — too tight. During A-MEM enrichment
|
|
640
|
+
or heavy indexing, watcher write locks exceed 500ms, causing SQLITE_BUSY. Raised to
|
|
641
|
+
5000ms (matches MCP server). Still completes within the 8s outer timeout.
|
|
639
642
|
|
|
640
643
|
Symptom: WSL hangs or becomes unresponsive during long sessions / watcher has 100K+ FDs
|
|
641
644
|
→ Pre-v0.2.3: fs.watch(recursive: true) registered inotify watches on EVERY subdirectory,
|
package/SKILL.md
CHANGED
|
@@ -642,6 +642,9 @@ Symptom: "UserPromptSubmit hook error" on context-surfacing hook (intermittent)
|
|
|
642
642
|
-> Default hook timeout is 8s (since v0.1.1). If you have an older install, re-run
|
|
643
643
|
`clawmem setup hooks`. If persistent, restart the watcher: `systemctl --user restart
|
|
644
644
|
clawmem-watcher.service`. Healthy memory is under 100MB — if 400MB+, restart clears it.
|
|
645
|
+
-> v0.2.4 fix: hook's SQLite busy_timeout was 500ms — too tight. During A-MEM enrichment
|
|
646
|
+
or heavy indexing, watcher write locks exceed 500ms, causing SQLITE_BUSY. Raised to
|
|
647
|
+
5000ms (matches MCP server). Still completes within the 8s outer timeout.
|
|
645
648
|
|
|
646
649
|
Symptom: WSL hangs or becomes unresponsive during long sessions / watcher has 100K+ FDs
|
|
647
650
|
-> Pre-v0.2.3: fs.watch(recursive: true) registered inotify watches on EVERY subdirectory,
|
package/package.json
CHANGED
package/src/clawmem.ts
CHANGED
|
@@ -772,7 +772,7 @@ async function cmdSurface(args: string[]) {
|
|
|
772
772
|
if (!input) process.exit(0);
|
|
773
773
|
|
|
774
774
|
// Open store: writable for both (context-surfacing writes dedupe data)
|
|
775
|
-
const s = createStore(undefined, { busyTimeout:
|
|
775
|
+
const s = createStore(undefined, { busyTimeout: 5000 });
|
|
776
776
|
|
|
777
777
|
try {
|
|
778
778
|
if (isBootstrap) {
|
package/src/mcp.ts
CHANGED
|
@@ -817,6 +817,143 @@ This is the recommended entry point for ALL memory queries.`,
|
|
|
817
817
|
}
|
|
818
818
|
);
|
|
819
819
|
|
|
820
|
+
// ---------------------------------------------------------------------------
|
|
821
|
+
// Lifecycle search helpers — resilient candidate finding for pin/snooze/forget
|
|
822
|
+
// ---------------------------------------------------------------------------
|
|
823
|
+
|
|
824
|
+
type LifecycleCandidate = {
|
|
825
|
+
displayPath: string;
|
|
826
|
+
title: string;
|
|
827
|
+
score: number;
|
|
828
|
+
source: "path" | "fts" | "title" | "vec";
|
|
829
|
+
};
|
|
830
|
+
|
|
831
|
+
const STOPWORDS = new Set([
|
|
832
|
+
"a", "an", "the", "is", "are", "was", "were", "be", "been", "being",
|
|
833
|
+
"in", "on", "at", "to", "for", "of", "with", "by", "from", "as",
|
|
834
|
+
"and", "or", "not", "no", "but", "if", "then", "so", "do", "did",
|
|
835
|
+
"has", "have", "had", "it", "its", "this", "that", "my", "our",
|
|
836
|
+
]);
|
|
837
|
+
|
|
838
|
+
/**
|
|
839
|
+
* Cascading search for lifecycle mutations: path match → BM25 → title overlap → vector.
|
|
840
|
+
* Returns ranked candidates. Never returns wrong results silently.
|
|
841
|
+
*/
|
|
842
|
+
async function findMemoryCandidates(
|
|
843
|
+
store: Store,
|
|
844
|
+
query: string,
|
|
845
|
+
limit: number = 5
|
|
846
|
+
): Promise<LifecycleCandidate[]> {
|
|
847
|
+
// 1. Exact path match (handles queries like "stack/research/foo.md")
|
|
848
|
+
if (query.includes("/") || query.endsWith(".md")) {
|
|
849
|
+
const normalized = query.replace(/^\//, "");
|
|
850
|
+
const pathHits = store.db.prepare(`
|
|
851
|
+
SELECT collection || '/' || path as displayPath, title
|
|
852
|
+
FROM documents WHERE active = 1 AND invalidated_at IS NULL
|
|
853
|
+
AND (path LIKE ? OR collection || '/' || path LIKE ?)
|
|
854
|
+
LIMIT ?
|
|
855
|
+
`).all(`%${normalized}%`, `%${normalized}%`, limit) as { displayPath: string; title: string }[];
|
|
856
|
+
if (pathHits.length > 0) {
|
|
857
|
+
return pathHits.map((h, i) => ({ ...h, score: 1.0 - i * 0.05, source: "path" as const }));
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
// 2. BM25 full-text search (fast, exact terms)
|
|
862
|
+
const ftsResults = store.searchFTS(query, limit);
|
|
863
|
+
if (ftsResults.length > 0) {
|
|
864
|
+
return ftsResults.map(r => ({
|
|
865
|
+
displayPath: r.displayPath,
|
|
866
|
+
title: r.title,
|
|
867
|
+
score: r.score,
|
|
868
|
+
source: "fts" as const,
|
|
869
|
+
}));
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
// 3. Title-token overlap (catches BM25 failures from too many AND'd terms)
|
|
873
|
+
const tokens = query.toLowerCase().split(/\s+/)
|
|
874
|
+
.filter(w => w.length >= 2 && !STOPWORDS.has(w))
|
|
875
|
+
.map(w => w.replace(/[^a-z0-9]/g, ""))
|
|
876
|
+
.filter(w => w.length >= 2);
|
|
877
|
+
|
|
878
|
+
if (tokens.length > 0) {
|
|
879
|
+
const minMatch = Math.max(2, Math.ceil(tokens.length / 2));
|
|
880
|
+
const titleHits = store.db.prepare(`
|
|
881
|
+
SELECT collection || '/' || path as displayPath, title,
|
|
882
|
+
${tokens.map((_, i) => `(CASE WHEN LOWER(title) LIKE ? THEN 1 ELSE 0 END)`).join(" + ")} as match_count
|
|
883
|
+
FROM documents
|
|
884
|
+
WHERE active = 1 AND invalidated_at IS NULL
|
|
885
|
+
HAVING match_count >= ?
|
|
886
|
+
ORDER BY match_count DESC, modified_at DESC
|
|
887
|
+
LIMIT ?
|
|
888
|
+
`).all(...tokens.map(t => `%${t}%`), minMatch, limit) as { displayPath: string; title: string; match_count: number }[];
|
|
889
|
+
|
|
890
|
+
if (titleHits.length > 0) {
|
|
891
|
+
return titleHits.map(h => ({
|
|
892
|
+
displayPath: h.displayPath,
|
|
893
|
+
title: h.title,
|
|
894
|
+
score: h.match_count / tokens.length,
|
|
895
|
+
source: "title" as const,
|
|
896
|
+
}));
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
// 4. Vector search fallback (semantic similarity)
|
|
901
|
+
try {
|
|
902
|
+
const llm = getDefaultLlamaCpp();
|
|
903
|
+
if (llm) {
|
|
904
|
+
const vecResults = await store.searchVec(query, DEFAULT_EMBED_MODEL, limit);
|
|
905
|
+
if (vecResults.length > 0) {
|
|
906
|
+
return vecResults.map(r => ({
|
|
907
|
+
displayPath: r.displayPath,
|
|
908
|
+
title: r.title,
|
|
909
|
+
score: r.score,
|
|
910
|
+
source: "vec" as const,
|
|
911
|
+
}));
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
} catch {
|
|
915
|
+
// Vector search unavailable — degrade gracefully
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
return [];
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
/**
|
|
922
|
+
* Select a single target from candidates, or return an ambiguity message.
|
|
923
|
+
* Stricter confidence requirement for destructive ops (forget).
|
|
924
|
+
*/
|
|
925
|
+
function selectLifecycleTarget(
|
|
926
|
+
candidates: LifecycleCandidate[],
|
|
927
|
+
query: string,
|
|
928
|
+
destructive: boolean = false
|
|
929
|
+
): { target: LifecycleCandidate } | { ambiguous: string } | { notFound: string } {
|
|
930
|
+
if (candidates.length === 0) {
|
|
931
|
+
return { notFound: `No matching memory found for "${query}"` };
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
const top = candidates[0]!;
|
|
935
|
+
|
|
936
|
+
// Clear winner: high score OR significant gap to #2
|
|
937
|
+
const gap = candidates.length > 1 ? top.score - candidates[1]!.score : 1.0;
|
|
938
|
+
const confident = top.score >= 0.7 || gap >= 0.2;
|
|
939
|
+
|
|
940
|
+
// For destructive ops (forget), require higher confidence
|
|
941
|
+
if (destructive && !confident) {
|
|
942
|
+
const list = candidates.slice(0, 3).map((c, i) =>
|
|
943
|
+
`${i + 1}. ${c.displayPath} — "${c.title}" (${c.source}, score: ${c.score.toFixed(2)})`
|
|
944
|
+
).join("\n");
|
|
945
|
+
return { ambiguous: `Multiple possible matches. Please be more specific or use a path:\n${list}` };
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
// For non-destructive ops (pin/snooze), accept top hit if any candidates exist
|
|
949
|
+
if (!confident && candidates.length > 1) {
|
|
950
|
+
// Low confidence but not destructive — take top hit but warn
|
|
951
|
+
return { target: top };
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
return { target: top };
|
|
955
|
+
}
|
|
956
|
+
|
|
820
957
|
// ---------------------------------------------------------------------------
|
|
821
958
|
// Tool: memory_forget
|
|
822
959
|
// ---------------------------------------------------------------------------
|
|
@@ -833,28 +970,32 @@ This is the recommended entry point for ALL memory queries.`,
|
|
|
833
970
|
},
|
|
834
971
|
},
|
|
835
972
|
async ({ query, confirm, vault }) => {
|
|
836
|
-
const
|
|
837
|
-
const
|
|
838
|
-
|
|
839
|
-
|
|
973
|
+
const s = getStore(vault);
|
|
974
|
+
const candidates = await findMemoryCandidates(s, query, 5);
|
|
975
|
+
const selection = selectLifecycleTarget(candidates, query, true); // destructive = true
|
|
976
|
+
|
|
977
|
+
if ("notFound" in selection) {
|
|
978
|
+
return { content: [{ type: "text", text: selection.notFound }] };
|
|
979
|
+
}
|
|
980
|
+
if ("ambiguous" in selection) {
|
|
981
|
+
return { content: [{ type: "text", text: selection.ambiguous }] };
|
|
840
982
|
}
|
|
841
983
|
|
|
842
|
-
const best =
|
|
984
|
+
const best = selection.target;
|
|
843
985
|
const parts = best.displayPath.split("/");
|
|
844
986
|
const collection = parts[0]!;
|
|
845
987
|
const path = parts.slice(1).join("/");
|
|
846
988
|
|
|
847
989
|
if (!confirm) {
|
|
848
990
|
return {
|
|
849
|
-
content: [{ type: "text", text: `Would forget: ${best.displayPath} — "${best.title}" (score ${Math.round(best.score * 100)}%)` }],
|
|
991
|
+
content: [{ type: "text", text: `Would forget: ${best.displayPath} — "${best.title}" (${best.source}, score ${Math.round(best.score * 100)}%)` }],
|
|
850
992
|
structuredContent: { path: best.displayPath, title: best.title, score: best.score, action: "preview" },
|
|
851
993
|
};
|
|
852
994
|
}
|
|
853
995
|
|
|
854
|
-
|
|
996
|
+
s.deactivateDocument(collection, path);
|
|
855
997
|
|
|
856
|
-
|
|
857
|
-
store.insertUsage({
|
|
998
|
+
s.insertUsage({
|
|
858
999
|
sessionId: "mcp-forget",
|
|
859
1000
|
timestamp: new Date().toISOString(),
|
|
860
1001
|
hookName: "memory_forget",
|
|
@@ -1976,21 +2117,27 @@ This is the recommended entry point for ALL memory queries.`,
|
|
|
1976
2117
|
},
|
|
1977
2118
|
},
|
|
1978
2119
|
async ({ query, unpin, vault }) => {
|
|
1979
|
-
const
|
|
1980
|
-
const
|
|
1981
|
-
|
|
1982
|
-
|
|
2120
|
+
const s = getStore(vault);
|
|
2121
|
+
const candidates = await findMemoryCandidates(s, query);
|
|
2122
|
+
const selection = selectLifecycleTarget(candidates, query);
|
|
2123
|
+
|
|
2124
|
+
if ("notFound" in selection) {
|
|
2125
|
+
return { content: [{ type: "text", text: selection.notFound }], isError: true };
|
|
2126
|
+
}
|
|
2127
|
+
if ("ambiguous" in selection) {
|
|
2128
|
+
return { content: [{ type: "text", text: selection.ambiguous }], isError: true };
|
|
1983
2129
|
}
|
|
1984
|
-
|
|
2130
|
+
|
|
2131
|
+
const r = selection.target;
|
|
1985
2132
|
const parts = r.displayPath.split("/");
|
|
1986
2133
|
const collection = parts[0]!;
|
|
1987
2134
|
const path = parts.slice(1).join("/");
|
|
1988
|
-
const doc =
|
|
2135
|
+
const doc = s.findActiveDocument(collection, path);
|
|
1989
2136
|
if (!doc) {
|
|
1990
2137
|
return { content: [{ type: "text", text: "Document not found." }], isError: true };
|
|
1991
2138
|
}
|
|
1992
|
-
|
|
1993
|
-
|
|
2139
|
+
s.pinDocument(collection, path, !unpin);
|
|
2140
|
+
s.insertUsage({
|
|
1994
2141
|
sessionId: "mcp-pin",
|
|
1995
2142
|
timestamp: new Date().toISOString(),
|
|
1996
2143
|
hookName: "memory_pin",
|
|
@@ -2019,21 +2166,27 @@ This is the recommended entry point for ALL memory queries.`,
|
|
|
2019
2166
|
},
|
|
2020
2167
|
},
|
|
2021
2168
|
async ({ query, until, vault }) => {
|
|
2022
|
-
const
|
|
2023
|
-
const
|
|
2024
|
-
|
|
2025
|
-
|
|
2169
|
+
const s = getStore(vault);
|
|
2170
|
+
const candidates = await findMemoryCandidates(s, query);
|
|
2171
|
+
const selection = selectLifecycleTarget(candidates, query);
|
|
2172
|
+
|
|
2173
|
+
if ("notFound" in selection) {
|
|
2174
|
+
return { content: [{ type: "text", text: selection.notFound }], isError: true };
|
|
2026
2175
|
}
|
|
2027
|
-
|
|
2176
|
+
if ("ambiguous" in selection) {
|
|
2177
|
+
return { content: [{ type: "text", text: selection.ambiguous }], isError: true };
|
|
2178
|
+
}
|
|
2179
|
+
|
|
2180
|
+
const r = selection.target;
|
|
2028
2181
|
const parts = r.displayPath.split("/");
|
|
2029
2182
|
const collection = parts[0]!;
|
|
2030
2183
|
const path = parts.slice(1).join("/");
|
|
2031
|
-
const doc =
|
|
2184
|
+
const doc = s.findActiveDocument(collection, path);
|
|
2032
2185
|
if (!doc) {
|
|
2033
2186
|
return { content: [{ type: "text", text: "Document not found." }], isError: true };
|
|
2034
2187
|
}
|
|
2035
|
-
|
|
2036
|
-
|
|
2188
|
+
s.snoozeDocument(collection, path, until || null);
|
|
2189
|
+
s.insertUsage({
|
|
2037
2190
|
sessionId: "mcp-snooze",
|
|
2038
2191
|
timestamp: new Date().toISOString(),
|
|
2039
2192
|
hookName: "memory_snooze",
|