memory-lancedb-pro 1.0.30 → 1.0.32
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/cli.ts +67 -14
- package/index.ts +130 -2
- package/openclaw.plugin.json +25 -1
- package/package.json +1 -1
- package/src/adaptive-retrieval.ts +4 -9
- package/src/tools.ts +14 -0
- package/test/cli-smoke.mjs +81 -8
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.0.32
|
|
4
|
+
|
|
5
|
+
- Fix: strip OpenClaw `Conversation info` / `Sender` metadata noise before auto-capture matching and adaptive retrieval normalization, reducing false captures and noisy retrieval triggers.
|
|
6
|
+
- Fix: parse `autoRecallMinRepeated` from plugin config so repeated-memory suppression works when configured.
|
|
7
|
+
|
|
8
|
+
PR: #50
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
## 1.0.31
|
|
14
|
+
|
|
15
|
+
- Fix: `memory-pro import` now preserves provided IDs and is idempotent (skips if ID already exists).
|
|
16
|
+
|
|
3
17
|
## 1.0.26
|
|
4
18
|
|
|
5
19
|
**Access Reinforcement for Time Decay**
|
package/cli.ts
CHANGED
|
@@ -379,25 +379,78 @@ export function registerMemoryCLI(program: Command, context: CLIContext): void {
|
|
|
379
379
|
continue;
|
|
380
380
|
}
|
|
381
381
|
|
|
382
|
-
|
|
383
|
-
const
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
382
|
+
const categoryRaw = memory.category;
|
|
383
|
+
const category: MemoryEntry["category"] =
|
|
384
|
+
categoryRaw === "preference" ||
|
|
385
|
+
categoryRaw === "fact" ||
|
|
386
|
+
categoryRaw === "decision" ||
|
|
387
|
+
categoryRaw === "entity" ||
|
|
388
|
+
categoryRaw === "other"
|
|
389
|
+
? categoryRaw
|
|
390
|
+
: "other";
|
|
391
|
+
|
|
392
|
+
const importanceRaw = Number(memory.importance);
|
|
393
|
+
const importance = Number.isFinite(importanceRaw)
|
|
394
|
+
? Math.max(0, Math.min(1, importanceRaw))
|
|
395
|
+
: 0.7;
|
|
396
|
+
|
|
397
|
+
const timestampRaw = Number(memory.timestamp);
|
|
398
|
+
const timestamp = Number.isFinite(timestampRaw) ? timestampRaw : Date.now();
|
|
399
|
+
|
|
400
|
+
const metadataRaw = memory.metadata;
|
|
401
|
+
const metadata =
|
|
402
|
+
typeof metadataRaw === "string"
|
|
403
|
+
? metadataRaw
|
|
404
|
+
: metadataRaw != null
|
|
405
|
+
? JSON.stringify(metadataRaw)
|
|
406
|
+
: "{}";
|
|
407
|
+
|
|
408
|
+
const idRaw = memory.id;
|
|
409
|
+
const id = typeof idRaw === "string" && idRaw.length > 0 ? idRaw : undefined;
|
|
410
|
+
|
|
411
|
+
// Idempotency: if the import file includes an id and we already have it, skip.
|
|
412
|
+
if (id && (await context.store.hasId(id))) {
|
|
389
413
|
skipped++;
|
|
390
414
|
continue;
|
|
391
415
|
}
|
|
392
416
|
|
|
417
|
+
// Back-compat dedupe: if no id provided, do a best-effort similarity check.
|
|
418
|
+
if (!id) {
|
|
419
|
+
const existing = await context.retriever.retrieve({
|
|
420
|
+
query: text,
|
|
421
|
+
limit: 1,
|
|
422
|
+
scopeFilter: [targetScope],
|
|
423
|
+
});
|
|
424
|
+
if (existing.length > 0 && existing[0].score > 0.95) {
|
|
425
|
+
skipped++;
|
|
426
|
+
continue;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
393
430
|
const vector = await context.embedder.embedPassage(text);
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
431
|
+
|
|
432
|
+
if (id) {
|
|
433
|
+
await context.store.importEntry({
|
|
434
|
+
id,
|
|
435
|
+
text,
|
|
436
|
+
vector,
|
|
437
|
+
category,
|
|
438
|
+
scope: targetScope,
|
|
439
|
+
importance,
|
|
440
|
+
timestamp,
|
|
441
|
+
metadata,
|
|
442
|
+
});
|
|
443
|
+
} else {
|
|
444
|
+
await context.store.store({
|
|
445
|
+
text,
|
|
446
|
+
vector,
|
|
447
|
+
importance,
|
|
448
|
+
category,
|
|
449
|
+
scope: targetScope,
|
|
450
|
+
metadata,
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
|
|
401
454
|
imported++;
|
|
402
455
|
} catch (error) {
|
|
403
456
|
console.warn(`Failed to import memory: ${error}`);
|
package/index.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
7
7
|
import { homedir } from "node:os";
|
|
8
8
|
import { join, dirname, basename } from "node:path";
|
|
9
|
-
import { readFile, readdir, writeFile, mkdir } from "node:fs/promises";
|
|
9
|
+
import { readFile, readdir, writeFile, mkdir, appendFile } from "node:fs/promises";
|
|
10
10
|
import { readFileSync } from "node:fs";
|
|
11
11
|
|
|
12
12
|
// Import core components
|
|
@@ -16,6 +16,7 @@ import { createRetriever, DEFAULT_RETRIEVAL_CONFIG } from "./src/retriever.js";
|
|
|
16
16
|
import { createScopeManager } from "./src/scopes.js";
|
|
17
17
|
import { createMigrator } from "./src/migrate.js";
|
|
18
18
|
import { registerAllMemoryTools } from "./src/tools.js";
|
|
19
|
+
import type { MdMirrorWriter } from "./src/tools.js";
|
|
19
20
|
import { shouldSkipRetrieval } from "./src/adaptive-retrieval.js";
|
|
20
21
|
import { AccessTracker } from "./src/access-tracker.js";
|
|
21
22
|
import { createMemoryCLI } from "./cli.js";
|
|
@@ -39,6 +40,7 @@ interface PluginConfig {
|
|
|
39
40
|
autoCapture?: boolean;
|
|
40
41
|
autoRecall?: boolean;
|
|
41
42
|
autoRecallMinLength?: number;
|
|
43
|
+
autoRecallMinRepeated?: number;
|
|
42
44
|
captureAssistant?: boolean;
|
|
43
45
|
retrieval?: {
|
|
44
46
|
mode?: "hybrid" | "vector";
|
|
@@ -67,6 +69,7 @@ interface PluginConfig {
|
|
|
67
69
|
};
|
|
68
70
|
enableManagementTools?: boolean;
|
|
69
71
|
sessionMemory?: { enabled?: boolean; messageCount?: number };
|
|
72
|
+
mdMirror?: { enabled?: boolean; dir?: string };
|
|
70
73
|
}
|
|
71
74
|
|
|
72
75
|
// ============================================================================
|
|
@@ -138,7 +141,11 @@ const CAPTURE_EXCLUDE_PATTERNS = [
|
|
|
138
141
|
];
|
|
139
142
|
|
|
140
143
|
export function shouldCapture(text: string): boolean {
|
|
141
|
-
|
|
144
|
+
let s = text.trim();
|
|
145
|
+
|
|
146
|
+
// Strip OpenClaw metadata headers (Conversation info or Sender)
|
|
147
|
+
const metadataPattern = /^(Conversation info|Sender) \(untrusted metadata\):[\s\S]*?\n\s*\n/gim;
|
|
148
|
+
s = s.replace(metadataPattern, "");
|
|
142
149
|
|
|
143
150
|
// CJK characters carry more meaning per character, use lower minimum threshold
|
|
144
151
|
const hasCJK = /[\u4e00-\u9fff\u3040-\u309f\u30a0-\u30ff\uac00-\ud7af]/.test(
|
|
@@ -337,6 +344,92 @@ async function findPreviousSessionFile(
|
|
|
337
344
|
} catch {}
|
|
338
345
|
}
|
|
339
346
|
|
|
347
|
+
// ============================================================================
|
|
348
|
+
// Markdown Mirror (dual-write)
|
|
349
|
+
// ============================================================================
|
|
350
|
+
|
|
351
|
+
type AgentWorkspaceMap = Record<string, string>;
|
|
352
|
+
|
|
353
|
+
function resolveAgentWorkspaceMap(api: OpenClawPluginApi): AgentWorkspaceMap {
|
|
354
|
+
const map: AgentWorkspaceMap = {};
|
|
355
|
+
|
|
356
|
+
// Try api.config first (runtime config)
|
|
357
|
+
const agents = Array.isArray((api as any).config?.agents?.list)
|
|
358
|
+
? (api as any).config.agents.list
|
|
359
|
+
: [];
|
|
360
|
+
|
|
361
|
+
for (const agent of agents) {
|
|
362
|
+
if (agent?.id && typeof agent.workspace === "string") {
|
|
363
|
+
map[String(agent.id)] = agent.workspace;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Fallback: read from openclaw.json (respect OPENCLAW_HOME if set)
|
|
368
|
+
if (Object.keys(map).length === 0) {
|
|
369
|
+
try {
|
|
370
|
+
const openclawHome = process.env.OPENCLAW_HOME || join(homedir(), ".openclaw");
|
|
371
|
+
const configPath = join(openclawHome, "openclaw.json");
|
|
372
|
+
const raw = readFileSync(configPath, "utf8");
|
|
373
|
+
const parsed = JSON.parse(raw);
|
|
374
|
+
const list = parsed?.agents?.list;
|
|
375
|
+
if (Array.isArray(list)) {
|
|
376
|
+
for (const agent of list) {
|
|
377
|
+
if (agent?.id && typeof agent.workspace === "string") {
|
|
378
|
+
map[String(agent.id)] = agent.workspace;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
} catch {
|
|
383
|
+
/* silent */
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
return map;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function createMdMirrorWriter(
|
|
391
|
+
api: OpenClawPluginApi,
|
|
392
|
+
config: PluginConfig,
|
|
393
|
+
): MdMirrorWriter | null {
|
|
394
|
+
if (config.mdMirror?.enabled !== true) return null;
|
|
395
|
+
|
|
396
|
+
const fallbackDir = api.resolvePath(config.mdMirror.dir || "memory-md");
|
|
397
|
+
const workspaceMap = resolveAgentWorkspaceMap(api);
|
|
398
|
+
|
|
399
|
+
if (Object.keys(workspaceMap).length > 0) {
|
|
400
|
+
api.logger.info(
|
|
401
|
+
`mdMirror: resolved ${Object.keys(workspaceMap).length} agent workspace(s)`,
|
|
402
|
+
);
|
|
403
|
+
} else {
|
|
404
|
+
api.logger.warn(
|
|
405
|
+
`mdMirror: no agent workspaces found, writes will use fallback dir: ${fallbackDir}`,
|
|
406
|
+
);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
return async (entry, meta) => {
|
|
410
|
+
try {
|
|
411
|
+
const ts = new Date(entry.timestamp || Date.now());
|
|
412
|
+
const dateStr = ts.toISOString().split("T")[0];
|
|
413
|
+
|
|
414
|
+
let mirrorDir = fallbackDir;
|
|
415
|
+
if (meta?.agentId && workspaceMap[meta.agentId]) {
|
|
416
|
+
mirrorDir = join(workspaceMap[meta.agentId], "memory");
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
const filePath = join(mirrorDir, `${dateStr}.md`);
|
|
420
|
+
const agentLabel = meta?.agentId ? ` agent=${meta.agentId}` : "";
|
|
421
|
+
const sourceLabel = meta?.source ? ` source=${meta.source}` : "";
|
|
422
|
+
const safeText = entry.text.replace(/\n/g, " ").slice(0, 500);
|
|
423
|
+
const line = `- ${ts.toISOString()} [${entry.category}:${entry.scope}]${agentLabel}${sourceLabel} ${safeText}\n`;
|
|
424
|
+
|
|
425
|
+
await mkdir(mirrorDir, { recursive: true });
|
|
426
|
+
await appendFile(filePath, line, "utf8");
|
|
427
|
+
} catch (err) {
|
|
428
|
+
api.logger.warn(`mdMirror: write failed: ${String(err)}`);
|
|
429
|
+
}
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
|
|
340
433
|
// ============================================================================
|
|
341
434
|
// Version
|
|
342
435
|
// ============================================================================
|
|
@@ -427,6 +520,12 @@ const memoryLanceDBProPlugin = {
|
|
|
427
520
|
`memory-lancedb-pro@${pluginVersion}: plugin registered (db: ${resolvedDbPath}, model: ${config.embedding.model || "text-embedding-3-small"})`,
|
|
428
521
|
);
|
|
429
522
|
|
|
523
|
+
// ========================================================================
|
|
524
|
+
// Markdown Mirror
|
|
525
|
+
// ========================================================================
|
|
526
|
+
|
|
527
|
+
const mdMirror = createMdMirrorWriter(api, config);
|
|
528
|
+
|
|
430
529
|
// ========================================================================
|
|
431
530
|
// Register Tools
|
|
432
531
|
// ========================================================================
|
|
@@ -439,6 +538,7 @@ const memoryLanceDBProPlugin = {
|
|
|
439
538
|
scopeManager,
|
|
440
539
|
embedder,
|
|
441
540
|
agentId: undefined, // Will be determined at runtime from context
|
|
541
|
+
mdMirror,
|
|
442
542
|
},
|
|
443
543
|
{
|
|
444
544
|
enableManagementTools: config.enableManagementTools,
|
|
@@ -649,6 +749,14 @@ const memoryLanceDBProPlugin = {
|
|
|
649
749
|
scope: defaultScope,
|
|
650
750
|
});
|
|
651
751
|
stored++;
|
|
752
|
+
|
|
753
|
+
// Dual-write to Markdown mirror if enabled
|
|
754
|
+
if (mdMirror) {
|
|
755
|
+
await mdMirror(
|
|
756
|
+
{ text, category, scope: defaultScope, timestamp: Date.now() },
|
|
757
|
+
{ source: "auto-capture", agentId },
|
|
758
|
+
);
|
|
759
|
+
}
|
|
652
760
|
}
|
|
653
761
|
|
|
654
762
|
if (stored > 0) {
|
|
@@ -758,6 +866,14 @@ const memoryLanceDBProPlugin = {
|
|
|
758
866
|
}),
|
|
759
867
|
});
|
|
760
868
|
|
|
869
|
+
// Dual-write to Markdown mirror if enabled
|
|
870
|
+
if (mdMirror) {
|
|
871
|
+
await mdMirror(
|
|
872
|
+
{ text: memoryText.replace(/\n/g, " ").slice(0, 500), category: "fact", scope: "global", timestamp: Date.now() },
|
|
873
|
+
{ source: "session-memory" },
|
|
874
|
+
);
|
|
875
|
+
}
|
|
876
|
+
|
|
761
877
|
api.logger.info(
|
|
762
878
|
`session-memory: stored session summary for ${currentSessionId || "unknown"}`,
|
|
763
879
|
);
|
|
@@ -987,6 +1103,7 @@ function parsePluginConfig(value: unknown): PluginConfig {
|
|
|
987
1103
|
// Default OFF: only enable when explicitly set to true.
|
|
988
1104
|
autoRecall: cfg.autoRecall === true,
|
|
989
1105
|
autoRecallMinLength: parsePositiveInt(cfg.autoRecallMinLength),
|
|
1106
|
+
autoRecallMinRepeated: parsePositiveInt(cfg.autoRecallMinRepeated),
|
|
990
1107
|
captureAssistant: cfg.captureAssistant === true,
|
|
991
1108
|
retrieval:
|
|
992
1109
|
typeof cfg.retrieval === "object" && cfg.retrieval !== null
|
|
@@ -1010,6 +1127,17 @@ function parsePluginConfig(value: unknown): PluginConfig {
|
|
|
1010
1127
|
: undefined,
|
|
1011
1128
|
}
|
|
1012
1129
|
: undefined,
|
|
1130
|
+
mdMirror:
|
|
1131
|
+
typeof cfg.mdMirror === "object" && cfg.mdMirror !== null
|
|
1132
|
+
? {
|
|
1133
|
+
enabled:
|
|
1134
|
+
(cfg.mdMirror as Record<string, unknown>).enabled === true,
|
|
1135
|
+
dir:
|
|
1136
|
+
typeof (cfg.mdMirror as Record<string, unknown>).dir === "string"
|
|
1137
|
+
? ((cfg.mdMirror as Record<string, unknown>).dir as string)
|
|
1138
|
+
: undefined,
|
|
1139
|
+
}
|
|
1140
|
+
: undefined,
|
|
1013
1141
|
};
|
|
1014
1142
|
}
|
|
1015
1143
|
|
package/openclaw.plugin.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"id": "memory-lancedb-pro",
|
|
3
3
|
"name": "Memory (LanceDB Pro)",
|
|
4
4
|
"description": "Enhanced LanceDB-backed long-term memory with hybrid retrieval, multi-scope isolation, long-context chunking, and management CLI",
|
|
5
|
-
"version": "1.0.
|
|
5
|
+
"version": "1.0.32",
|
|
6
6
|
"kind": "memory",
|
|
7
7
|
"configSchema": {
|
|
8
8
|
"type": "object",
|
|
@@ -270,6 +270,21 @@
|
|
|
270
270
|
}
|
|
271
271
|
}
|
|
272
272
|
}
|
|
273
|
+
},
|
|
274
|
+
"mdMirror": {
|
|
275
|
+
"type": "object",
|
|
276
|
+
"additionalProperties": false,
|
|
277
|
+
"properties": {
|
|
278
|
+
"enabled": {
|
|
279
|
+
"type": "boolean",
|
|
280
|
+
"default": false,
|
|
281
|
+
"description": "Enable dual-write: store memories in both LanceDB and human-readable Markdown files"
|
|
282
|
+
},
|
|
283
|
+
"dir": {
|
|
284
|
+
"type": "string",
|
|
285
|
+
"description": "Fallback directory for Markdown mirror files when agent workspace is unknown"
|
|
286
|
+
}
|
|
287
|
+
}
|
|
273
288
|
}
|
|
274
289
|
},
|
|
275
290
|
"required": [
|
|
@@ -448,6 +463,15 @@
|
|
|
448
463
|
"label": "Management Tools",
|
|
449
464
|
"help": "Enable memory_list and memory_stats tools for debugging and auditing",
|
|
450
465
|
"advanced": true
|
|
466
|
+
},
|
|
467
|
+
"mdMirror.enabled": {
|
|
468
|
+
"label": "Markdown Mirror",
|
|
469
|
+
"help": "Write a human-readable Markdown copy alongside LanceDB storage (dual-write mode)"
|
|
470
|
+
},
|
|
471
|
+
"mdMirror.dir": {
|
|
472
|
+
"label": "Mirror Fallback Directory",
|
|
473
|
+
"help": "Fallback directory when agent workspace mapping is unavailable",
|
|
474
|
+
"advanced": true
|
|
451
475
|
}
|
|
452
476
|
}
|
|
453
477
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "memory-lancedb-pro",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.32",
|
|
4
4
|
"description": "OpenClaw enhanced LanceDB memory plugin with hybrid retrieval (Vector + BM25), cross-encoder rerank, multi-scope isolation, long-context chunking, and management CLI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.ts",
|
|
@@ -46,15 +46,10 @@ const FORCE_RETRIEVE_PATTERNS = [
|
|
|
46
46
|
function normalizeQuery(query: string): string {
|
|
47
47
|
let s = query.trim();
|
|
48
48
|
|
|
49
|
-
// 1. Strip OpenClaw injected metadata
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
const parts = s.split(/\n\s*\n/, 2);
|
|
54
|
-
if (parts.length === 2) {
|
|
55
|
-
s = parts[1].trim();
|
|
56
|
-
}
|
|
57
|
-
}
|
|
49
|
+
// 1. Strip OpenClaw injected metadata headers (Conversation info or Sender).
|
|
50
|
+
// Use a global regex to strip all metadata blocks including following blank lines.
|
|
51
|
+
const metadataPattern = /^(Conversation info|Sender) \(untrusted metadata\):[\s\S]*?\n\s*\n/gim;
|
|
52
|
+
s = s.replace(metadataPattern, "");
|
|
58
53
|
|
|
59
54
|
// 2. Strip OpenClaw cron wrapper prefix.
|
|
60
55
|
s = s.trim().replace(/^\[cron:[^\]]+\]\s*/i, "");
|
package/src/tools.ts
CHANGED
|
@@ -24,12 +24,18 @@ export const MEMORY_CATEGORIES = [
|
|
|
24
24
|
"other",
|
|
25
25
|
] as const;
|
|
26
26
|
|
|
27
|
+
export type MdMirrorWriter = (
|
|
28
|
+
entry: { text: string; category: string; scope: string; timestamp?: number },
|
|
29
|
+
meta?: { source?: string; agentId?: string },
|
|
30
|
+
) => Promise<void>;
|
|
31
|
+
|
|
27
32
|
interface ToolContext {
|
|
28
33
|
retriever: MemoryRetriever;
|
|
29
34
|
store: MemoryStore;
|
|
30
35
|
scopeManager: MemoryScopeManager;
|
|
31
36
|
embedder: Embedder;
|
|
32
37
|
agentId?: string;
|
|
38
|
+
mdMirror?: MdMirrorWriter | null;
|
|
33
39
|
}
|
|
34
40
|
|
|
35
41
|
function resolveAgentId(runtimeAgentId: unknown, fallback?: string): string | undefined {
|
|
@@ -301,6 +307,14 @@ export function registerMemoryStoreTool(
|
|
|
301
307
|
scope: targetScope,
|
|
302
308
|
});
|
|
303
309
|
|
|
310
|
+
// Dual-write to Markdown mirror if enabled
|
|
311
|
+
if (context.mdMirror) {
|
|
312
|
+
await context.mdMirror(
|
|
313
|
+
{ text, category: category as string, scope: targetScope, timestamp: entry.timestamp },
|
|
314
|
+
{ source: "memory_store", agentId },
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
|
|
304
318
|
return {
|
|
305
319
|
content: [
|
|
306
320
|
{
|
package/test/cli-smoke.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
|
-
import { mkdtempSync, rmSync } from "node:fs";
|
|
2
|
+
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
|
|
@@ -47,14 +47,23 @@ async function runCliSmoke() {
|
|
|
47
47
|
const program = new Command();
|
|
48
48
|
program.exitOverride();
|
|
49
49
|
|
|
50
|
+
const { MemoryStore } = jiti("../src/store.ts");
|
|
51
|
+
|
|
52
|
+
const store = new MemoryStore({
|
|
53
|
+
dbPath: path.join(workDir, "target-db"),
|
|
54
|
+
vectorDim: 4,
|
|
55
|
+
});
|
|
56
|
+
|
|
50
57
|
const context = {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
retriever: {},
|
|
54
|
-
scopeManager: {},
|
|
58
|
+
store,
|
|
59
|
+
// Only used for similarity-based dedupe when the import file has no id.
|
|
60
|
+
retriever: { retrieve: async () => [] },
|
|
61
|
+
scopeManager: { getDefaultScope: () => "global" },
|
|
55
62
|
migrator: {},
|
|
56
|
-
//
|
|
57
|
-
embedder: {
|
|
63
|
+
// Stub embedder used by import/reembed.
|
|
64
|
+
embedder: {
|
|
65
|
+
embedPassage: async () => [0, 0, 0, 0],
|
|
66
|
+
},
|
|
58
67
|
};
|
|
59
68
|
|
|
60
69
|
// Register commands under `memory-pro`
|
|
@@ -78,7 +87,71 @@ async function runCliSmoke() {
|
|
|
78
87
|
"--dry-run",
|
|
79
88
|
]);
|
|
80
89
|
|
|
81
|
-
// 3)
|
|
90
|
+
// 3) import should preserve id and be idempotent (skip on second import)
|
|
91
|
+
const importId = "smoke_import_id_1";
|
|
92
|
+
const importPhrase = `smoke-import-${Date.now()}`;
|
|
93
|
+
const importFile = path.join(workDir, "import-test.json");
|
|
94
|
+
|
|
95
|
+
writeFileSync(
|
|
96
|
+
importFile,
|
|
97
|
+
JSON.stringify(
|
|
98
|
+
{
|
|
99
|
+
version: "1.0",
|
|
100
|
+
exportedAt: new Date().toISOString(),
|
|
101
|
+
count: 1,
|
|
102
|
+
filters: {},
|
|
103
|
+
memories: [
|
|
104
|
+
{
|
|
105
|
+
id: importId,
|
|
106
|
+
text: `Import smoke test. UniquePhrase=${importPhrase}.`,
|
|
107
|
+
category: "other",
|
|
108
|
+
scope: "global",
|
|
109
|
+
importance: 0.3,
|
|
110
|
+
timestamp: Date.now(),
|
|
111
|
+
metadata: "{}",
|
|
112
|
+
},
|
|
113
|
+
],
|
|
114
|
+
},
|
|
115
|
+
null,
|
|
116
|
+
2,
|
|
117
|
+
),
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
const captureLogs = async (argv) => {
|
|
121
|
+
const logs = [];
|
|
122
|
+
const origLog = console.log;
|
|
123
|
+
console.log = (...args) => logs.push(args.join(" "));
|
|
124
|
+
try {
|
|
125
|
+
await program.parseAsync(argv);
|
|
126
|
+
} finally {
|
|
127
|
+
console.log = origLog;
|
|
128
|
+
}
|
|
129
|
+
return logs.join("\n");
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const out1 = await captureLogs([
|
|
133
|
+
"node",
|
|
134
|
+
"openclaw",
|
|
135
|
+
"memory-pro",
|
|
136
|
+
"import",
|
|
137
|
+
importFile,
|
|
138
|
+
"--scope",
|
|
139
|
+
"agent:smoke",
|
|
140
|
+
]);
|
|
141
|
+
assert.match(out1, /Import completed: 1 imported/, out1);
|
|
142
|
+
|
|
143
|
+
const out2 = await captureLogs([
|
|
144
|
+
"node",
|
|
145
|
+
"openclaw",
|
|
146
|
+
"memory-pro",
|
|
147
|
+
"import",
|
|
148
|
+
importFile,
|
|
149
|
+
"--scope",
|
|
150
|
+
"agent:smoke",
|
|
151
|
+
]);
|
|
152
|
+
assert.match(out2, /Import completed: 0 imported, 1 skipped/, out2);
|
|
153
|
+
|
|
154
|
+
// 4) Access reinforcement formula smoke test
|
|
82
155
|
const { parseAccessMetadata, buildUpdatedMetadata, computeEffectiveHalfLife } =
|
|
83
156
|
jiti("../src/access-tracker.ts");
|
|
84
157
|
|