@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/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};