@bamdra/bamdra-openclaw-memory 0.3.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 +32 -0
- package/dist/index.d.ts +43 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1960 -0
- package/dist/index.js.map +1 -0
- package/dist/openclaw.plugin.json +29 -0
- package/dist/package.json +65 -0
- package/dist/schema.sql +108 -0
- package/openclaw.plugin.json +25 -0
- package/package.json +65 -0
- package/skills/bamdra-memory-operator/SKILL.md +84 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1960 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
activate: () => activate,
|
|
24
|
+
register: () => register
|
|
25
|
+
});
|
|
26
|
+
module.exports = __toCommonJS(index_exports);
|
|
27
|
+
|
|
28
|
+
// ../../../node_modules/.pnpm/tsup@8.5.1_typescript@5.9.3/node_modules/tsup/assets/cjs_shims.js
|
|
29
|
+
var getImportMetaUrl = () => typeof document === "undefined" ? new URL(`file:${__filename}`).href : document.currentScript && document.currentScript.tagName.toUpperCase() === "SCRIPT" ? document.currentScript.src : new URL("main.js", document.baseURI).href;
|
|
30
|
+
var importMetaUrl = /* @__PURE__ */ getImportMetaUrl();
|
|
31
|
+
|
|
32
|
+
// ../../packages/context-assembler/src/index.ts
|
|
33
|
+
var ContextAssembler = class {
|
|
34
|
+
constructor(config) {
|
|
35
|
+
this.config = config;
|
|
36
|
+
}
|
|
37
|
+
assemble(input) {
|
|
38
|
+
const sections = [];
|
|
39
|
+
if (input.topic) {
|
|
40
|
+
sections.push({
|
|
41
|
+
kind: "topic",
|
|
42
|
+
content: `Topic: ${input.topic.title}
|
|
43
|
+
Labels: ${joinList(input.topic.labels)}`
|
|
44
|
+
});
|
|
45
|
+
if (this.config.contextAssembly?.includeTopicShortSummary !== false) {
|
|
46
|
+
sections.push({
|
|
47
|
+
kind: "summary",
|
|
48
|
+
content: input.topic.summaryShort || "(no short summary yet)"
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
if (this.config.contextAssembly?.includeOpenLoops !== false && input.topic.openLoops.length > 0) {
|
|
52
|
+
sections.push({
|
|
53
|
+
kind: "open_loops",
|
|
54
|
+
content: input.topic.openLoops.map((item) => `- ${item}`).join("\n")
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
const facts = [
|
|
59
|
+
...limitFacts(
|
|
60
|
+
input.alwaysFacts,
|
|
61
|
+
this.config.contextAssembly?.alwaysFactLimit ?? 12
|
|
62
|
+
),
|
|
63
|
+
...limitFacts(
|
|
64
|
+
input.topicFacts,
|
|
65
|
+
this.config.contextAssembly?.topicFactLimit ?? 16
|
|
66
|
+
)
|
|
67
|
+
];
|
|
68
|
+
if (facts.length > 0) {
|
|
69
|
+
sections.push({
|
|
70
|
+
kind: "facts",
|
|
71
|
+
content: facts.map((fact) => {
|
|
72
|
+
const prefix = fact.sensitivity === "secret_ref" ? "[secret-ref]" : `[${fact.category}]`;
|
|
73
|
+
return `${prefix} ${fact.key}: ${fact.value}`;
|
|
74
|
+
}).join("\n")
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
if (input.recentMessages.length > 0) {
|
|
78
|
+
sections.push({
|
|
79
|
+
kind: "recent_messages",
|
|
80
|
+
content: input.recentMessages.map(({ message }) => `${message.role}: ${message.text}`).join("\n")
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
sessionId: input.sessionId,
|
|
85
|
+
topicId: input.topic?.id ?? null,
|
|
86
|
+
text: sections.map((section) => `[${section.kind}]
|
|
87
|
+
${section.content}`).join("\n\n"),
|
|
88
|
+
sections
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
function joinList(values) {
|
|
93
|
+
return values.length > 0 ? values.join(", ") : "(none)";
|
|
94
|
+
}
|
|
95
|
+
function limitFacts(values, limit) {
|
|
96
|
+
return values.slice(0, Math.max(0, limit));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ../../packages/fact-extractor/src/index.ts
|
|
100
|
+
var FactExtractor = class {
|
|
101
|
+
constructor(_config) {
|
|
102
|
+
this._config = _config;
|
|
103
|
+
}
|
|
104
|
+
extract(input) {
|
|
105
|
+
const candidates = [];
|
|
106
|
+
candidates.push(...extractNodeVersion(input));
|
|
107
|
+
candidates.push(...extractAccountLikeFacts(input));
|
|
108
|
+
candidates.push(...extractConstraintFacts(input));
|
|
109
|
+
candidates.push(...extractPreferenceFacts(input));
|
|
110
|
+
return dedupeCandidates(candidates);
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
function extractNodeVersion(input) {
|
|
114
|
+
const match = input.text.match(/\bnode(?:\.js)?\s*v?(\d+\.\d+\.\d+)\b/i);
|
|
115
|
+
if (!match) {
|
|
116
|
+
return [];
|
|
117
|
+
}
|
|
118
|
+
return [
|
|
119
|
+
{
|
|
120
|
+
category: "environment",
|
|
121
|
+
key: "runtime.node",
|
|
122
|
+
value: `Node ${match[1]}`,
|
|
123
|
+
sensitivity: "normal",
|
|
124
|
+
recallPolicy: "always",
|
|
125
|
+
scope: "global",
|
|
126
|
+
confidence: 0.95,
|
|
127
|
+
tags: ["runtime", "node"]
|
|
128
|
+
}
|
|
129
|
+
];
|
|
130
|
+
}
|
|
131
|
+
function extractAccountLikeFacts(input) {
|
|
132
|
+
const lower = input.text.toLowerCase();
|
|
133
|
+
const candidates = [];
|
|
134
|
+
if (!/(账号|账户|account|appid|appsecret|token|apikey|api key)/i.test(input.text)) {
|
|
135
|
+
return candidates;
|
|
136
|
+
}
|
|
137
|
+
const scope = input.topic ? `topic:${input.topic.id}` : "shared";
|
|
138
|
+
const labels = input.topic?.labels ?? [];
|
|
139
|
+
if (/(appid|appsecret|token|apikey|api key)/i.test(input.text)) {
|
|
140
|
+
candidates.push({
|
|
141
|
+
category: "security",
|
|
142
|
+
key: "secret.reference",
|
|
143
|
+
value: abbreviate(input.text),
|
|
144
|
+
sensitivity: "secret_ref",
|
|
145
|
+
recallPolicy: "topic_bound",
|
|
146
|
+
scope,
|
|
147
|
+
confidence: 0.8,
|
|
148
|
+
tags: [...labels, "security", "account"]
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
if (/(账号|账户|account)/i.test(input.text)) {
|
|
152
|
+
candidates.push({
|
|
153
|
+
category: "account",
|
|
154
|
+
key: `account.note.${stableKeyFragment(input.text)}`,
|
|
155
|
+
value: abbreviate(input.text),
|
|
156
|
+
sensitivity: "sensitive",
|
|
157
|
+
recallPolicy: "topic_bound",
|
|
158
|
+
scope,
|
|
159
|
+
confidence: 0.72,
|
|
160
|
+
tags: [...labels, "account"]
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
return candidates;
|
|
164
|
+
}
|
|
165
|
+
function extractConstraintFacts(input) {
|
|
166
|
+
if (!/(必须|不能|不要|禁止|只能|must|cannot|can't|should not|do not)/i.test(input.text)) {
|
|
167
|
+
return [];
|
|
168
|
+
}
|
|
169
|
+
return [
|
|
170
|
+
{
|
|
171
|
+
category: "constraint",
|
|
172
|
+
key: `constraint.${stableKeyFragment(input.text)}`,
|
|
173
|
+
value: abbreviate(input.text),
|
|
174
|
+
sensitivity: "normal",
|
|
175
|
+
recallPolicy: "topic_bound",
|
|
176
|
+
scope: input.topic ? `topic:${input.topic.id}` : "shared",
|
|
177
|
+
confidence: 0.82,
|
|
178
|
+
tags: [...input.topic?.labels ?? [], "constraint"]
|
|
179
|
+
}
|
|
180
|
+
];
|
|
181
|
+
}
|
|
182
|
+
function extractPreferenceFacts(input) {
|
|
183
|
+
if (!/(偏好|喜欢|不喜欢|prefer|preference|默认)/i.test(input.text)) {
|
|
184
|
+
return [];
|
|
185
|
+
}
|
|
186
|
+
return [
|
|
187
|
+
{
|
|
188
|
+
category: "preference",
|
|
189
|
+
key: `preference.${stableKeyFragment(input.text)}`,
|
|
190
|
+
value: abbreviate(input.text),
|
|
191
|
+
sensitivity: "normal",
|
|
192
|
+
recallPolicy: "always",
|
|
193
|
+
scope: "shared",
|
|
194
|
+
confidence: 0.76,
|
|
195
|
+
tags: [...input.topic?.labels ?? [], "preference"]
|
|
196
|
+
}
|
|
197
|
+
];
|
|
198
|
+
}
|
|
199
|
+
function dedupeCandidates(candidates) {
|
|
200
|
+
const seen = /* @__PURE__ */ new Set();
|
|
201
|
+
return candidates.filter((candidate) => {
|
|
202
|
+
const dedupeKey = `${candidate.scope}:${candidate.key}:${candidate.value}`;
|
|
203
|
+
if (seen.has(dedupeKey)) {
|
|
204
|
+
return false;
|
|
205
|
+
}
|
|
206
|
+
seen.add(dedupeKey);
|
|
207
|
+
return true;
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
function abbreviate(text) {
|
|
211
|
+
const compact = text.trim().replace(/\s+/g, " ");
|
|
212
|
+
return compact.length <= 120 ? compact : `${compact.slice(0, 120)}...`;
|
|
213
|
+
}
|
|
214
|
+
function stableKeyFragment(text) {
|
|
215
|
+
return text.toLowerCase().replace(/[^a-z0-9\u4e00-\u9fff]+/gi, ".").replace(/^\.+|\.+$/g, "").slice(0, 48) || "note";
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// ../../packages/memory-cache-memory/src/index.ts
|
|
219
|
+
function logCacheEvent(event, details = {}) {
|
|
220
|
+
try {
|
|
221
|
+
console.info("[bamdra-memory-cache]", event, JSON.stringify(details));
|
|
222
|
+
} catch {
|
|
223
|
+
console.info("[bamdra-memory-cache]", event);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
var InMemoryCacheStore = class {
|
|
227
|
+
sessionState = /* @__PURE__ */ new Map();
|
|
228
|
+
maxSessions;
|
|
229
|
+
constructor(config = { provider: "memory" }) {
|
|
230
|
+
this.maxSessions = config.maxSessions ?? 128;
|
|
231
|
+
logCacheEvent("init", { provider: "memory", maxSessions: this.maxSessions });
|
|
232
|
+
}
|
|
233
|
+
async getActiveTopicId(sessionId) {
|
|
234
|
+
const activeTopicId = this.sessionState.get(sessionId)?.activeTopicId ?? null;
|
|
235
|
+
logCacheEvent("get-active-topic", {
|
|
236
|
+
sessionId,
|
|
237
|
+
hit: activeTopicId !== null,
|
|
238
|
+
activeTopicId
|
|
239
|
+
});
|
|
240
|
+
return activeTopicId;
|
|
241
|
+
}
|
|
242
|
+
async setActiveTopicId(sessionId, topicId) {
|
|
243
|
+
const current = this.sessionState.get(sessionId);
|
|
244
|
+
await this.setSessionState(sessionId, {
|
|
245
|
+
activeTopicId: topicId,
|
|
246
|
+
updatedAt: current?.updatedAt ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
async getSessionState(sessionId) {
|
|
250
|
+
const state = this.sessionState.get(sessionId) ?? null;
|
|
251
|
+
logCacheEvent("get-session-state", {
|
|
252
|
+
sessionId,
|
|
253
|
+
hit: state !== null,
|
|
254
|
+
activeTopicId: state?.activeTopicId ?? null
|
|
255
|
+
});
|
|
256
|
+
return state;
|
|
257
|
+
}
|
|
258
|
+
async setSessionState(sessionId, state) {
|
|
259
|
+
if (!this.sessionState.has(sessionId) && this.sessionState.size >= this.maxSessions) {
|
|
260
|
+
const oldestKey = this.sessionState.keys().next().value;
|
|
261
|
+
if (oldestKey) {
|
|
262
|
+
this.sessionState.delete(oldestKey);
|
|
263
|
+
logCacheEvent("evict-session-state", {
|
|
264
|
+
sessionId: oldestKey,
|
|
265
|
+
sizeAfter: this.sessionState.size
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
this.sessionState.set(sessionId, state);
|
|
270
|
+
logCacheEvent("set-session-state", {
|
|
271
|
+
sessionId,
|
|
272
|
+
activeTopicId: state.activeTopicId,
|
|
273
|
+
updatedAt: state.updatedAt,
|
|
274
|
+
size: this.sessionState.size
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
async deleteSessionState(sessionId) {
|
|
278
|
+
this.sessionState.delete(sessionId);
|
|
279
|
+
logCacheEvent("delete-session-state", {
|
|
280
|
+
sessionId,
|
|
281
|
+
size: this.sessionState.size
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
async close() {
|
|
285
|
+
}
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
// ../../packages/memory-sqlite/src/index.ts
|
|
289
|
+
var import_node_fs = require("node:fs");
|
|
290
|
+
var import_promises = require("node:fs/promises");
|
|
291
|
+
var import_node_path = require("node:path");
|
|
292
|
+
var import_node_sqlite = require("node:sqlite");
|
|
293
|
+
var import_node_url = require("node:url");
|
|
294
|
+
var SCHEMA_VERSION = 1;
|
|
295
|
+
var MemorySqliteStore = class {
|
|
296
|
+
constructor(options) {
|
|
297
|
+
this.options = options;
|
|
298
|
+
(0, import_node_fs.mkdirSync)((0, import_node_path.dirname)(options.path), { recursive: true });
|
|
299
|
+
this.db = new import_node_sqlite.DatabaseSync(options.path);
|
|
300
|
+
}
|
|
301
|
+
db;
|
|
302
|
+
async getSchemaVersion() {
|
|
303
|
+
return SCHEMA_VERSION;
|
|
304
|
+
}
|
|
305
|
+
async applyMigrations() {
|
|
306
|
+
const schemaSql = await loadSchemaSql();
|
|
307
|
+
this.db.exec(schemaSql);
|
|
308
|
+
}
|
|
309
|
+
async close() {
|
|
310
|
+
this.db.close();
|
|
311
|
+
}
|
|
312
|
+
async upsertMessage(record) {
|
|
313
|
+
this.db.prepare(
|
|
314
|
+
`INSERT INTO messages (
|
|
315
|
+
id,
|
|
316
|
+
session_id,
|
|
317
|
+
turn_id,
|
|
318
|
+
parent_turn_id,
|
|
319
|
+
role,
|
|
320
|
+
event_type,
|
|
321
|
+
text,
|
|
322
|
+
ts,
|
|
323
|
+
raw_json
|
|
324
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
325
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
326
|
+
session_id = excluded.session_id,
|
|
327
|
+
turn_id = excluded.turn_id,
|
|
328
|
+
parent_turn_id = excluded.parent_turn_id,
|
|
329
|
+
role = excluded.role,
|
|
330
|
+
event_type = excluded.event_type,
|
|
331
|
+
text = excluded.text,
|
|
332
|
+
ts = excluded.ts,
|
|
333
|
+
raw_json = excluded.raw_json`
|
|
334
|
+
).run(
|
|
335
|
+
record.id,
|
|
336
|
+
record.sessionId,
|
|
337
|
+
record.turnId,
|
|
338
|
+
record.parentTurnId,
|
|
339
|
+
record.role,
|
|
340
|
+
record.eventType,
|
|
341
|
+
record.text,
|
|
342
|
+
record.ts,
|
|
343
|
+
record.rawJson
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
async getSessionState(sessionId) {
|
|
347
|
+
const row = this.db.prepare(
|
|
348
|
+
`SELECT session_id, active_topic_id, last_compacted_at, last_turn_id, updated_at
|
|
349
|
+
FROM session_state
|
|
350
|
+
WHERE session_id = ?`
|
|
351
|
+
).get(sessionId);
|
|
352
|
+
return row ? mapSessionStateRow(row) : null;
|
|
353
|
+
}
|
|
354
|
+
async upsertSessionState(record) {
|
|
355
|
+
this.db.prepare(
|
|
356
|
+
`INSERT INTO session_state (
|
|
357
|
+
session_id,
|
|
358
|
+
active_topic_id,
|
|
359
|
+
last_compacted_at,
|
|
360
|
+
last_turn_id,
|
|
361
|
+
updated_at
|
|
362
|
+
) VALUES (?, ?, ?, ?, ?)
|
|
363
|
+
ON CONFLICT(session_id) DO UPDATE SET
|
|
364
|
+
active_topic_id = excluded.active_topic_id,
|
|
365
|
+
last_compacted_at = excluded.last_compacted_at,
|
|
366
|
+
last_turn_id = excluded.last_turn_id,
|
|
367
|
+
updated_at = excluded.updated_at`
|
|
368
|
+
).run(
|
|
369
|
+
record.sessionId,
|
|
370
|
+
record.activeTopicId,
|
|
371
|
+
record.lastCompactedAt,
|
|
372
|
+
record.lastTurnId,
|
|
373
|
+
record.updatedAt
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
async listTopics(sessionId) {
|
|
377
|
+
const rows = this.db.prepare(
|
|
378
|
+
`SELECT
|
|
379
|
+
id,
|
|
380
|
+
session_id,
|
|
381
|
+
title,
|
|
382
|
+
status,
|
|
383
|
+
parent_topic_id,
|
|
384
|
+
summary_short,
|
|
385
|
+
summary_long,
|
|
386
|
+
open_loops_json,
|
|
387
|
+
labels_json,
|
|
388
|
+
created_at,
|
|
389
|
+
last_active_at
|
|
390
|
+
FROM topics
|
|
391
|
+
WHERE session_id = ?
|
|
392
|
+
ORDER BY last_active_at DESC`
|
|
393
|
+
).all(sessionId);
|
|
394
|
+
return rows.map(mapTopicRow);
|
|
395
|
+
}
|
|
396
|
+
async getTopic(topicId) {
|
|
397
|
+
const row = this.db.prepare(
|
|
398
|
+
`SELECT
|
|
399
|
+
id,
|
|
400
|
+
session_id,
|
|
401
|
+
title,
|
|
402
|
+
status,
|
|
403
|
+
parent_topic_id,
|
|
404
|
+
summary_short,
|
|
405
|
+
summary_long,
|
|
406
|
+
open_loops_json,
|
|
407
|
+
labels_json,
|
|
408
|
+
created_at,
|
|
409
|
+
last_active_at
|
|
410
|
+
FROM topics
|
|
411
|
+
WHERE id = ?`
|
|
412
|
+
).get(topicId);
|
|
413
|
+
return row ? mapTopicRow(row) : null;
|
|
414
|
+
}
|
|
415
|
+
async upsertTopic(record) {
|
|
416
|
+
this.db.prepare(
|
|
417
|
+
`INSERT INTO topics (
|
|
418
|
+
id,
|
|
419
|
+
session_id,
|
|
420
|
+
title,
|
|
421
|
+
status,
|
|
422
|
+
parent_topic_id,
|
|
423
|
+
summary_short,
|
|
424
|
+
summary_long,
|
|
425
|
+
open_loops_json,
|
|
426
|
+
labels_json,
|
|
427
|
+
created_at,
|
|
428
|
+
last_active_at
|
|
429
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
430
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
431
|
+
session_id = excluded.session_id,
|
|
432
|
+
title = excluded.title,
|
|
433
|
+
status = excluded.status,
|
|
434
|
+
parent_topic_id = excluded.parent_topic_id,
|
|
435
|
+
summary_short = excluded.summary_short,
|
|
436
|
+
summary_long = excluded.summary_long,
|
|
437
|
+
open_loops_json = excluded.open_loops_json,
|
|
438
|
+
labels_json = excluded.labels_json,
|
|
439
|
+
created_at = excluded.created_at,
|
|
440
|
+
last_active_at = excluded.last_active_at`
|
|
441
|
+
).run(
|
|
442
|
+
record.id,
|
|
443
|
+
record.sessionId,
|
|
444
|
+
record.title,
|
|
445
|
+
record.status,
|
|
446
|
+
record.parentTopicId,
|
|
447
|
+
record.summaryShort,
|
|
448
|
+
record.summaryLong,
|
|
449
|
+
JSON.stringify(record.openLoops),
|
|
450
|
+
JSON.stringify(record.labels),
|
|
451
|
+
record.createdAt,
|
|
452
|
+
record.lastActiveAt
|
|
453
|
+
);
|
|
454
|
+
}
|
|
455
|
+
async upsertTopicMembership(record) {
|
|
456
|
+
this.db.prepare(
|
|
457
|
+
`INSERT INTO topic_membership (
|
|
458
|
+
message_id,
|
|
459
|
+
topic_id,
|
|
460
|
+
score,
|
|
461
|
+
is_primary,
|
|
462
|
+
reason,
|
|
463
|
+
created_at
|
|
464
|
+
) VALUES (?, ?, ?, ?, ?, ?)
|
|
465
|
+
ON CONFLICT(message_id, topic_id) DO UPDATE SET
|
|
466
|
+
score = excluded.score,
|
|
467
|
+
is_primary = excluded.is_primary,
|
|
468
|
+
reason = excluded.reason,
|
|
469
|
+
created_at = excluded.created_at`
|
|
470
|
+
).run(
|
|
471
|
+
record.messageId,
|
|
472
|
+
record.topicId,
|
|
473
|
+
record.score,
|
|
474
|
+
record.isPrimary ? 1 : 0,
|
|
475
|
+
record.reason,
|
|
476
|
+
record.createdAt
|
|
477
|
+
);
|
|
478
|
+
}
|
|
479
|
+
async upsertFact(record, tags = []) {
|
|
480
|
+
this.db.exec("BEGIN");
|
|
481
|
+
try {
|
|
482
|
+
this.db.prepare(
|
|
483
|
+
`INSERT INTO facts (
|
|
484
|
+
id,
|
|
485
|
+
scope,
|
|
486
|
+
category,
|
|
487
|
+
key,
|
|
488
|
+
value,
|
|
489
|
+
sensitivity,
|
|
490
|
+
recall_policy,
|
|
491
|
+
confidence,
|
|
492
|
+
source_message_id,
|
|
493
|
+
source_topic_id,
|
|
494
|
+
updated_at
|
|
495
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
496
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
497
|
+
scope = excluded.scope,
|
|
498
|
+
category = excluded.category,
|
|
499
|
+
key = excluded.key,
|
|
500
|
+
value = excluded.value,
|
|
501
|
+
sensitivity = excluded.sensitivity,
|
|
502
|
+
recall_policy = excluded.recall_policy,
|
|
503
|
+
confidence = excluded.confidence,
|
|
504
|
+
source_message_id = excluded.source_message_id,
|
|
505
|
+
source_topic_id = excluded.source_topic_id,
|
|
506
|
+
updated_at = excluded.updated_at`
|
|
507
|
+
).run(
|
|
508
|
+
record.id,
|
|
509
|
+
record.scope,
|
|
510
|
+
record.category,
|
|
511
|
+
record.key,
|
|
512
|
+
record.value,
|
|
513
|
+
record.sensitivity,
|
|
514
|
+
record.recallPolicy,
|
|
515
|
+
record.confidence,
|
|
516
|
+
record.sourceMessageId,
|
|
517
|
+
record.sourceTopicId,
|
|
518
|
+
record.updatedAt
|
|
519
|
+
);
|
|
520
|
+
this.db.prepare(`DELETE FROM fact_tags WHERE fact_id = ?`).run(record.id);
|
|
521
|
+
const insertTag = this.db.prepare(
|
|
522
|
+
`INSERT INTO fact_tags (fact_id, tag) VALUES (?, ?)`
|
|
523
|
+
);
|
|
524
|
+
for (const tag of tags) {
|
|
525
|
+
insertTag.run(record.id, tag);
|
|
526
|
+
}
|
|
527
|
+
this.db.exec("COMMIT");
|
|
528
|
+
} catch (error) {
|
|
529
|
+
this.db.exec("ROLLBACK");
|
|
530
|
+
throw error;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
async listFactsByScope(scope) {
|
|
534
|
+
const rows = this.db.prepare(
|
|
535
|
+
`SELECT
|
|
536
|
+
f.id,
|
|
537
|
+
f.scope,
|
|
538
|
+
f.category,
|
|
539
|
+
f.key,
|
|
540
|
+
f.value,
|
|
541
|
+
f.sensitivity,
|
|
542
|
+
f.recall_policy,
|
|
543
|
+
f.confidence,
|
|
544
|
+
f.source_message_id,
|
|
545
|
+
f.source_topic_id,
|
|
546
|
+
f.updated_at,
|
|
547
|
+
COALESCE(json_group_array(ft.tag) FILTER (WHERE ft.tag IS NOT NULL), '[]') AS tags_json
|
|
548
|
+
FROM facts f
|
|
549
|
+
LEFT JOIN fact_tags ft ON ft.fact_id = f.id
|
|
550
|
+
WHERE f.scope = ?
|
|
551
|
+
GROUP BY
|
|
552
|
+
f.id,
|
|
553
|
+
f.scope,
|
|
554
|
+
f.category,
|
|
555
|
+
f.key,
|
|
556
|
+
f.value,
|
|
557
|
+
f.sensitivity,
|
|
558
|
+
f.recall_policy,
|
|
559
|
+
f.confidence,
|
|
560
|
+
f.source_message_id,
|
|
561
|
+
f.source_topic_id,
|
|
562
|
+
f.updated_at
|
|
563
|
+
ORDER BY f.updated_at DESC`
|
|
564
|
+
).all(scope);
|
|
565
|
+
return rows.map(mapFactRow);
|
|
566
|
+
}
|
|
567
|
+
async listFactsByTags(tags, recallPolicies = ["always", "topic_bound"]) {
|
|
568
|
+
if (tags.length === 0) {
|
|
569
|
+
return [];
|
|
570
|
+
}
|
|
571
|
+
const tagPlaceholders = tags.map(() => "?").join(", ");
|
|
572
|
+
const recallPolicyPlaceholders = recallPolicies.map(() => "?").join(", ");
|
|
573
|
+
const rows = this.db.prepare(
|
|
574
|
+
`SELECT
|
|
575
|
+
f.id,
|
|
576
|
+
f.scope,
|
|
577
|
+
f.category,
|
|
578
|
+
f.key,
|
|
579
|
+
f.value,
|
|
580
|
+
f.sensitivity,
|
|
581
|
+
f.recall_policy,
|
|
582
|
+
f.confidence,
|
|
583
|
+
f.source_message_id,
|
|
584
|
+
f.source_topic_id,
|
|
585
|
+
f.updated_at,
|
|
586
|
+
COALESCE(json_group_array(DISTINCT ft_all.tag) FILTER (WHERE ft_all.tag IS NOT NULL), '[]') AS tags_json
|
|
587
|
+
FROM facts f
|
|
588
|
+
JOIN fact_tags ft_match ON ft_match.fact_id = f.id
|
|
589
|
+
LEFT JOIN fact_tags ft_all ON ft_all.fact_id = f.id
|
|
590
|
+
WHERE ft_match.tag IN (${tagPlaceholders})
|
|
591
|
+
AND f.recall_policy IN (${recallPolicyPlaceholders})
|
|
592
|
+
GROUP BY
|
|
593
|
+
f.id,
|
|
594
|
+
f.scope,
|
|
595
|
+
f.category,
|
|
596
|
+
f.key,
|
|
597
|
+
f.value,
|
|
598
|
+
f.sensitivity,
|
|
599
|
+
f.recall_policy,
|
|
600
|
+
f.confidence,
|
|
601
|
+
f.source_message_id,
|
|
602
|
+
f.source_topic_id,
|
|
603
|
+
f.updated_at
|
|
604
|
+
ORDER BY f.updated_at DESC`
|
|
605
|
+
).all(...tags, ...recallPolicies);
|
|
606
|
+
return rows.map(mapFactRow);
|
|
607
|
+
}
|
|
608
|
+
async listRecentMessagesForTopic(topicId, limit) {
|
|
609
|
+
const rows = this.db.prepare(
|
|
610
|
+
`SELECT
|
|
611
|
+
tm.message_id,
|
|
612
|
+
tm.topic_id,
|
|
613
|
+
tm.score,
|
|
614
|
+
tm.is_primary,
|
|
615
|
+
tm.reason,
|
|
616
|
+
tm.created_at,
|
|
617
|
+
m.id,
|
|
618
|
+
m.session_id,
|
|
619
|
+
m.turn_id,
|
|
620
|
+
m.parent_turn_id,
|
|
621
|
+
m.role,
|
|
622
|
+
m.event_type,
|
|
623
|
+
m.text,
|
|
624
|
+
m.ts,
|
|
625
|
+
m.raw_json
|
|
626
|
+
FROM topic_membership tm
|
|
627
|
+
JOIN messages m ON m.id = tm.message_id
|
|
628
|
+
WHERE tm.topic_id = ?
|
|
629
|
+
ORDER BY m.ts DESC
|
|
630
|
+
LIMIT ?`
|
|
631
|
+
).all(topicId, limit);
|
|
632
|
+
return rows.map(mapRecentTopicMessageRow).reverse();
|
|
633
|
+
}
|
|
634
|
+
async searchTopics(sessionId, query, limit) {
|
|
635
|
+
const normalizedQuery = normalizeSearchQuery(query);
|
|
636
|
+
if (!normalizedQuery) {
|
|
637
|
+
return [];
|
|
638
|
+
}
|
|
639
|
+
const likeValue = `%${escapeLike(normalizedQuery)}%`;
|
|
640
|
+
const rows = this.db.prepare(
|
|
641
|
+
`SELECT
|
|
642
|
+
id,
|
|
643
|
+
session_id,
|
|
644
|
+
title,
|
|
645
|
+
status,
|
|
646
|
+
parent_topic_id,
|
|
647
|
+
summary_short,
|
|
648
|
+
summary_long,
|
|
649
|
+
open_loops_json,
|
|
650
|
+
labels_json,
|
|
651
|
+
created_at,
|
|
652
|
+
last_active_at
|
|
653
|
+
FROM topics
|
|
654
|
+
WHERE session_id = ?
|
|
655
|
+
AND (
|
|
656
|
+
lower(title) LIKE ? ESCAPE '\\'
|
|
657
|
+
OR lower(summary_short) LIKE ? ESCAPE '\\'
|
|
658
|
+
OR lower(summary_long) LIKE ? ESCAPE '\\'
|
|
659
|
+
OR lower(labels_json) LIKE ? ESCAPE '\\'
|
|
660
|
+
)
|
|
661
|
+
ORDER BY last_active_at DESC
|
|
662
|
+
LIMIT ?`
|
|
663
|
+
).all(sessionId, likeValue, likeValue, likeValue, likeValue, limit);
|
|
664
|
+
return rows.map((row) => mapTopicSearchResult(row, normalizedQuery)).sort((a, b) => b.score - a.score);
|
|
665
|
+
}
|
|
666
|
+
async searchFacts(args) {
|
|
667
|
+
const normalizedQuery = normalizeSearchQuery(args.query);
|
|
668
|
+
if (!normalizedQuery) {
|
|
669
|
+
return [];
|
|
670
|
+
}
|
|
671
|
+
const likeValue = `%${escapeLike(normalizedQuery)}%`;
|
|
672
|
+
const topicScope = args.topicId ? `topic:${args.topicId}` : null;
|
|
673
|
+
const sessionScope = `session:${args.sessionId}`;
|
|
674
|
+
const rows = this.db.prepare(
|
|
675
|
+
`SELECT
|
|
676
|
+
f.id,
|
|
677
|
+
f.scope,
|
|
678
|
+
f.category,
|
|
679
|
+
f.key,
|
|
680
|
+
f.value,
|
|
681
|
+
f.sensitivity,
|
|
682
|
+
f.recall_policy,
|
|
683
|
+
f.confidence,
|
|
684
|
+
f.source_message_id,
|
|
685
|
+
f.source_topic_id,
|
|
686
|
+
f.updated_at,
|
|
687
|
+
COALESCE(json_group_array(DISTINCT ft_all.tag) FILTER (WHERE ft_all.tag IS NOT NULL), '[]') AS tags_json
|
|
688
|
+
FROM facts f
|
|
689
|
+
LEFT JOIN fact_tags ft_match ON ft_match.fact_id = f.id
|
|
690
|
+
LEFT JOIN fact_tags ft_all ON ft_all.fact_id = f.id
|
|
691
|
+
WHERE (
|
|
692
|
+
f.scope IN ('global', 'shared')
|
|
693
|
+
OR f.scope = ?
|
|
694
|
+
OR f.scope = ?
|
|
695
|
+
OR f.source_topic_id IN (
|
|
696
|
+
SELECT id FROM topics WHERE session_id = ?
|
|
697
|
+
)
|
|
698
|
+
)
|
|
699
|
+
AND (
|
|
700
|
+
lower(f.key) LIKE ? ESCAPE '\\'
|
|
701
|
+
OR lower(f.value) LIKE ? ESCAPE '\\'
|
|
702
|
+
OR lower(f.category) LIKE ? ESCAPE '\\'
|
|
703
|
+
OR lower(COALESCE(ft_match.tag, '')) LIKE ? ESCAPE '\\'
|
|
704
|
+
)
|
|
705
|
+
GROUP BY
|
|
706
|
+
f.id,
|
|
707
|
+
f.scope,
|
|
708
|
+
f.category,
|
|
709
|
+
f.key,
|
|
710
|
+
f.value,
|
|
711
|
+
f.sensitivity,
|
|
712
|
+
f.recall_policy,
|
|
713
|
+
f.confidence,
|
|
714
|
+
f.source_message_id,
|
|
715
|
+
f.source_topic_id,
|
|
716
|
+
f.updated_at
|
|
717
|
+
ORDER BY f.updated_at DESC
|
|
718
|
+
LIMIT ?`
|
|
719
|
+
).all(sessionScope, topicScope, args.sessionId, likeValue, likeValue, likeValue, likeValue, args.limit);
|
|
720
|
+
return rows.map((row) => mapFactSearchResult(row, normalizedQuery)).sort((a, b) => b.score - a.score);
|
|
721
|
+
}
|
|
722
|
+
};
|
|
723
|
+
async function loadSchemaSql() {
|
|
724
|
+
const candidatePaths = [
|
|
725
|
+
(0, import_node_url.fileURLToPath)(new URL("./schema.sql", importMetaUrl)),
|
|
726
|
+
(0, import_node_url.fileURLToPath)(new URL("../src/schema.sql", importMetaUrl))
|
|
727
|
+
];
|
|
728
|
+
for (const schemaPath of candidatePaths) {
|
|
729
|
+
try {
|
|
730
|
+
await (0, import_promises.access)(schemaPath);
|
|
731
|
+
return (0, import_promises.readFile)(schemaPath, "utf8");
|
|
732
|
+
} catch {
|
|
733
|
+
continue;
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
throw new Error("Unable to locate memory-v2 SQLite schema.sql");
|
|
737
|
+
}
|
|
738
|
+
function mapSessionStateRow(row) {
|
|
739
|
+
return {
|
|
740
|
+
sessionId: row.session_id,
|
|
741
|
+
activeTopicId: row.active_topic_id,
|
|
742
|
+
lastCompactedAt: row.last_compacted_at,
|
|
743
|
+
lastTurnId: row.last_turn_id,
|
|
744
|
+
updatedAt: row.updated_at
|
|
745
|
+
};
|
|
746
|
+
}
|
|
747
|
+
function mapTopicRow(row) {
|
|
748
|
+
return {
|
|
749
|
+
id: row.id,
|
|
750
|
+
sessionId: row.session_id,
|
|
751
|
+
title: row.title,
|
|
752
|
+
status: row.status,
|
|
753
|
+
parentTopicId: row.parent_topic_id,
|
|
754
|
+
summaryShort: row.summary_short,
|
|
755
|
+
summaryLong: row.summary_long,
|
|
756
|
+
openLoops: parseJsonArray(row.open_loops_json),
|
|
757
|
+
labels: parseJsonArray(row.labels_json),
|
|
758
|
+
createdAt: row.created_at,
|
|
759
|
+
lastActiveAt: row.last_active_at
|
|
760
|
+
};
|
|
761
|
+
}
|
|
762
|
+
function mapFactRow(row) {
|
|
763
|
+
return {
|
|
764
|
+
id: row.id,
|
|
765
|
+
scope: row.scope,
|
|
766
|
+
category: row.category,
|
|
767
|
+
key: row.key,
|
|
768
|
+
value: row.value,
|
|
769
|
+
sensitivity: row.sensitivity,
|
|
770
|
+
recallPolicy: row.recall_policy,
|
|
771
|
+
confidence: row.confidence,
|
|
772
|
+
sourceMessageId: row.source_message_id,
|
|
773
|
+
sourceTopicId: row.source_topic_id,
|
|
774
|
+
updatedAt: row.updated_at,
|
|
775
|
+
tags: parseJsonArray(row.tags_json)
|
|
776
|
+
};
|
|
777
|
+
}
|
|
778
|
+
function mapTopicSearchResult(row, normalizedQuery) {
|
|
779
|
+
const topic = mapTopicRow(row);
|
|
780
|
+
const haystacks = [
|
|
781
|
+
{ reason: "title", value: topic.title },
|
|
782
|
+
{ reason: "summary_short", value: topic.summaryShort },
|
|
783
|
+
{ reason: "summary_long", value: topic.summaryLong },
|
|
784
|
+
{ reason: "labels", value: topic.labels.join(" ") }
|
|
785
|
+
];
|
|
786
|
+
const matchReasons = haystacks.filter((entry) => entry.value.toLowerCase().includes(normalizedQuery)).map((entry) => entry.reason);
|
|
787
|
+
return {
|
|
788
|
+
topic,
|
|
789
|
+
score: scoreTopicSearch(topic, matchReasons),
|
|
790
|
+
matchReasons
|
|
791
|
+
};
|
|
792
|
+
}
|
|
793
|
+
function mapFactSearchResult(row, normalizedQuery) {
|
|
794
|
+
const fact = mapFactRow(row);
|
|
795
|
+
const haystacks = [
|
|
796
|
+
{ reason: "key", value: fact.key },
|
|
797
|
+
{ reason: "value", value: fact.value },
|
|
798
|
+
{ reason: "category", value: fact.category },
|
|
799
|
+
{ reason: "tags", value: fact.tags.join(" ") }
|
|
800
|
+
];
|
|
801
|
+
const matchReasons = haystacks.filter((entry) => entry.value.toLowerCase().includes(normalizedQuery)).map((entry) => entry.reason);
|
|
802
|
+
return {
|
|
803
|
+
fact,
|
|
804
|
+
score: scoreFactSearch(fact, matchReasons),
|
|
805
|
+
matchReasons
|
|
806
|
+
};
|
|
807
|
+
}
|
|
808
|
+
function mapRecentTopicMessageRow(row) {
|
|
809
|
+
return {
|
|
810
|
+
membership: {
|
|
811
|
+
messageId: row.message_id,
|
|
812
|
+
topicId: row.topic_id,
|
|
813
|
+
score: row.score,
|
|
814
|
+
isPrimary: row.is_primary === 1,
|
|
815
|
+
reason: row.reason,
|
|
816
|
+
createdAt: row.created_at
|
|
817
|
+
},
|
|
818
|
+
message: {
|
|
819
|
+
id: row.id,
|
|
820
|
+
sessionId: row.session_id,
|
|
821
|
+
turnId: row.turn_id,
|
|
822
|
+
parentTurnId: row.parent_turn_id,
|
|
823
|
+
role: row.role,
|
|
824
|
+
eventType: row.event_type,
|
|
825
|
+
text: row.text,
|
|
826
|
+
ts: row.ts,
|
|
827
|
+
rawJson: row.raw_json
|
|
828
|
+
}
|
|
829
|
+
};
|
|
830
|
+
}
|
|
831
|
+
function parseJsonArray(value) {
|
|
832
|
+
const parsed = JSON.parse(value);
|
|
833
|
+
if (!Array.isArray(parsed)) {
|
|
834
|
+
return [];
|
|
835
|
+
}
|
|
836
|
+
return parsed.filter((item) => typeof item === "string");
|
|
837
|
+
}
|
|
838
|
+
function normalizeSearchQuery(query) {
|
|
839
|
+
return query.trim().toLowerCase();
|
|
840
|
+
}
|
|
841
|
+
function escapeLike(value) {
|
|
842
|
+
return value.replace(/[\\%_]/g, "\\$&");
|
|
843
|
+
}
|
|
844
|
+
function scoreTopicSearch(topic, matchReasons) {
|
|
845
|
+
let score = 0;
|
|
846
|
+
for (const reason of matchReasons) {
|
|
847
|
+
if (reason === "title") {
|
|
848
|
+
score += 5;
|
|
849
|
+
} else if (reason === "labels") {
|
|
850
|
+
score += 4;
|
|
851
|
+
} else if (reason === "summary_short") {
|
|
852
|
+
score += 3;
|
|
853
|
+
} else if (reason === "summary_long") {
|
|
854
|
+
score += 2;
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
if (topic.status === "active") {
|
|
858
|
+
score += 1;
|
|
859
|
+
}
|
|
860
|
+
return score;
|
|
861
|
+
}
|
|
862
|
+
function scoreFactSearch(fact, matchReasons) {
|
|
863
|
+
let score = 0;
|
|
864
|
+
for (const reason of matchReasons) {
|
|
865
|
+
if (reason === "key") {
|
|
866
|
+
score += 5;
|
|
867
|
+
} else if (reason === "tags") {
|
|
868
|
+
score += 4;
|
|
869
|
+
} else if (reason === "value") {
|
|
870
|
+
score += 3;
|
|
871
|
+
} else if (reason === "category") {
|
|
872
|
+
score += 1;
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
if (fact.recallPolicy === "always") {
|
|
876
|
+
score += 1;
|
|
877
|
+
}
|
|
878
|
+
return score;
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
// ../../packages/summary-refresher/src/index.ts
|
|
882
|
+
var SummaryRefresher = class {
|
|
883
|
+
constructor(_config) {
|
|
884
|
+
this._config = _config;
|
|
885
|
+
}
|
|
886
|
+
refresh(input) {
|
|
887
|
+
const latestMessage = input.recentMessages.at(-1)?.message.text ?? input.topic.summaryShort;
|
|
888
|
+
const factFragments = input.facts.slice(0, 2).map((fact) => `${fact.key}=${fact.value}`).join("; ");
|
|
889
|
+
const loopFragment = input.topic.openLoops.length > 0 ? ` Open loops: ${input.topic.openLoops.slice(-2).join(" | ")}.` : "";
|
|
890
|
+
const summaryShort = truncate(
|
|
891
|
+
[input.topic.title, latestMessage, factFragments].filter(Boolean).join(" | "),
|
|
892
|
+
220
|
|
893
|
+
);
|
|
894
|
+
const summaryLong = truncate(
|
|
895
|
+
`${summaryShort}${loopFragment}`,
|
|
896
|
+
600
|
|
897
|
+
);
|
|
898
|
+
return {
|
|
899
|
+
summaryShort,
|
|
900
|
+
summaryLong
|
|
901
|
+
};
|
|
902
|
+
}
|
|
903
|
+
};
|
|
904
|
+
function truncate(value, max) {
|
|
905
|
+
return value.length <= max ? value : `${value.slice(0, max)}...`;
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
// ../../packages/topic-router/src/index.ts
|
|
909
|
+
var TopicRouter = class {
|
|
910
|
+
constructor(config) {
|
|
911
|
+
this.config = config;
|
|
912
|
+
}
|
|
913
|
+
route(input) {
|
|
914
|
+
const normalizedText = normalize(input.text);
|
|
915
|
+
const hasShiftSignal = containsShiftSignal(normalizedText);
|
|
916
|
+
const hasExplicitNewTopicSignal = containsExplicitNewTopicSignal(normalizedText);
|
|
917
|
+
if (!normalizedText) {
|
|
918
|
+
if (input.activeTopicId) {
|
|
919
|
+
return {
|
|
920
|
+
action: "continue",
|
|
921
|
+
topicId: input.activeTopicId,
|
|
922
|
+
reason: "empty-message-falls-back-to-active-topic"
|
|
923
|
+
};
|
|
924
|
+
}
|
|
925
|
+
return {
|
|
926
|
+
action: "spawn",
|
|
927
|
+
topicId: null,
|
|
928
|
+
reason: "empty-message-without-active-topic"
|
|
929
|
+
};
|
|
930
|
+
}
|
|
931
|
+
const activeTopic = input.recentTopics.find(
|
|
932
|
+
(topic) => topic.id === input.activeTopicId
|
|
933
|
+
);
|
|
934
|
+
if (hasExplicitNewTopicSignal) {
|
|
935
|
+
return {
|
|
936
|
+
action: "spawn",
|
|
937
|
+
topicId: null,
|
|
938
|
+
reason: "explicit-new-topic-signal"
|
|
939
|
+
};
|
|
940
|
+
}
|
|
941
|
+
if (activeTopic && !shouldBreakFromActive(activeTopic, normalizedText, hasShiftSignal) && scoreTopicMatch(activeTopic, normalizedText, {
|
|
942
|
+
rank: 0,
|
|
943
|
+
isActive: true
|
|
944
|
+
}) >= this.getContinueThreshold()) {
|
|
945
|
+
return {
|
|
946
|
+
action: "continue",
|
|
947
|
+
topicId: activeTopic.id,
|
|
948
|
+
reason: "matched-active-topic"
|
|
949
|
+
};
|
|
950
|
+
}
|
|
951
|
+
const matchedExisting = findBestTopicMatch(
|
|
952
|
+
input.recentTopics,
|
|
953
|
+
normalizedText,
|
|
954
|
+
this.getSwitchThreshold(),
|
|
955
|
+
input.activeTopicId
|
|
956
|
+
);
|
|
957
|
+
if (matchedExisting) {
|
|
958
|
+
return {
|
|
959
|
+
action: "switch",
|
|
960
|
+
topicId: matchedExisting.id,
|
|
961
|
+
reason: "matched-existing-topic"
|
|
962
|
+
};
|
|
963
|
+
}
|
|
964
|
+
return {
|
|
965
|
+
action: "spawn",
|
|
966
|
+
topicId: null,
|
|
967
|
+
reason: "no-topic-match-above-threshold"
|
|
968
|
+
};
|
|
969
|
+
}
|
|
970
|
+
getSwitchThreshold() {
|
|
971
|
+
return this.config.topicRouting?.switchTopicThreshold ?? 0.68;
|
|
972
|
+
}
|
|
973
|
+
getContinueThreshold() {
|
|
974
|
+
const configured = this.config.topicRouting?.newTopicThreshold;
|
|
975
|
+
if (typeof configured === "number") {
|
|
976
|
+
return configured;
|
|
977
|
+
}
|
|
978
|
+
return Math.max(0.22, this.getSwitchThreshold() * 0.55);
|
|
979
|
+
}
|
|
980
|
+
};
|
|
981
|
+
function findBestTopicMatch(topics, normalizedText, threshold, activeTopicId) {
|
|
982
|
+
let bestTopic = null;
|
|
983
|
+
let bestScore = 0;
|
|
984
|
+
let rank = 0;
|
|
985
|
+
for (const topic of topics) {
|
|
986
|
+
if (topic.id === activeTopicId) {
|
|
987
|
+
continue;
|
|
988
|
+
}
|
|
989
|
+
const score = scoreTopicMatch(topic, normalizedText, {
|
|
990
|
+
rank,
|
|
991
|
+
isActive: false
|
|
992
|
+
});
|
|
993
|
+
if (score >= threshold && score > bestScore) {
|
|
994
|
+
bestScore = score;
|
|
995
|
+
bestTopic = topic;
|
|
996
|
+
}
|
|
997
|
+
rank += 1;
|
|
998
|
+
}
|
|
999
|
+
return bestTopic;
|
|
1000
|
+
}
|
|
1001
|
+
function scoreTopicMatch(topic, normalizedText, context) {
|
|
1002
|
+
const textTokens = tokenizeMeaningful(normalizedText);
|
|
1003
|
+
const candidateTexts = [
|
|
1004
|
+
topic.title,
|
|
1005
|
+
topic.summaryShort,
|
|
1006
|
+
topic.summaryLong,
|
|
1007
|
+
...topic.openLoops
|
|
1008
|
+
].map(normalize);
|
|
1009
|
+
const candidateTokens = tokenizeMeaningful(
|
|
1010
|
+
[topic.title, topic.summaryShort, topic.summaryLong, ...topic.labels, ...topic.openLoops].map(normalize).join(" ")
|
|
1011
|
+
);
|
|
1012
|
+
const labelTokens = tokenizeMeaningful(topic.labels.join(" "));
|
|
1013
|
+
let bestPhraseScore = 0;
|
|
1014
|
+
for (const candidate of candidateTexts) {
|
|
1015
|
+
if (!candidate) {
|
|
1016
|
+
continue;
|
|
1017
|
+
}
|
|
1018
|
+
if (candidate.includes(normalizedText) || normalizedText.includes(candidate)) {
|
|
1019
|
+
bestPhraseScore = Math.max(
|
|
1020
|
+
bestPhraseScore,
|
|
1021
|
+
overlapScore(candidate, normalizedText)
|
|
1022
|
+
);
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
const tokenIntersection = countIntersection(candidateTokens, textTokens);
|
|
1026
|
+
const labelIntersection = countIntersection(labelTokens, textTokens);
|
|
1027
|
+
const tokenScore = tokenIntersection === 0 ? 0 : tokenIntersection / Math.max(1, Math.min(candidateTokens.size, textTokens.size));
|
|
1028
|
+
const labelScore = labelIntersection === 0 ? 0 : labelIntersection / Math.max(1, Math.min(labelTokens.size, textTokens.size));
|
|
1029
|
+
const recencyBonus = context.rank === 0 ? 0.12 : context.rank === 1 ? 0.06 : 0;
|
|
1030
|
+
const activeBonus = context.isActive ? 0.18 : 0;
|
|
1031
|
+
const loopBonus = topic.openLoops.length > 0 && /继续|待办|下一步|todo|follow up/i.test(normalizedText) ? 0.08 : 0;
|
|
1032
|
+
return clamp01(
|
|
1033
|
+
bestPhraseScore * 0.5 + tokenScore * 0.6 + labelScore * 0.45 + recencyBonus + activeBonus + loopBonus
|
|
1034
|
+
);
|
|
1035
|
+
}
|
|
1036
|
+
function shouldBreakFromActive(topic, normalizedText, hasShiftSignal) {
|
|
1037
|
+
if (!hasShiftSignal) {
|
|
1038
|
+
return false;
|
|
1039
|
+
}
|
|
1040
|
+
const activeTokens = tokenizeMeaningful(
|
|
1041
|
+
[topic.title, topic.summaryShort, ...topic.labels].map(normalize).join(" ")
|
|
1042
|
+
);
|
|
1043
|
+
const textTokens = tokenizeMeaningful(normalizedText);
|
|
1044
|
+
const overlap = countIntersection(activeTokens, textTokens);
|
|
1045
|
+
return overlap <= 1;
|
|
1046
|
+
}
|
|
1047
|
+
function overlapScore(left, right) {
|
|
1048
|
+
const smaller = Math.min(left.length, right.length);
|
|
1049
|
+
const larger = Math.max(left.length, right.length);
|
|
1050
|
+
return smaller / larger;
|
|
1051
|
+
}
|
|
1052
|
+
function tokenizeMeaningful(value) {
|
|
1053
|
+
return new Set(
|
|
1054
|
+
value.split(/[^a-z0-9_\u4e00-\u9fff]+/i).map((token) => token.trim()).filter((token) => token.length >= 2).filter((token) => !STOP_TOKENS.has(token))
|
|
1055
|
+
);
|
|
1056
|
+
}
|
|
1057
|
+
function normalize(value) {
|
|
1058
|
+
return value.trim().toLowerCase();
|
|
1059
|
+
}
|
|
1060
|
+
function containsShiftSignal(value) {
|
|
1061
|
+
return [
|
|
1062
|
+
"\u5207\u5230",
|
|
1063
|
+
"\u5207\u56DE",
|
|
1064
|
+
"\u8F6C\u5230",
|
|
1065
|
+
"\u6362\u5230",
|
|
1066
|
+
"\u6362\u4E2A\u8BDD\u9898",
|
|
1067
|
+
"\u6362\u4E00\u4E2A\u8BDD\u9898",
|
|
1068
|
+
"\u804A\u804A",
|
|
1069
|
+
"\u56DE\u5230",
|
|
1070
|
+
"\u91CD\u65B0\u804A",
|
|
1071
|
+
"\u6362\u4E2A\u4E3B\u9898",
|
|
1072
|
+
"\u5207\u6362\u5230",
|
|
1073
|
+
"switch to",
|
|
1074
|
+
"move to",
|
|
1075
|
+
"back to",
|
|
1076
|
+
"new topic"
|
|
1077
|
+
].some((marker) => value.includes(marker));
|
|
1078
|
+
}
|
|
1079
|
+
function containsExplicitNewTopicSignal(value) {
|
|
1080
|
+
return [
|
|
1081
|
+
"\u65B0\u7684 topic",
|
|
1082
|
+
"\u65B0\u7684topic",
|
|
1083
|
+
"\u65B0 topic",
|
|
1084
|
+
"\u65B0topic",
|
|
1085
|
+
"\u8FD9\u662F\u4E00\u4E2A\u65B0\u7684",
|
|
1086
|
+
"\u8FD9\u662F\u65B0\u7684\u8BDD\u9898",
|
|
1087
|
+
"\u8FD9\u662F\u4E00\u4E2A\u65B0\u7684\u8BDD\u9898",
|
|
1088
|
+
"\u5F00\u542F\u4E00\u4E2A\u65B0\u8BDD\u9898",
|
|
1089
|
+
"\u5F00\u59CB\u4E00\u4E2A\u65B0\u8BDD\u9898",
|
|
1090
|
+
"\u5F00\u4E00\u4E2A\u65B0\u8BDD\u9898",
|
|
1091
|
+
"\u6362\u4E2A\u65B0\u8BDD\u9898",
|
|
1092
|
+
"\u6211\u4EEC\u5F00\u542F\u4E00\u4E2A\u65B0\u8BDD\u9898",
|
|
1093
|
+
"\u73B0\u5728\u5F00\u59CB\u4E00\u4E2A\u65B0\u8BDD\u9898",
|
|
1094
|
+
"\u4ECE\u8FD9\u9875\u91CD\u65B0\u5F00\u59CB",
|
|
1095
|
+
"\u91CD\u65B0\u5F00\u59CB\u4E00\u4E2A\u8BDD\u9898",
|
|
1096
|
+
"let's start a new topic",
|
|
1097
|
+
"start a new topic",
|
|
1098
|
+
"this is a new topic"
|
|
1099
|
+
].some((marker) => value.includes(marker));
|
|
1100
|
+
}
|
|
1101
|
+
function countIntersection(left, right) {
|
|
1102
|
+
return [...left].filter((token) => right.has(token)).length;
|
|
1103
|
+
}
|
|
1104
|
+
function clamp01(value) {
|
|
1105
|
+
return Math.max(0, Math.min(1, value));
|
|
1106
|
+
}
|
|
1107
|
+
var STOP_TOKENS = /* @__PURE__ */ new Set([
|
|
1108
|
+
"\u5F53\u524D",
|
|
1109
|
+
"\u8FD9\u4E2A",
|
|
1110
|
+
"\u6211\u4EEC",
|
|
1111
|
+
"\u7EE7\u7EED",
|
|
1112
|
+
"\u8BA8\u8BBA",
|
|
1113
|
+
"\u5904\u7406",
|
|
1114
|
+
"\u4E00\u4E0B",
|
|
1115
|
+
"\u4E00\u4E2A",
|
|
1116
|
+
"topic",
|
|
1117
|
+
"topics"
|
|
1118
|
+
]);
|
|
1119
|
+
|
|
1120
|
+
// ../bamdra-memory-context-engine/src/index.ts
|
|
1121
|
+
var import_node_crypto = require("crypto");
|
|
1122
|
+
var import_node_os = require("os");
|
|
1123
|
+
var import_node_path2 = require("node:path");
|
|
1124
|
+
var DEFAULT_DB_PATH = (0, import_node_path2.join)(
|
|
1125
|
+
(0, import_node_os.homedir)(),
|
|
1126
|
+
".openclaw",
|
|
1127
|
+
"memory",
|
|
1128
|
+
process.env.OPENCLAW_BAMDRA_MEMORY_DB_BASENAME || "main.sqlite"
|
|
1129
|
+
);
|
|
1130
|
+
function logMemoryEvent(event, details = {}) {
|
|
1131
|
+
try {
|
|
1132
|
+
console.info("[bamdra-memory]", event, JSON.stringify(details));
|
|
1133
|
+
} catch {
|
|
1134
|
+
console.info("[bamdra-memory]", event);
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
function createContextEngineMemoryV2Plugin(inputConfig, api) {
|
|
1138
|
+
const config = normalizeMemoryConfig(inputConfig);
|
|
1139
|
+
const store = new MemorySqliteStore({
|
|
1140
|
+
path: config.store.path
|
|
1141
|
+
});
|
|
1142
|
+
let cache = new InMemoryCacheStore(config.cache);
|
|
1143
|
+
const router = new TopicRouter(config);
|
|
1144
|
+
const assembler = new ContextAssembler(config);
|
|
1145
|
+
const factExtractor = new FactExtractor(config);
|
|
1146
|
+
const summaryRefresher = new SummaryRefresher(config);
|
|
1147
|
+
let migrationsApplied = false;
|
|
1148
|
+
let migrationsPromise = null;
|
|
1149
|
+
async function ensureMigrations() {
|
|
1150
|
+
if (migrationsApplied) return;
|
|
1151
|
+
if (migrationsPromise) return migrationsPromise;
|
|
1152
|
+
migrationsPromise = store.applyMigrations().then(() => {
|
|
1153
|
+
migrationsApplied = true;
|
|
1154
|
+
});
|
|
1155
|
+
return migrationsPromise;
|
|
1156
|
+
}
|
|
1157
|
+
let hooksRegistered = false;
|
|
1158
|
+
const plugin = {
|
|
1159
|
+
name: "bamdra-memory-context-engine",
|
|
1160
|
+
type: "context-engine",
|
|
1161
|
+
slot: "memory",
|
|
1162
|
+
capabilities: ["memory"],
|
|
1163
|
+
config,
|
|
1164
|
+
registerHooks(hostApi) {
|
|
1165
|
+
if (hooksRegistered) {
|
|
1166
|
+
return;
|
|
1167
|
+
}
|
|
1168
|
+
const registerHook = getInternalHookRegistrar(hostApi);
|
|
1169
|
+
const registerTypedHook = getTypedHookRegistrar(hostApi);
|
|
1170
|
+
if (registerHook) {
|
|
1171
|
+
registerHook(
|
|
1172
|
+
["message:received", "message:preprocessed"],
|
|
1173
|
+
async (event) => {
|
|
1174
|
+
const sessionId = getSessionIdFromHookContext(event);
|
|
1175
|
+
const text = getTextFromHookContext(event);
|
|
1176
|
+
logMemoryEvent("hook-ingest-received", {
|
|
1177
|
+
hasSessionId: Boolean(sessionId),
|
|
1178
|
+
textPreview: typeof text === "string" ? text.slice(0, 80) : null
|
|
1179
|
+
});
|
|
1180
|
+
if (!sessionId || !text) {
|
|
1181
|
+
return;
|
|
1182
|
+
}
|
|
1183
|
+
const result = await plugin.routeAndTrack(sessionId, text);
|
|
1184
|
+
logMemoryEvent("hook-ingest-tracked", {
|
|
1185
|
+
sessionId,
|
|
1186
|
+
action: result.decision.action,
|
|
1187
|
+
reason: result.decision.reason,
|
|
1188
|
+
topicId: result.topicId,
|
|
1189
|
+
messageId: result.messageId
|
|
1190
|
+
});
|
|
1191
|
+
},
|
|
1192
|
+
{
|
|
1193
|
+
name: "bamdra-memory-ingest",
|
|
1194
|
+
description: "Track inbound conversation turns into bamdra memory topics and facts"
|
|
1195
|
+
}
|
|
1196
|
+
);
|
|
1197
|
+
} else {
|
|
1198
|
+
logMemoryEvent("register-hooks-skipped", { reason: "registerHook unavailable" });
|
|
1199
|
+
}
|
|
1200
|
+
if (registerTypedHook) {
|
|
1201
|
+
registerTypedHook("before_prompt_build", async (event, hookContext) => {
|
|
1202
|
+
const sessionId = getSessionIdFromHookContext(hookContext);
|
|
1203
|
+
const text = getTextFromHookContext(event);
|
|
1204
|
+
logMemoryEvent("hook-assemble-received", {
|
|
1205
|
+
hasSessionId: Boolean(sessionId),
|
|
1206
|
+
hasText: Boolean(text)
|
|
1207
|
+
});
|
|
1208
|
+
if (!sessionId) {
|
|
1209
|
+
return;
|
|
1210
|
+
}
|
|
1211
|
+
if (text) {
|
|
1212
|
+
const result = await plugin.routeAndTrack(sessionId, text);
|
|
1213
|
+
logMemoryEvent("hook-before-prompt-tracked", {
|
|
1214
|
+
sessionId,
|
|
1215
|
+
action: result.decision.action,
|
|
1216
|
+
reason: result.decision.reason,
|
|
1217
|
+
topicId: result.topicId,
|
|
1218
|
+
messageId: result.messageId
|
|
1219
|
+
});
|
|
1220
|
+
}
|
|
1221
|
+
const assembled = await plugin.assembleContext(sessionId);
|
|
1222
|
+
logMemoryEvent("hook-assemble-complete", {
|
|
1223
|
+
sessionId,
|
|
1224
|
+
topicId: assembled.topicId,
|
|
1225
|
+
sections: assembled.sections.length
|
|
1226
|
+
});
|
|
1227
|
+
return buildBeforePromptBuildResult(assembled);
|
|
1228
|
+
});
|
|
1229
|
+
} else {
|
|
1230
|
+
logMemoryEvent("register-typed-hooks-skipped", { reason: "typed hook registrar unavailable" });
|
|
1231
|
+
}
|
|
1232
|
+
if (!registerHook && !registerTypedHook) {
|
|
1233
|
+
return;
|
|
1234
|
+
}
|
|
1235
|
+
logMemoryEvent("register-hooks-complete", {
|
|
1236
|
+
internalHooks: registerHook ? ["message:received", "message:preprocessed"] : [],
|
|
1237
|
+
typedHooks: registerTypedHook ? ["before_prompt_build"] : []
|
|
1238
|
+
});
|
|
1239
|
+
hooksRegistered = true;
|
|
1240
|
+
},
|
|
1241
|
+
async setup() {
|
|
1242
|
+
await ensureMigrations();
|
|
1243
|
+
},
|
|
1244
|
+
async close() {
|
|
1245
|
+
await store.close();
|
|
1246
|
+
},
|
|
1247
|
+
async listTopics(sessionId) {
|
|
1248
|
+
await ensureMigrations();
|
|
1249
|
+
const sessionState = await resolveSessionState(store, cache, sessionId);
|
|
1250
|
+
const topics = await store.listTopics(sessionId);
|
|
1251
|
+
return topics.map((topic) => ({
|
|
1252
|
+
...topic,
|
|
1253
|
+
isActive: sessionState?.activeTopicId === topic.id
|
|
1254
|
+
}));
|
|
1255
|
+
},
|
|
1256
|
+
async switchTopic(sessionId, topicId) {
|
|
1257
|
+
await ensureMigrations();
|
|
1258
|
+
const topic = await store.getTopic(topicId);
|
|
1259
|
+
if (!topic || topic.sessionId !== sessionId) {
|
|
1260
|
+
throw new Error(`Topic ${topicId} does not belong to session ${sessionId}`);
|
|
1261
|
+
}
|
|
1262
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1263
|
+
await cache.setSessionState(sessionId, {
|
|
1264
|
+
activeTopicId: topicId,
|
|
1265
|
+
updatedAt: now
|
|
1266
|
+
});
|
|
1267
|
+
const previousState = await store.getSessionState(sessionId);
|
|
1268
|
+
const updatedTopic = {
|
|
1269
|
+
...topic,
|
|
1270
|
+
status: "active",
|
|
1271
|
+
lastActiveAt: now
|
|
1272
|
+
};
|
|
1273
|
+
await store.upsertTopic(updatedTopic);
|
|
1274
|
+
await store.upsertSessionState({
|
|
1275
|
+
sessionId,
|
|
1276
|
+
activeTopicId: topicId,
|
|
1277
|
+
lastCompactedAt: previousState?.lastCompactedAt ?? null,
|
|
1278
|
+
lastTurnId: previousState?.lastTurnId ?? null,
|
|
1279
|
+
updatedAt: now
|
|
1280
|
+
});
|
|
1281
|
+
logMemoryEvent("switch-topic", { sessionId, topicId, title: updatedTopic.title });
|
|
1282
|
+
return updatedTopic;
|
|
1283
|
+
},
|
|
1284
|
+
async saveFact(args) {
|
|
1285
|
+
await ensureMigrations();
|
|
1286
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1287
|
+
const sessionState = await resolveSessionState(store, cache, args.sessionId);
|
|
1288
|
+
const resolvedTopicId = args.topicId ?? sessionState?.activeTopicId ?? null;
|
|
1289
|
+
const resolvedTopic = resolvedTopicId != null ? await store.getTopic(resolvedTopicId) : null;
|
|
1290
|
+
const normalizedScope = normalizeFactScope(args.scope, args.sessionId);
|
|
1291
|
+
const scope = normalizedScope ?? (resolvedTopic != null ? `topic:${resolvedTopic.id}` : "shared");
|
|
1292
|
+
const tags = dedupeTextItems([
|
|
1293
|
+
...resolvedTopic?.labels ?? [],
|
|
1294
|
+
...args.tags ?? [],
|
|
1295
|
+
args.category ?? "background"
|
|
1296
|
+
]);
|
|
1297
|
+
await store.upsertFact(
|
|
1298
|
+
{
|
|
1299
|
+
id: createFactId(scope, args.key),
|
|
1300
|
+
scope,
|
|
1301
|
+
category: args.category ?? "background",
|
|
1302
|
+
key: args.key,
|
|
1303
|
+
value: args.value,
|
|
1304
|
+
sensitivity: args.sensitivity ?? "normal",
|
|
1305
|
+
recallPolicy: args.recallPolicy ?? (resolvedTopic ? "topic_bound" : "always"),
|
|
1306
|
+
confidence: 1,
|
|
1307
|
+
sourceMessageId: null,
|
|
1308
|
+
sourceTopicId: resolvedTopic?.id ?? null,
|
|
1309
|
+
updatedAt: now
|
|
1310
|
+
},
|
|
1311
|
+
tags
|
|
1312
|
+
);
|
|
1313
|
+
if (resolvedTopic) {
|
|
1314
|
+
await refreshTopicSummary(store, summaryRefresher, config, resolvedTopic.id, now);
|
|
1315
|
+
}
|
|
1316
|
+
logMemoryEvent("save-fact", {
|
|
1317
|
+
sessionId: args.sessionId,
|
|
1318
|
+
topicId: resolvedTopic?.id ?? null,
|
|
1319
|
+
key: args.key,
|
|
1320
|
+
scope,
|
|
1321
|
+
tags
|
|
1322
|
+
});
|
|
1323
|
+
return {
|
|
1324
|
+
topicId: resolvedTopic?.id ?? null,
|
|
1325
|
+
tags
|
|
1326
|
+
};
|
|
1327
|
+
},
|
|
1328
|
+
async compactTopic(args) {
|
|
1329
|
+
await ensureMigrations();
|
|
1330
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1331
|
+
const sessionState = await resolveSessionState(store, cache, args.sessionId);
|
|
1332
|
+
const topicId = args.topicId ?? sessionState?.activeTopicId ?? null;
|
|
1333
|
+
if (!topicId) {
|
|
1334
|
+
throw new Error(`Session ${args.sessionId} does not have an active topic`);
|
|
1335
|
+
}
|
|
1336
|
+
const topic = await store.getTopic(topicId);
|
|
1337
|
+
if (!topic || topic.sessionId !== args.sessionId) {
|
|
1338
|
+
throw new Error(`Topic ${topicId} does not belong to session ${args.sessionId}`);
|
|
1339
|
+
}
|
|
1340
|
+
const refreshedTopic = await refreshTopicSummary(
|
|
1341
|
+
store,
|
|
1342
|
+
summaryRefresher,
|
|
1343
|
+
config,
|
|
1344
|
+
topic.id,
|
|
1345
|
+
now
|
|
1346
|
+
);
|
|
1347
|
+
await store.upsertSessionState({
|
|
1348
|
+
sessionId: args.sessionId,
|
|
1349
|
+
activeTopicId: sessionState?.activeTopicId ?? topic.id,
|
|
1350
|
+
lastCompactedAt: now,
|
|
1351
|
+
lastTurnId: sessionState?.lastTurnId ?? null,
|
|
1352
|
+
updatedAt: now
|
|
1353
|
+
});
|
|
1354
|
+
await cache.setSessionState(args.sessionId, {
|
|
1355
|
+
activeTopicId: sessionState?.activeTopicId ?? topic.id,
|
|
1356
|
+
updatedAt: now
|
|
1357
|
+
});
|
|
1358
|
+
logMemoryEvent("compact-topic", {
|
|
1359
|
+
sessionId: args.sessionId,
|
|
1360
|
+
topicId: refreshedTopic.id,
|
|
1361
|
+
title: refreshedTopic.title
|
|
1362
|
+
});
|
|
1363
|
+
return refreshedTopic;
|
|
1364
|
+
},
|
|
1365
|
+
async searchMemory(args) {
|
|
1366
|
+
await ensureMigrations();
|
|
1367
|
+
const sessionState = await resolveSessionState(store, cache, args.sessionId);
|
|
1368
|
+
const limit = args.limit ?? 5;
|
|
1369
|
+
const resolvedTopicId = args.topicId ?? sessionState?.activeTopicId ?? null;
|
|
1370
|
+
const [topics, facts] = await Promise.all([
|
|
1371
|
+
store.searchTopics(args.sessionId, args.query, limit),
|
|
1372
|
+
store.searchFacts({
|
|
1373
|
+
sessionId: args.sessionId,
|
|
1374
|
+
query: args.query,
|
|
1375
|
+
topicId: resolvedTopicId,
|
|
1376
|
+
limit
|
|
1377
|
+
})
|
|
1378
|
+
]);
|
|
1379
|
+
return {
|
|
1380
|
+
sessionId: args.sessionId,
|
|
1381
|
+
query: args.query,
|
|
1382
|
+
topics,
|
|
1383
|
+
facts
|
|
1384
|
+
};
|
|
1385
|
+
},
|
|
1386
|
+
async routeTopic(sessionId, text) {
|
|
1387
|
+
await ensureMigrations();
|
|
1388
|
+
const persistedState = await resolveSessionState(store, cache, sessionId);
|
|
1389
|
+
const recentTopics = await store.listTopics(sessionId);
|
|
1390
|
+
return router.route({
|
|
1391
|
+
sessionId,
|
|
1392
|
+
text,
|
|
1393
|
+
activeTopicId: persistedState?.activeTopicId ?? null,
|
|
1394
|
+
recentTopics
|
|
1395
|
+
});
|
|
1396
|
+
},
|
|
1397
|
+
async assembleContext(sessionId) {
|
|
1398
|
+
await ensureMigrations();
|
|
1399
|
+
const sessionState = await resolveSessionState(store, cache, sessionId);
|
|
1400
|
+
const topic = sessionState?.activeTopicId != null ? await store.getTopic(sessionState.activeTopicId) : null;
|
|
1401
|
+
const recentMessages = topic != null ? await store.listRecentMessagesForTopic(
|
|
1402
|
+
topic.id,
|
|
1403
|
+
config.contextAssembly?.recentTurns ?? 6
|
|
1404
|
+
) : [];
|
|
1405
|
+
const alwaysFacts = await store.listFactsByScope("global");
|
|
1406
|
+
const sharedFacts = await store.listFactsByScope("shared");
|
|
1407
|
+
const sessionFacts = await store.listFactsByScope(`session:${sessionId}`);
|
|
1408
|
+
const scopedTopicFacts = topic != null ? await store.listFactsByScope(`topic:${topic.id}`) : [];
|
|
1409
|
+
const labelFacts = topic != null ? await store.listFactsByTags(topic.labels, ["always", "topic_bound"]) : [];
|
|
1410
|
+
return assembler.assemble({
|
|
1411
|
+
sessionId,
|
|
1412
|
+
topic,
|
|
1413
|
+
recentMessages,
|
|
1414
|
+
alwaysFacts: dedupeFacts([...alwaysFacts, ...sharedFacts, ...sessionFacts]),
|
|
1415
|
+
topicFacts: dedupeFacts([...scopedTopicFacts, ...labelFacts])
|
|
1416
|
+
});
|
|
1417
|
+
},
|
|
1418
|
+
async routeAndTrack(sessionId, text) {
|
|
1419
|
+
await ensureMigrations();
|
|
1420
|
+
const cachedState = await cache.getSessionState(sessionId);
|
|
1421
|
+
const persistedState = cachedState ? mapCachedStateToSessionState(sessionId, cachedState) : await store.getSessionState(sessionId);
|
|
1422
|
+
const recentTopics = await store.listTopics(sessionId);
|
|
1423
|
+
const decision = router.route({
|
|
1424
|
+
sessionId,
|
|
1425
|
+
text,
|
|
1426
|
+
activeTopicId: persistedState?.activeTopicId ?? null,
|
|
1427
|
+
recentTopics
|
|
1428
|
+
});
|
|
1429
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1430
|
+
const topicId = decision.action === "spawn" ? createTopicId(sessionId, text) : decision.topicId;
|
|
1431
|
+
const messageId = (0, import_node_crypto.randomUUID)();
|
|
1432
|
+
const topicRecord = decision.action === "spawn" ? createSpawnedTopic(sessionId, topicId, text, now, persistedState?.activeTopicId ?? null) : await store.getTopic(topicId);
|
|
1433
|
+
if (!topicRecord) {
|
|
1434
|
+
throw new Error(`Unable to resolve topic ${topicId} after routing`);
|
|
1435
|
+
}
|
|
1436
|
+
if (decision.action === "spawn") {
|
|
1437
|
+
await store.upsertTopic(topicRecord);
|
|
1438
|
+
} else {
|
|
1439
|
+
await store.upsertTopic({
|
|
1440
|
+
...topicRecord,
|
|
1441
|
+
status: "active",
|
|
1442
|
+
openLoops: mergeOpenLoops(topicRecord.openLoops, text),
|
|
1443
|
+
lastActiveAt: now
|
|
1444
|
+
});
|
|
1445
|
+
}
|
|
1446
|
+
const messageRecord = {
|
|
1447
|
+
id: messageId,
|
|
1448
|
+
sessionId,
|
|
1449
|
+
turnId: messageId,
|
|
1450
|
+
parentTurnId: persistedState?.lastTurnId ?? null,
|
|
1451
|
+
role: "user",
|
|
1452
|
+
eventType: "message",
|
|
1453
|
+
text,
|
|
1454
|
+
ts: now,
|
|
1455
|
+
rawJson: JSON.stringify({ role: "user", text })
|
|
1456
|
+
};
|
|
1457
|
+
await store.upsertMessage(messageRecord);
|
|
1458
|
+
const membership = {
|
|
1459
|
+
messageId,
|
|
1460
|
+
topicId,
|
|
1461
|
+
score: 1,
|
|
1462
|
+
isPrimary: true,
|
|
1463
|
+
reason: decision.reason,
|
|
1464
|
+
createdAt: now
|
|
1465
|
+
};
|
|
1466
|
+
await store.upsertTopicMembership(membership);
|
|
1467
|
+
const extractedFacts = factExtractor.extract({
|
|
1468
|
+
sessionId,
|
|
1469
|
+
text,
|
|
1470
|
+
topic: decision.action === "spawn" ? topicRecord : {
|
|
1471
|
+
...topicRecord,
|
|
1472
|
+
openLoops: mergeOpenLoops(topicRecord.openLoops, text),
|
|
1473
|
+
lastActiveAt: now
|
|
1474
|
+
}
|
|
1475
|
+
});
|
|
1476
|
+
for (const candidate of extractedFacts) {
|
|
1477
|
+
await store.upsertFact(
|
|
1478
|
+
mapExtractedFactCandidate(candidate, {
|
|
1479
|
+
sourceMessageId: messageId,
|
|
1480
|
+
sourceTopicId: topicId,
|
|
1481
|
+
updatedAt: now
|
|
1482
|
+
}),
|
|
1483
|
+
candidate.tags
|
|
1484
|
+
);
|
|
1485
|
+
}
|
|
1486
|
+
await refreshTopicSummary(store, summaryRefresher, config, topicId, now);
|
|
1487
|
+
await cache.setSessionState(sessionId, {
|
|
1488
|
+
activeTopicId: topicId,
|
|
1489
|
+
updatedAt: now
|
|
1490
|
+
});
|
|
1491
|
+
await store.upsertSessionState({
|
|
1492
|
+
sessionId,
|
|
1493
|
+
activeTopicId: topicId,
|
|
1494
|
+
lastCompactedAt: persistedState?.lastCompactedAt ?? null,
|
|
1495
|
+
lastTurnId: messageId,
|
|
1496
|
+
updatedAt: now
|
|
1497
|
+
});
|
|
1498
|
+
logMemoryEvent("route-and-track", {
|
|
1499
|
+
sessionId,
|
|
1500
|
+
action: decision.action,
|
|
1501
|
+
reason: decision.reason,
|
|
1502
|
+
topicId,
|
|
1503
|
+
extractedFactCount: extractedFacts.length
|
|
1504
|
+
});
|
|
1505
|
+
return {
|
|
1506
|
+
decision,
|
|
1507
|
+
topicId,
|
|
1508
|
+
messageId
|
|
1509
|
+
};
|
|
1510
|
+
}
|
|
1511
|
+
};
|
|
1512
|
+
plugin.registerHooks(api);
|
|
1513
|
+
return plugin;
|
|
1514
|
+
}
|
|
1515
|
+
async function resolveSessionState(store, cache, sessionId) {
|
|
1516
|
+
const cachedState = await cache.getSessionState(sessionId);
|
|
1517
|
+
return cachedState ? mapCachedStateToSessionState(sessionId, cachedState) : store.getSessionState(sessionId);
|
|
1518
|
+
}
|
|
1519
|
+
function mapCachedStateToSessionState(sessionId, cached) {
|
|
1520
|
+
return {
|
|
1521
|
+
sessionId,
|
|
1522
|
+
activeTopicId: cached.activeTopicId,
|
|
1523
|
+
lastCompactedAt: null,
|
|
1524
|
+
lastTurnId: null,
|
|
1525
|
+
updatedAt: cached.updatedAt
|
|
1526
|
+
};
|
|
1527
|
+
}
|
|
1528
|
+
function createTopicId(sessionId, text) {
|
|
1529
|
+
const digest = (0, import_node_crypto.createHash)("sha1").update(`${sessionId}:${text}:${Date.now()}`).digest("hex").slice(0, 12);
|
|
1530
|
+
return `topic-${digest}`;
|
|
1531
|
+
}
|
|
1532
|
+
function createFactId(scope, key) {
|
|
1533
|
+
const digest = (0, import_node_crypto.createHash)("sha1").update(`${scope}:${key}`).digest("hex").slice(0, 16);
|
|
1534
|
+
return `fact-${digest}`;
|
|
1535
|
+
}
|
|
1536
|
+
function normalizeFactScope(scope, sessionId) {
|
|
1537
|
+
if (typeof scope !== "string") {
|
|
1538
|
+
return null;
|
|
1539
|
+
}
|
|
1540
|
+
const normalized = scope.trim().toLowerCase();
|
|
1541
|
+
if (!normalized) {
|
|
1542
|
+
return null;
|
|
1543
|
+
}
|
|
1544
|
+
if (normalized === "session") {
|
|
1545
|
+
return `session:${sessionId}`;
|
|
1546
|
+
}
|
|
1547
|
+
return normalized;
|
|
1548
|
+
}
|
|
1549
|
+
function createSpawnedTopic(sessionId, topicId, text, now, parentTopicId) {
|
|
1550
|
+
return {
|
|
1551
|
+
id: topicId,
|
|
1552
|
+
sessionId,
|
|
1553
|
+
title: deriveTopicTitle(text),
|
|
1554
|
+
status: "active",
|
|
1555
|
+
parentTopicId,
|
|
1556
|
+
summaryShort: text,
|
|
1557
|
+
summaryLong: "",
|
|
1558
|
+
openLoops: mergeOpenLoops([], text),
|
|
1559
|
+
labels: deriveTopicLabels(text),
|
|
1560
|
+
createdAt: now,
|
|
1561
|
+
lastActiveAt: now
|
|
1562
|
+
};
|
|
1563
|
+
}
|
|
1564
|
+
function deriveTopicTitle(text) {
|
|
1565
|
+
const compact = text.trim().replace(/\s+/g, " ");
|
|
1566
|
+
return compact.length <= 32 ? compact : `${compact.slice(0, 32)}...`;
|
|
1567
|
+
}
|
|
1568
|
+
function deriveTopicLabels(text) {
|
|
1569
|
+
return dedupeTextItems(
|
|
1570
|
+
text.toLowerCase().split(/[^a-z0-9_\u4e00-\u9fff]+/i).map((token) => token.trim()).filter((token) => token.length >= 2).slice(0, 8)
|
|
1571
|
+
);
|
|
1572
|
+
}
|
|
1573
|
+
function mergeOpenLoops(existing, text) {
|
|
1574
|
+
const next = [...existing];
|
|
1575
|
+
if (looksLikeOpenLoop(text)) {
|
|
1576
|
+
next.push(text.trim());
|
|
1577
|
+
}
|
|
1578
|
+
return dedupeTextItems(next).slice(-8);
|
|
1579
|
+
}
|
|
1580
|
+
function looksLikeOpenLoop(text) {
|
|
1581
|
+
const normalized = text.toLowerCase();
|
|
1582
|
+
return [
|
|
1583
|
+
"todo",
|
|
1584
|
+
"\u5F85\u529E",
|
|
1585
|
+
"\u9700\u8981",
|
|
1586
|
+
"\u7EE7\u7EED",
|
|
1587
|
+
"\u540E\u9762",
|
|
1588
|
+
"remember",
|
|
1589
|
+
"follow up",
|
|
1590
|
+
"\u4E0B\u4E00\u6B65"
|
|
1591
|
+
].some((marker) => normalized.includes(marker));
|
|
1592
|
+
}
|
|
1593
|
+
function dedupeFacts(facts) {
|
|
1594
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1595
|
+
return facts.filter((fact) => {
|
|
1596
|
+
if (seen.has(fact.id)) {
|
|
1597
|
+
return false;
|
|
1598
|
+
}
|
|
1599
|
+
seen.add(fact.id);
|
|
1600
|
+
return true;
|
|
1601
|
+
});
|
|
1602
|
+
}
|
|
1603
|
+
function dedupeTextItems(values) {
|
|
1604
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1605
|
+
const result = [];
|
|
1606
|
+
for (const value of values) {
|
|
1607
|
+
const normalized = value.trim();
|
|
1608
|
+
if (!normalized) {
|
|
1609
|
+
continue;
|
|
1610
|
+
}
|
|
1611
|
+
const key = normalized.toLowerCase();
|
|
1612
|
+
if (seen.has(key)) {
|
|
1613
|
+
continue;
|
|
1614
|
+
}
|
|
1615
|
+
seen.add(key);
|
|
1616
|
+
result.push(normalized);
|
|
1617
|
+
}
|
|
1618
|
+
return result;
|
|
1619
|
+
}
|
|
1620
|
+
function mapExtractedFactCandidate(candidate, meta) {
|
|
1621
|
+
return {
|
|
1622
|
+
id: (0, import_node_crypto.createHash)("sha1").update(`${candidate.scope}:${candidate.key}:${candidate.value}`).digest("hex").slice(0, 24),
|
|
1623
|
+
scope: candidate.scope,
|
|
1624
|
+
category: candidate.category,
|
|
1625
|
+
key: candidate.key,
|
|
1626
|
+
value: candidate.value,
|
|
1627
|
+
sensitivity: candidate.sensitivity,
|
|
1628
|
+
recallPolicy: candidate.recallPolicy,
|
|
1629
|
+
confidence: candidate.confidence,
|
|
1630
|
+
sourceMessageId: meta.sourceMessageId,
|
|
1631
|
+
sourceTopicId: meta.sourceTopicId,
|
|
1632
|
+
updatedAt: meta.updatedAt
|
|
1633
|
+
};
|
|
1634
|
+
}
|
|
1635
|
+
async function refreshTopicSummary(store, summaryRefresher, config, topicId, now) {
|
|
1636
|
+
const topic = await store.getTopic(topicId);
|
|
1637
|
+
if (!topic) {
|
|
1638
|
+
throw new Error(`Unable to refresh summary for missing topic ${topicId}`);
|
|
1639
|
+
}
|
|
1640
|
+
const recentMessages = await store.listRecentMessagesForTopic(
|
|
1641
|
+
topicId,
|
|
1642
|
+
config.contextAssembly?.recentTurns ?? 6
|
|
1643
|
+
);
|
|
1644
|
+
const refreshedFacts = dedupeFacts([
|
|
1645
|
+
...await store.listFactsByScope(`topic:${topicId}`),
|
|
1646
|
+
...await store.listFactsByTags(topic.labels, ["always", "topic_bound"])
|
|
1647
|
+
]);
|
|
1648
|
+
const refreshedSummary = summaryRefresher.refresh({
|
|
1649
|
+
topic,
|
|
1650
|
+
recentMessages,
|
|
1651
|
+
facts: refreshedFacts
|
|
1652
|
+
});
|
|
1653
|
+
const updatedTopic = {
|
|
1654
|
+
...topic,
|
|
1655
|
+
...refreshedSummary,
|
|
1656
|
+
lastActiveAt: now
|
|
1657
|
+
};
|
|
1658
|
+
await store.upsertTopic(updatedTopic);
|
|
1659
|
+
return updatedTopic;
|
|
1660
|
+
}
|
|
1661
|
+
function normalizeMemoryConfig(inputConfig) {
|
|
1662
|
+
return {
|
|
1663
|
+
enabled: inputConfig?.enabled ?? true,
|
|
1664
|
+
store: {
|
|
1665
|
+
provider: "sqlite",
|
|
1666
|
+
path: inputConfig?.store?.path || process.env.OPENCLAW_BAMDRA_MEMORY_DB_PATH || process.env.OPENCLAW_MEMORY_DB_PATH || DEFAULT_DB_PATH
|
|
1667
|
+
},
|
|
1668
|
+
cache: {
|
|
1669
|
+
provider: "memory",
|
|
1670
|
+
maxSessions: inputConfig?.cache?.maxSessions ?? 128,
|
|
1671
|
+
maxTopicsPerSession: inputConfig?.cache?.maxTopicsPerSession ?? 64,
|
|
1672
|
+
maxFacts: inputConfig?.cache?.maxFacts ?? 2048
|
|
1673
|
+
},
|
|
1674
|
+
topicRouting: {
|
|
1675
|
+
maxRecentTopics: inputConfig?.topicRouting?.maxRecentTopics ?? 12,
|
|
1676
|
+
newTopicThreshold: inputConfig?.topicRouting?.newTopicThreshold ?? 0.28,
|
|
1677
|
+
switchTopicThreshold: inputConfig?.topicRouting?.switchTopicThreshold ?? 0.55
|
|
1678
|
+
},
|
|
1679
|
+
contextAssembly: {
|
|
1680
|
+
recentTurns: inputConfig?.contextAssembly?.recentTurns ?? 6,
|
|
1681
|
+
includeTopicShortSummary: inputConfig?.contextAssembly?.includeTopicShortSummary ?? true,
|
|
1682
|
+
includeOpenLoops: inputConfig?.contextAssembly?.includeOpenLoops ?? true,
|
|
1683
|
+
alwaysFactLimit: inputConfig?.contextAssembly?.alwaysFactLimit ?? 12,
|
|
1684
|
+
topicFactLimit: inputConfig?.contextAssembly?.topicFactLimit ?? 16
|
|
1685
|
+
}
|
|
1686
|
+
};
|
|
1687
|
+
}
|
|
1688
|
+
function getInternalHookRegistrar(api) {
|
|
1689
|
+
if (!api || typeof api !== "object") {
|
|
1690
|
+
return null;
|
|
1691
|
+
}
|
|
1692
|
+
const registrar = api.registerHook;
|
|
1693
|
+
if (typeof registrar !== "function") {
|
|
1694
|
+
return null;
|
|
1695
|
+
}
|
|
1696
|
+
return registrar.bind(api);
|
|
1697
|
+
}
|
|
1698
|
+
function getTypedHookRegistrar(api) {
|
|
1699
|
+
if (!api || typeof api !== "object") {
|
|
1700
|
+
return null;
|
|
1701
|
+
}
|
|
1702
|
+
const registrar = api.on;
|
|
1703
|
+
if (typeof registrar !== "function") {
|
|
1704
|
+
return null;
|
|
1705
|
+
}
|
|
1706
|
+
return registrar.bind(api);
|
|
1707
|
+
}
|
|
1708
|
+
function getSessionIdFromHookContext(context) {
|
|
1709
|
+
if (!context || typeof context !== "object") {
|
|
1710
|
+
return null;
|
|
1711
|
+
}
|
|
1712
|
+
const candidate = context;
|
|
1713
|
+
const sessionId = candidate.sessionKey ?? candidate.sessionId ?? candidate.session?.id ?? candidate.conversation?.id ?? candidate.metadata?.sessionId ?? candidate.context?.sessionId ?? candidate.input?.sessionId ?? candidate.input?.session?.id;
|
|
1714
|
+
return typeof sessionId === "string" && sessionId.trim() ? sessionId : null;
|
|
1715
|
+
}
|
|
1716
|
+
function getTextFromHookContext(context) {
|
|
1717
|
+
if (!context || typeof context !== "object") {
|
|
1718
|
+
return null;
|
|
1719
|
+
}
|
|
1720
|
+
const candidate = context;
|
|
1721
|
+
const directText = normalizeHookText(
|
|
1722
|
+
candidate.bodyForAgent ?? candidate.body ?? candidate.prompt ?? candidate.text ?? candidate.context?.bodyForAgent ?? candidate.context?.body ?? candidate.context?.text ?? candidate.context?.content
|
|
1723
|
+
);
|
|
1724
|
+
if (directText) {
|
|
1725
|
+
return directText;
|
|
1726
|
+
}
|
|
1727
|
+
const messageText = normalizeHookText(candidate.message?.text ?? candidate.message?.content);
|
|
1728
|
+
if (messageText) {
|
|
1729
|
+
return messageText;
|
|
1730
|
+
}
|
|
1731
|
+
const inputText = extractTextFromInput(candidate.input);
|
|
1732
|
+
if (inputText) {
|
|
1733
|
+
return inputText;
|
|
1734
|
+
}
|
|
1735
|
+
const lastUserMessage = [...candidate.messages ?? []].reverse().find((message) => (message.role ?? "user") === "user");
|
|
1736
|
+
return normalizeHookText(lastUserMessage?.text ?? lastUserMessage?.content);
|
|
1737
|
+
}
|
|
1738
|
+
function extractTextFromInput(input) {
|
|
1739
|
+
if (typeof input === "string") {
|
|
1740
|
+
return normalizeHookText(input);
|
|
1741
|
+
}
|
|
1742
|
+
if (!input || typeof input !== "object") {
|
|
1743
|
+
return null;
|
|
1744
|
+
}
|
|
1745
|
+
const candidate = input;
|
|
1746
|
+
const directText = normalizeHookText(candidate.text ?? candidate.content);
|
|
1747
|
+
if (directText) {
|
|
1748
|
+
return directText;
|
|
1749
|
+
}
|
|
1750
|
+
const messageText = normalizeHookText(candidate.message?.text ?? candidate.message?.content);
|
|
1751
|
+
if (messageText) {
|
|
1752
|
+
return messageText;
|
|
1753
|
+
}
|
|
1754
|
+
const lastUserMessage = [...candidate.messages ?? []].reverse().find((message) => (message.role ?? "user") === "user");
|
|
1755
|
+
return normalizeHookText(lastUserMessage?.text ?? lastUserMessage?.content);
|
|
1756
|
+
}
|
|
1757
|
+
function normalizeHookText(value) {
|
|
1758
|
+
if (typeof value !== "string") {
|
|
1759
|
+
return null;
|
|
1760
|
+
}
|
|
1761
|
+
const normalized = value.trim();
|
|
1762
|
+
return normalized ? normalized : null;
|
|
1763
|
+
}
|
|
1764
|
+
function buildBeforePromptBuildResult(assembled) {
|
|
1765
|
+
const text = assembled.text.trim();
|
|
1766
|
+
if (!text) {
|
|
1767
|
+
return void 0;
|
|
1768
|
+
}
|
|
1769
|
+
return {
|
|
1770
|
+
prependSystemContext: text
|
|
1771
|
+
};
|
|
1772
|
+
}
|
|
1773
|
+
|
|
1774
|
+
// src/index.ts
|
|
1775
|
+
var PLUGIN_ID = "bamdra-openclaw-memory";
|
|
1776
|
+
var ENGINE_GLOBAL_KEY = "__OPENCLAW_BAMDRA_MEMORY_CONTEXT_ENGINE__";
|
|
1777
|
+
var TOOLS_REGISTERED_KEY = /* @__PURE__ */ Symbol.for("bamdra-memory.tools-registered");
|
|
1778
|
+
var ENGINE_REGISTERED_KEY = /* @__PURE__ */ Symbol.for("bamdra-memory.context-engine-registered");
|
|
1779
|
+
function logUnifiedMemoryEvent(event, details = {}) {
|
|
1780
|
+
try {
|
|
1781
|
+
console.info("[bamdra-memory]", event, JSON.stringify(details));
|
|
1782
|
+
} catch {
|
|
1783
|
+
console.info("[bamdra-memory]", event);
|
|
1784
|
+
}
|
|
1785
|
+
}
|
|
1786
|
+
function register(api) {
|
|
1787
|
+
initializeUnifiedPlugin(api, "register");
|
|
1788
|
+
}
|
|
1789
|
+
async function activate(api) {
|
|
1790
|
+
initializeUnifiedPlugin(api, "activate");
|
|
1791
|
+
}
|
|
1792
|
+
function initializeUnifiedPlugin(api, phase) {
|
|
1793
|
+
logUnifiedMemoryEvent(`${phase}-plugin`, { id: PLUGIN_ID });
|
|
1794
|
+
const engine = brandContextEngine(createContextEngineMemoryV2Plugin(api.pluginConfig ?? api.config, api));
|
|
1795
|
+
exposeContextEngine(engine);
|
|
1796
|
+
engine.registerHooks(api);
|
|
1797
|
+
if (!api[TOOLS_REGISTERED_KEY] && typeof api.registerTool === "function") {
|
|
1798
|
+
registerUnifiedTools(api, engine);
|
|
1799
|
+
api[TOOLS_REGISTERED_KEY] = true;
|
|
1800
|
+
logUnifiedMemoryEvent("tools-ready", { id: PLUGIN_ID });
|
|
1801
|
+
}
|
|
1802
|
+
if (api[ENGINE_REGISTERED_KEY]) {
|
|
1803
|
+
return;
|
|
1804
|
+
}
|
|
1805
|
+
api.registerContextEngine(PLUGIN_ID, async (config) => {
|
|
1806
|
+
const configured = brandContextEngine(createContextEngineMemoryV2Plugin(config, api));
|
|
1807
|
+
exposeContextEngine(configured);
|
|
1808
|
+
configured.registerHooks(api);
|
|
1809
|
+
await configured.setup();
|
|
1810
|
+
logUnifiedMemoryEvent("context-engine-ready", {
|
|
1811
|
+
id: PLUGIN_ID,
|
|
1812
|
+
dbPath: configured.config.store.path
|
|
1813
|
+
});
|
|
1814
|
+
return configured;
|
|
1815
|
+
});
|
|
1816
|
+
api[ENGINE_REGISTERED_KEY] = true;
|
|
1817
|
+
}
|
|
1818
|
+
function brandContextEngine(engine) {
|
|
1819
|
+
engine.name = PLUGIN_ID;
|
|
1820
|
+
return engine;
|
|
1821
|
+
}
|
|
1822
|
+
function exposeContextEngine(engine) {
|
|
1823
|
+
globalThis[ENGINE_GLOBAL_KEY] = engine;
|
|
1824
|
+
process.env.OPENCLAW_BAMDRA_MEMORY_DB_PATH = engine.config.store.path;
|
|
1825
|
+
}
|
|
1826
|
+
function registerUnifiedTools(api, engine) {
|
|
1827
|
+
const definitions = [
|
|
1828
|
+
createToolDefinitions({
|
|
1829
|
+
canonicalName: "memory_list_topics",
|
|
1830
|
+
aliasName: "bamdra_memory_list_topics",
|
|
1831
|
+
description: "List known topics for a session",
|
|
1832
|
+
parameters: {
|
|
1833
|
+
type: "object",
|
|
1834
|
+
additionalProperties: false,
|
|
1835
|
+
required: ["sessionId"],
|
|
1836
|
+
properties: {
|
|
1837
|
+
sessionId: { type: "string" }
|
|
1838
|
+
}
|
|
1839
|
+
},
|
|
1840
|
+
async execute(params) {
|
|
1841
|
+
return engine.listTopics(params.sessionId);
|
|
1842
|
+
}
|
|
1843
|
+
}),
|
|
1844
|
+
createToolDefinitions({
|
|
1845
|
+
canonicalName: "memory_switch_topic",
|
|
1846
|
+
aliasName: "bamdra_memory_switch_topic",
|
|
1847
|
+
description: "Switch the active topic for a session",
|
|
1848
|
+
parameters: {
|
|
1849
|
+
type: "object",
|
|
1850
|
+
additionalProperties: false,
|
|
1851
|
+
required: ["sessionId", "topicId"],
|
|
1852
|
+
properties: {
|
|
1853
|
+
sessionId: { type: "string" },
|
|
1854
|
+
topicId: { type: "string" }
|
|
1855
|
+
}
|
|
1856
|
+
},
|
|
1857
|
+
async execute(params) {
|
|
1858
|
+
return engine.switchTopic(params.sessionId, params.topicId);
|
|
1859
|
+
}
|
|
1860
|
+
}),
|
|
1861
|
+
createToolDefinitions({
|
|
1862
|
+
canonicalName: "memory_save_fact",
|
|
1863
|
+
aliasName: "bamdra_memory_save_fact",
|
|
1864
|
+
description: "Persist a pinned memory fact for the current or selected topic",
|
|
1865
|
+
parameters: {
|
|
1866
|
+
type: "object",
|
|
1867
|
+
additionalProperties: false,
|
|
1868
|
+
required: ["sessionId", "key", "value"],
|
|
1869
|
+
properties: {
|
|
1870
|
+
sessionId: { type: "string" },
|
|
1871
|
+
key: { type: "string" },
|
|
1872
|
+
value: { type: "string" },
|
|
1873
|
+
category: { type: "string" },
|
|
1874
|
+
sensitivity: { type: "string" },
|
|
1875
|
+
recallPolicy: { type: "string" },
|
|
1876
|
+
scope: { type: "string" },
|
|
1877
|
+
topicId: { type: ["string", "null"] },
|
|
1878
|
+
tags: {
|
|
1879
|
+
type: "array",
|
|
1880
|
+
items: { type: "string" }
|
|
1881
|
+
}
|
|
1882
|
+
}
|
|
1883
|
+
},
|
|
1884
|
+
async execute(params) {
|
|
1885
|
+
return engine.saveFact(params);
|
|
1886
|
+
}
|
|
1887
|
+
}),
|
|
1888
|
+
createToolDefinitions({
|
|
1889
|
+
canonicalName: "memory_compact_topic",
|
|
1890
|
+
aliasName: "bamdra_memory_compact_topic",
|
|
1891
|
+
description: "Force refresh the summary for the current or selected topic",
|
|
1892
|
+
parameters: {
|
|
1893
|
+
type: "object",
|
|
1894
|
+
additionalProperties: false,
|
|
1895
|
+
required: ["sessionId"],
|
|
1896
|
+
properties: {
|
|
1897
|
+
sessionId: { type: "string" },
|
|
1898
|
+
topicId: { type: ["string", "null"] }
|
|
1899
|
+
}
|
|
1900
|
+
},
|
|
1901
|
+
async execute(params) {
|
|
1902
|
+
return engine.compactTopic(params);
|
|
1903
|
+
}
|
|
1904
|
+
}),
|
|
1905
|
+
createToolDefinitions({
|
|
1906
|
+
canonicalName: "memory_search",
|
|
1907
|
+
aliasName: "bamdra_memory_search",
|
|
1908
|
+
description: "Search topics and durable facts for a session",
|
|
1909
|
+
parameters: {
|
|
1910
|
+
type: "object",
|
|
1911
|
+
additionalProperties: false,
|
|
1912
|
+
required: ["sessionId", "query"],
|
|
1913
|
+
properties: {
|
|
1914
|
+
sessionId: { type: "string" },
|
|
1915
|
+
query: { type: "string" },
|
|
1916
|
+
topicId: { type: ["string", "null"] },
|
|
1917
|
+
limit: { type: "integer", minimum: 1, maximum: 20 }
|
|
1918
|
+
}
|
|
1919
|
+
},
|
|
1920
|
+
async execute(params) {
|
|
1921
|
+
return engine.searchMemory(params);
|
|
1922
|
+
}
|
|
1923
|
+
})
|
|
1924
|
+
];
|
|
1925
|
+
for (const group of definitions) {
|
|
1926
|
+
for (const definition of group) {
|
|
1927
|
+
api.registerTool?.(definition);
|
|
1928
|
+
logUnifiedMemoryEvent("tool-registered", { name: definition.name });
|
|
1929
|
+
}
|
|
1930
|
+
}
|
|
1931
|
+
}
|
|
1932
|
+
function createToolDefinitions(definition) {
|
|
1933
|
+
return [definition.canonicalName, definition.aliasName].map((name) => ({
|
|
1934
|
+
name,
|
|
1935
|
+
description: definition.description,
|
|
1936
|
+
parameters: definition.parameters,
|
|
1937
|
+
async execute(invocationId, params) {
|
|
1938
|
+
const result = await definition.execute(params);
|
|
1939
|
+
logUnifiedMemoryEvent("tool-execute", {
|
|
1940
|
+
name,
|
|
1941
|
+
invocationId,
|
|
1942
|
+
sessionId: params && typeof params === "object" && "sessionId" in params ? params.sessionId : null
|
|
1943
|
+
});
|
|
1944
|
+
return asTextResult(result);
|
|
1945
|
+
}
|
|
1946
|
+
}));
|
|
1947
|
+
}
|
|
1948
|
+
function asTextResult(value) {
|
|
1949
|
+
return {
|
|
1950
|
+
content: [
|
|
1951
|
+
{
|
|
1952
|
+
type: "text",
|
|
1953
|
+
text: typeof value === "string" ? value : JSON.stringify(value, null, 2)
|
|
1954
|
+
}
|
|
1955
|
+
]
|
|
1956
|
+
};
|
|
1957
|
+
}
|
|
1958
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
1959
|
+
module.exports = {activate,
|
|
1960
|
+
register};
|