@betterdb/memory 0.1.2 → 0.4.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/README.md +101 -10
- package/package.json +3 -1
- package/scripts/aging-worker.ts +4 -1
- package/scripts/docker-valkey.sh +101 -0
- package/scripts/register-hooks.ts +94 -0
- package/scripts/setup-index.ts +10 -3
- package/scripts/unregister-hooks.ts +79 -0
- package/src/client/memory-store.ts +406 -0
- package/src/client/model.ts +10 -10
- package/src/client/providers/local.ts +58 -0
- package/src/client/valkey.ts +9 -0
- package/src/config.ts +38 -6
- package/src/hooks/post-tool.ts +2 -0
- package/src/hooks/pre-tool.ts +12 -11
- package/src/hooks/session-end.ts +14 -4
- package/src/hooks/session-start.ts +33 -8
- package/src/index.ts +379 -21
- package/src/mcp/server.ts +82 -42
- package/src/memory/aging.ts +78 -196
- package/src/memory/recall.ts +169 -0
- package/src/memory/retrieval.ts +73 -70
package/src/mcp/server.ts
CHANGED
|
@@ -2,43 +2,72 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
2
2
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { getValkeyClient } from "../client/valkey.js";
|
|
5
|
+
import { getPluginMemoryStore } from "../client/memory-store.js";
|
|
5
6
|
import { createModelClient } from "../client/model.js";
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
7
|
+
import { formatSearchResult } from "../memory/retrieval.js";
|
|
8
|
+
import { escalatingRecall } from "../memory/recall.js";
|
|
9
|
+
import { getCwdProject, getGitBranch } from "../memory/capture.js";
|
|
10
|
+
import { isConfigured } from "../config.js";
|
|
8
11
|
import type { EpisodicMemory, KnowledgeEntry } from "../memory/schema.js";
|
|
9
12
|
|
|
13
|
+
const SETUP_MESSAGE =
|
|
14
|
+
"BetterDB Memory is not configured yet. Run /betterdb-memory:setup to connect to Valkey and create the search index.";
|
|
15
|
+
|
|
10
16
|
const server = new McpServer({
|
|
11
17
|
name: "betterdb-memory",
|
|
12
|
-
version: "0.
|
|
18
|
+
version: "0.4.0",
|
|
13
19
|
});
|
|
14
20
|
|
|
15
21
|
// --- Tool: search_context ---
|
|
16
22
|
|
|
17
23
|
server.tool(
|
|
18
24
|
"search_context",
|
|
19
|
-
"Search your past Claude Code sessions for relevant context, decisions, or patterns"
|
|
25
|
+
"Search your past Claude Code sessions for relevant context, decisions, or patterns. " +
|
|
26
|
+
"Escalates automatically (project → wider → cross-project) and gates by relevance, " +
|
|
27
|
+
"so a miss means nothing relevant is stored — never fabricate to fill a miss.",
|
|
20
28
|
{
|
|
21
29
|
query: z.string().describe("The search query"),
|
|
22
|
-
top_k: z.number().int().min(1).max(20).optional().describe("Max results (default: 5)"),
|
|
30
|
+
top_k: z.number().int().min(1).max(20).optional().describe("Max results shown (default: 5)"),
|
|
31
|
+
scope: z
|
|
32
|
+
.enum(["project", "all"])
|
|
33
|
+
.optional()
|
|
34
|
+
.describe(
|
|
35
|
+
"Search scope. 'project' (default) stays in the current project; " +
|
|
36
|
+
"'all' also searches across every project — use when a project-scoped search found nothing.",
|
|
37
|
+
),
|
|
38
|
+
tags: z
|
|
39
|
+
.array(z.enum(["decision", "pattern", "problem", "open-thread"]))
|
|
40
|
+
.optional()
|
|
41
|
+
.describe(
|
|
42
|
+
"Filter to memories of these content types — e.g. ['decision'] to " +
|
|
43
|
+
"recall only decisions, ['open-thread'] for unresolved items.",
|
|
44
|
+
),
|
|
23
45
|
},
|
|
24
|
-
async ({ query, top_k }) => {
|
|
25
|
-
|
|
46
|
+
async ({ query, top_k, scope, tags }) => {
|
|
47
|
+
if (!isConfigured()) {
|
|
48
|
+
return { content: [{ type: "text" as const, text: SETUP_MESSAGE }] };
|
|
49
|
+
}
|
|
50
|
+
|
|
26
51
|
const modelClient = await createModelClient();
|
|
52
|
+
const store = await getPluginMemoryStore((t) => modelClient.embed(t));
|
|
27
53
|
|
|
28
|
-
const embedding = await modelClient.embed(query);
|
|
29
54
|
const project = getCwdProject();
|
|
55
|
+
const branch = getGitBranch();
|
|
30
56
|
const k = top_k ?? 5;
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
57
|
+
// Default (project) scope stays in-project so a miss can *offer* to widen
|
|
58
|
+
// to all projects — the two-step consent flow. An explicit scope="all"
|
|
59
|
+
// requests the cross-project rung; escalatingRecall still gates it on
|
|
60
|
+
// BETTERDB_ALLOW_CROSS_PROJECT and flags the miss honestly if it's off.
|
|
61
|
+
const result = await escalatingRecall(store, query, {
|
|
62
|
+
project,
|
|
63
|
+
...(branch !== "unknown" ? { branch } : {}),
|
|
64
|
+
...(tags !== undefined ? { tags } : {}),
|
|
65
|
+
crossProjectRequested: scope === "all",
|
|
66
|
+
});
|
|
67
|
+
const formatted = formatSearchResult(query, result, k);
|
|
34
68
|
|
|
35
69
|
return {
|
|
36
|
-
content: [
|
|
37
|
-
{
|
|
38
|
-
type: "text" as const,
|
|
39
|
-
text: formatted || "No matching memories found.",
|
|
40
|
-
},
|
|
41
|
-
],
|
|
70
|
+
content: [{ type: "text" as const, text: formatted }],
|
|
42
71
|
};
|
|
43
72
|
},
|
|
44
73
|
);
|
|
@@ -56,25 +85,17 @@ server.tool(
|
|
|
56
85
|
project: z.string().optional().describe("Project name (auto-detected if omitted)"),
|
|
57
86
|
},
|
|
58
87
|
async ({ content, category, project: projectInput }) => {
|
|
88
|
+
if (!isConfigured()) {
|
|
89
|
+
return { content: [{ type: "text" as const, text: SETUP_MESSAGE }] };
|
|
90
|
+
}
|
|
91
|
+
|
|
59
92
|
const valkeyClient = await getValkeyClient();
|
|
60
93
|
const modelClient = await createModelClient();
|
|
94
|
+
const store = await getPluginMemoryStore((t) => modelClient.embed(t));
|
|
61
95
|
const project = projectInput ?? getCwdProject();
|
|
62
96
|
|
|
63
|
-
// Store as
|
|
64
|
-
|
|
65
|
-
entryId: crypto.randomUUID(),
|
|
66
|
-
project,
|
|
67
|
-
topic: category,
|
|
68
|
-
fact: content,
|
|
69
|
-
confidence: 0.9,
|
|
70
|
-
sourceMemoryIds: [],
|
|
71
|
-
lastUpdated: new Date().toISOString(),
|
|
72
|
-
accessCount: 0,
|
|
73
|
-
};
|
|
74
|
-
await valkeyClient.storeKnowledge(entry);
|
|
75
|
-
|
|
76
|
-
// Also store as EpisodicMemory for vector searchability
|
|
77
|
-
const embedding = await modelClient.embed(content);
|
|
97
|
+
// Store as EpisodicMemory for vector searchability. MemoryStore mints the
|
|
98
|
+
// id, so capture it for the knowledge link and the user-facing response.
|
|
78
99
|
const memory: EpisodicMemory = {
|
|
79
100
|
memoryId: crypto.randomUUID(),
|
|
80
101
|
project,
|
|
@@ -92,13 +113,26 @@ server.tool(
|
|
|
92
113
|
accessCount: 0,
|
|
93
114
|
lastAccessed: new Date().toISOString(),
|
|
94
115
|
};
|
|
95
|
-
await
|
|
116
|
+
const memoryId = await store.storeMemory(memory);
|
|
117
|
+
|
|
118
|
+
// Store as KnowledgeEntry, linked to the episodic memory just written.
|
|
119
|
+
const entry: KnowledgeEntry = {
|
|
120
|
+
entryId: crypto.randomUUID(),
|
|
121
|
+
project,
|
|
122
|
+
topic: category,
|
|
123
|
+
fact: content,
|
|
124
|
+
confidence: 0.9,
|
|
125
|
+
sourceMemoryIds: [memoryId],
|
|
126
|
+
lastUpdated: new Date().toISOString(),
|
|
127
|
+
accessCount: 0,
|
|
128
|
+
};
|
|
129
|
+
await valkeyClient.storeKnowledge(entry);
|
|
96
130
|
|
|
97
131
|
return {
|
|
98
132
|
content: [
|
|
99
133
|
{
|
|
100
134
|
type: "text" as const,
|
|
101
|
-
text: `Stored ${category}: "${content}" (memory: ${
|
|
135
|
+
text: `Stored ${category}: "${content}" (memory: ${memoryId})`,
|
|
102
136
|
},
|
|
103
137
|
],
|
|
104
138
|
};
|
|
@@ -114,15 +148,17 @@ server.tool(
|
|
|
114
148
|
project: z.string().optional().describe("Project name (auto-detected if omitted)"),
|
|
115
149
|
},
|
|
116
150
|
async ({ project: projectInput }) => {
|
|
117
|
-
|
|
151
|
+
if (!isConfigured()) {
|
|
152
|
+
return { content: [{ type: "text" as const, text: SETUP_MESSAGE }] };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const store = await getPluginMemoryStore();
|
|
118
156
|
const project = projectInput ?? getCwdProject();
|
|
119
157
|
|
|
120
|
-
const
|
|
158
|
+
const memories = await store.listMemories(project, 0.5);
|
|
121
159
|
const threads = new Set<string>();
|
|
122
160
|
|
|
123
|
-
for (const
|
|
124
|
-
const memory = await valkeyClient.getMemory(id);
|
|
125
|
-
if (!memory) continue;
|
|
161
|
+
for (const memory of memories) {
|
|
126
162
|
for (const thread of memory.summary.openThreads) {
|
|
127
163
|
threads.add(thread);
|
|
128
164
|
}
|
|
@@ -154,10 +190,14 @@ server.tool(
|
|
|
154
190
|
confirmed: z.boolean().optional().describe("Set to true to confirm deletion"),
|
|
155
191
|
},
|
|
156
192
|
async ({ memory_id, confirmed }) => {
|
|
157
|
-
|
|
193
|
+
if (!isConfigured()) {
|
|
194
|
+
return { content: [{ type: "text" as const, text: SETUP_MESSAGE }] };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const store = await getPluginMemoryStore();
|
|
158
198
|
|
|
159
199
|
if (!confirmed) {
|
|
160
|
-
const memory = await
|
|
200
|
+
const memory = await store.getMemory(memory_id);
|
|
161
201
|
if (!memory) {
|
|
162
202
|
return {
|
|
163
203
|
content: [
|
|
@@ -182,7 +222,7 @@ server.tool(
|
|
|
182
222
|
};
|
|
183
223
|
}
|
|
184
224
|
|
|
185
|
-
await
|
|
225
|
+
await store.deleteMemory(memory_id);
|
|
186
226
|
|
|
187
227
|
return {
|
|
188
228
|
content: [
|
package/src/memory/aging.ts
CHANGED
|
@@ -1,199 +1,86 @@
|
|
|
1
|
+
import type { MemoryItem } from "@betterdb/agent-memory";
|
|
1
2
|
import { config } from "../config.js";
|
|
2
3
|
import type { ModelClient } from "../client/model.js";
|
|
3
4
|
import type { ValkeyClient } from "../client/valkey.js";
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
let dot = 0;
|
|
11
|
-
let magA = 0;
|
|
12
|
-
let magB = 0;
|
|
13
|
-
for (let i = 0; i < a.length; i++) {
|
|
14
|
-
const av = a[i] ?? 0;
|
|
15
|
-
const bv = b[i] ?? 0;
|
|
16
|
-
dot += av * bv;
|
|
17
|
-
magA += av * av;
|
|
18
|
-
magB += bv * bv;
|
|
19
|
-
}
|
|
20
|
-
const denom = Math.sqrt(magA) * Math.sqrt(magB);
|
|
21
|
-
return denom === 0 ? 0 : dot / denom;
|
|
22
|
-
}
|
|
5
|
+
import {
|
|
6
|
+
itemToEpisodic,
|
|
7
|
+
type PluginMemoryStore,
|
|
8
|
+
} from "../client/memory-store.js";
|
|
9
|
+
import type { EpisodicMemory } from "./schema.js";
|
|
10
|
+
import { computeInitialImportance } from "./capture.js";
|
|
23
11
|
|
|
24
12
|
// --- Aging Pipeline ---
|
|
13
|
+
//
|
|
14
|
+
// Recency decay and similarity clustering used to live here as bespoke code;
|
|
15
|
+
// both are now provided by @betterdb/agent-memory's MemoryStore — composite
|
|
16
|
+
// recall scoring handles recency at query time, and consolidate() merges a
|
|
17
|
+
// scope's low-value memories into a single summary. What remains here is the
|
|
18
|
+
// plugin-specific glue: ingest-queue processing, LLM-driven consolidation
|
|
19
|
+
// summarization, and pattern distillation into KnowledgeEntries.
|
|
20
|
+
//
|
|
21
|
+
// Consolidation only runs when a project has at least this many low-importance
|
|
22
|
+
// memories, so a lone low-value memory isn't pointlessly re-summarized (which
|
|
23
|
+
// would discard its structured summary and reset its access stats).
|
|
24
|
+
const CONSOLIDATE_MIN_CANDIDATES = 3;
|
|
25
25
|
|
|
26
26
|
export class AgingPipeline {
|
|
27
27
|
private valkeyClient: ValkeyClient;
|
|
28
|
+
private store: PluginMemoryStore;
|
|
28
29
|
private modelClient: ModelClient;
|
|
29
30
|
|
|
30
|
-
constructor(
|
|
31
|
+
constructor(
|
|
32
|
+
valkeyClient: ValkeyClient,
|
|
33
|
+
store: PluginMemoryStore,
|
|
34
|
+
modelClient: ModelClient,
|
|
35
|
+
) {
|
|
31
36
|
this.valkeyClient = valkeyClient;
|
|
37
|
+
this.store = store;
|
|
32
38
|
this.modelClient = modelClient;
|
|
33
39
|
}
|
|
34
40
|
|
|
35
|
-
// ---
|
|
36
|
-
|
|
37
|
-
async runDecay(
|
|
38
|
-
project?: string,
|
|
39
|
-
): Promise<{ processed: number; flagged: number }> {
|
|
40
|
-
const memoryIds = await this.valkeyClient.listMemoryIds(project);
|
|
41
|
-
let processed = 0;
|
|
42
|
-
let flagged = 0;
|
|
43
|
-
|
|
44
|
-
for (const id of memoryIds) {
|
|
45
|
-
const memory = await this.valkeyClient.getMemory(id);
|
|
46
|
-
if (!memory) continue;
|
|
47
|
-
|
|
48
|
-
const daysSince =
|
|
49
|
-
(Date.now() - new Date(memory.lastAccessed).getTime()) /
|
|
50
|
-
(1000 * 60 * 60 * 24);
|
|
51
|
-
const newScore =
|
|
52
|
-
memory.importanceScore *
|
|
53
|
-
Math.pow(config.memory.decayRate, daysSince);
|
|
41
|
+
// --- Consolidation ---
|
|
54
42
|
|
|
55
|
-
|
|
56
|
-
|
|
43
|
+
async runConsolidation(
|
|
44
|
+
project: string,
|
|
45
|
+
): Promise<{ consolidated: number; created: number; deleted: number }> {
|
|
46
|
+
const threshold = config.memory.compressThreshold;
|
|
47
|
+
const candidates = (await this.store.listMemories(project)).filter(
|
|
48
|
+
(m) => m.importanceScore <= threshold,
|
|
49
|
+
);
|
|
57
50
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
flagged++;
|
|
61
|
-
}
|
|
51
|
+
if (candidates.length < CONSOLIDATE_MIN_CANDIDATES) {
|
|
52
|
+
return { consolidated: 0, created: 0, deleted: 0 };
|
|
62
53
|
}
|
|
63
54
|
|
|
64
|
-
|
|
55
|
+
const result = await this.store.consolidate({
|
|
56
|
+
namespace: project,
|
|
57
|
+
maxImportance: threshold,
|
|
58
|
+
summaryImportance: threshold,
|
|
59
|
+
summarize: (items) => this.summarizeCluster(items),
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
consolidated: result.consolidated,
|
|
64
|
+
created: result.created.length,
|
|
65
|
+
deleted: result.deleted,
|
|
66
|
+
};
|
|
65
67
|
}
|
|
66
68
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
embedding: number[];
|
|
77
|
-
}> = [];
|
|
78
|
-
|
|
79
|
-
for (const id of ids) {
|
|
80
|
-
const memory = await this.valkeyClient.getMemory(id);
|
|
81
|
-
const embedding = await this.valkeyClient.getMemoryEmbedding(id);
|
|
82
|
-
if (memory && embedding) {
|
|
83
|
-
entries.push({ memory, embedding });
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// Group by project
|
|
88
|
-
const byProject = new Map<
|
|
89
|
-
string,
|
|
90
|
-
Array<{ memory: EpisodicMemory; embedding: number[] }>
|
|
91
|
-
>();
|
|
92
|
-
for (const entry of entries) {
|
|
93
|
-
const group = byProject.get(entry.memory.project) ?? [];
|
|
94
|
-
group.push(entry);
|
|
95
|
-
byProject.set(entry.memory.project, group);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
let merged = 0;
|
|
99
|
-
let deleted = 0;
|
|
100
|
-
|
|
101
|
-
for (const [, group] of byProject) {
|
|
102
|
-
// Batch size guard: only process 100 lowest-importance per project
|
|
103
|
-
const sorted = group
|
|
104
|
-
.sort((a, b) => a.memory.importanceScore - b.memory.importanceScore)
|
|
105
|
-
.slice(0, 100);
|
|
106
|
-
|
|
107
|
-
if (sorted.length < group.length) {
|
|
108
|
-
console.error(
|
|
109
|
-
`[betterdb] Project group exceeds 100 memories, processing only lowest-importance 100. Additional runs needed.`,
|
|
69
|
+
private async summarizeCluster(items: MemoryItem[]): Promise<string> {
|
|
70
|
+
const transcript = items
|
|
71
|
+
.map((item) => {
|
|
72
|
+
const memory = itemToEpisodic(item);
|
|
73
|
+
if (!memory) return item.content;
|
|
74
|
+
return (
|
|
75
|
+
`Session: ${memory.summary.oneLineSummary}\n` +
|
|
76
|
+
`Decisions: ${memory.summary.decisions.join("; ")}\n` +
|
|
77
|
+
`Patterns: ${memory.summary.patterns.join("; ")}`
|
|
110
78
|
);
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// Find clusters of similar memories
|
|
114
|
-
const used = new Set<number>();
|
|
115
|
-
const clusters: Array<
|
|
116
|
-
Array<{ memory: EpisodicMemory; embedding: number[] }>
|
|
117
|
-
> = [];
|
|
118
|
-
|
|
119
|
-
for (let i = 0; i < sorted.length; i++) {
|
|
120
|
-
if (used.has(i)) continue;
|
|
121
|
-
const cluster = [sorted[i]!];
|
|
122
|
-
used.add(i);
|
|
123
|
-
|
|
124
|
-
for (let j = i + 1; j < sorted.length; j++) {
|
|
125
|
-
if (used.has(j)) continue;
|
|
126
|
-
// Check if similar to all cluster members
|
|
127
|
-
const similar = cluster.every(
|
|
128
|
-
(c) =>
|
|
129
|
-
cosineSimilarity(c.embedding, sorted[j]!.embedding) > 0.85,
|
|
130
|
-
);
|
|
131
|
-
if (similar) {
|
|
132
|
-
cluster.push(sorted[j]!);
|
|
133
|
-
used.add(j);
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
clusters.push(cluster);
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
// Process clusters
|
|
141
|
-
for (const cluster of clusters) {
|
|
142
|
-
if (cluster.length >= 3) {
|
|
143
|
-
// Merge cluster into a single memory
|
|
144
|
-
const combinedTranscript = cluster
|
|
145
|
-
.map(
|
|
146
|
-
(c) =>
|
|
147
|
-
`Session ${c.memory.memoryId}: ${c.memory.summary.oneLineSummary}\n` +
|
|
148
|
-
`Decisions: ${c.memory.summary.decisions.join("; ")}\n` +
|
|
149
|
-
`Patterns: ${c.memory.summary.patterns.join("; ")}`,
|
|
150
|
-
)
|
|
151
|
-
.join("\n\n");
|
|
152
|
-
|
|
153
|
-
const mergedSummary =
|
|
154
|
-
await this.modelClient.summarize(combinedTranscript);
|
|
155
|
-
const mergedEmbedding = await this.modelClient.embed(
|
|
156
|
-
mergedSummary.oneLineSummary,
|
|
157
|
-
);
|
|
158
|
-
|
|
159
|
-
const avgImportance =
|
|
160
|
-
cluster.reduce((sum, c) => sum + c.memory.importanceScore, 0) /
|
|
161
|
-
cluster.length;
|
|
162
|
-
|
|
163
|
-
const newMemory: EpisodicMemory = {
|
|
164
|
-
memoryId: crypto.randomUUID(),
|
|
165
|
-
project: cluster[0]!.memory.project,
|
|
166
|
-
branch: cluster[0]!.memory.branch,
|
|
167
|
-
timestamp: new Date().toISOString(),
|
|
168
|
-
summary: mergedSummary,
|
|
169
|
-
importanceScore: avgImportance,
|
|
170
|
-
accessCount: 0,
|
|
171
|
-
lastAccessed: new Date().toISOString(),
|
|
172
|
-
};
|
|
173
|
-
|
|
174
|
-
await this.valkeyClient.storeMemory(newMemory, mergedEmbedding);
|
|
175
|
-
|
|
176
|
-
// Delete originals
|
|
177
|
-
for (const c of cluster) {
|
|
178
|
-
await this.valkeyClient.deleteMemory(c.memory.memoryId);
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
merged += cluster.length;
|
|
182
|
-
} else if (cluster.length === 1) {
|
|
183
|
-
const m = cluster[0]!.memory;
|
|
184
|
-
const daysSince =
|
|
185
|
-
(Date.now() - new Date(m.lastAccessed).getTime()) /
|
|
186
|
-
(1000 * 60 * 60 * 24);
|
|
187
|
-
|
|
188
|
-
if (m.importanceScore < 0.05 && daysSince > 90) {
|
|
189
|
-
await this.valkeyClient.deleteMemory(m.memoryId);
|
|
190
|
-
deleted++;
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
}
|
|
79
|
+
})
|
|
80
|
+
.join("\n\n");
|
|
195
81
|
|
|
196
|
-
|
|
82
|
+
const summary = await this.modelClient.summarize(transcript);
|
|
83
|
+
return summary.oneLineSummary;
|
|
197
84
|
}
|
|
198
85
|
|
|
199
86
|
// --- Distillation ---
|
|
@@ -201,13 +88,7 @@ export class AgingPipeline {
|
|
|
201
88
|
async runDistillation(
|
|
202
89
|
project: string,
|
|
203
90
|
): Promise<{ distilled: number }> {
|
|
204
|
-
const
|
|
205
|
-
const memories: EpisodicMemory[] = [];
|
|
206
|
-
|
|
207
|
-
for (const id of memoryIds) {
|
|
208
|
-
const memory = await this.valkeyClient.getMemory(id);
|
|
209
|
-
if (memory) memories.push(memory);
|
|
210
|
-
}
|
|
91
|
+
const memories = await this.store.listMemories(project, 0.5);
|
|
211
92
|
|
|
212
93
|
if (memories.length < config.memory.distillMinSessions) {
|
|
213
94
|
return { distilled: 0 };
|
|
@@ -259,9 +140,6 @@ export class AgingPipeline {
|
|
|
259
140
|
for (const item of items) {
|
|
260
141
|
try {
|
|
261
142
|
const summary = await this.modelClient.summarize(item.transcript);
|
|
262
|
-
const embedding = await this.modelClient.embed(
|
|
263
|
-
summary.oneLineSummary,
|
|
264
|
-
);
|
|
265
143
|
const importance = computeInitialImportance(summary);
|
|
266
144
|
|
|
267
145
|
const meta = item.meta as Record<string, string>;
|
|
@@ -276,7 +154,7 @@ export class AgingPipeline {
|
|
|
276
154
|
lastAccessed: new Date().toISOString(),
|
|
277
155
|
};
|
|
278
156
|
|
|
279
|
-
await this.
|
|
157
|
+
await this.store.storeMemory(memory);
|
|
280
158
|
processed++;
|
|
281
159
|
} catch (err) {
|
|
282
160
|
console.error("[betterdb] Failed to process queued transcript:", err);
|
|
@@ -300,22 +178,26 @@ export class AgingPipeline {
|
|
|
300
178
|
const { processed: ingested } = await this.processIngestQueue();
|
|
301
179
|
console.error(`[betterdb] Ingest queue: processed ${ingested} items`);
|
|
302
180
|
|
|
303
|
-
const
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
);
|
|
181
|
+
const projects = project
|
|
182
|
+
? [project]
|
|
183
|
+
: await this.allProjects();
|
|
307
184
|
|
|
308
|
-
const
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
185
|
+
for (const p of projects) {
|
|
186
|
+
const { consolidated, created, deleted } = await this.runConsolidation(p);
|
|
187
|
+
console.error(
|
|
188
|
+
`[betterdb] Consolidation (${p}): merged ${consolidated} into ${created}, deleted ${deleted}`,
|
|
189
|
+
);
|
|
312
190
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
console.error(`[betterdb] Distillation: distilled ${distilled} entries`);
|
|
191
|
+
const { distilled } = await this.runDistillation(p);
|
|
192
|
+
console.error(`[betterdb] Distillation (${p}): distilled ${distilled} entries`);
|
|
316
193
|
}
|
|
317
194
|
|
|
318
195
|
await this.valkeyClient.setLastAgingRun(new Date());
|
|
319
196
|
console.error("[betterdb] Aging pipeline complete.");
|
|
320
197
|
}
|
|
198
|
+
|
|
199
|
+
private async allProjects(): Promise<string[]> {
|
|
200
|
+
const memories = await this.store.listMemories();
|
|
201
|
+
return [...new Set(memories.map((m) => m.project))];
|
|
202
|
+
}
|
|
321
203
|
}
|