@ubundi/openclaw-cortex 1.7.5 → 2.0.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 +89 -27
- package/dist/adapters/cortex/client.d.ts.map +1 -1
- package/dist/adapters/cortex/client.js +2 -1
- package/dist/adapters/cortex/client.js.map +1 -1
- package/dist/features/recall/formatter.d.ts +5 -0
- package/dist/features/recall/formatter.d.ts.map +1 -1
- package/dist/features/recall/formatter.js +79 -5
- package/dist/features/recall/formatter.js.map +1 -1
- package/dist/features/recall/handler.d.ts +5 -1
- package/dist/features/recall/handler.d.ts.map +1 -1
- package/dist/features/recall/handler.js +5 -4
- package/dist/features/recall/handler.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/internal/dedupe.d.ts +21 -0
- package/dist/internal/dedupe.d.ts.map +1 -0
- package/dist/internal/dedupe.js +66 -0
- package/dist/internal/dedupe.js.map +1 -0
- package/dist/plugin/config/schema.d.ts +10 -0
- package/dist/plugin/config/schema.d.ts.map +1 -1
- package/dist/plugin/config/schema.js +2 -0
- package/dist/plugin/config/schema.js.map +1 -1
- package/dist/plugin/index.d.ts +4 -0
- package/dist/plugin/index.d.ts.map +1 -1
- package/dist/plugin/index.js +162 -19
- package/dist/plugin/index.js.map +1 -1
- package/openclaw.plugin.json +76 -2
- package/package.json +6 -1
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client-side deduplication for memory saves.
|
|
3
|
+
*
|
|
4
|
+
* Maintains a rolling window of recent saves and checks new text
|
|
5
|
+
* against them using word-set Jaccard similarity.
|
|
6
|
+
*/
|
|
7
|
+
export declare class RecentSaves {
|
|
8
|
+
private entries;
|
|
9
|
+
private readonly windowMs;
|
|
10
|
+
private readonly similarityThreshold;
|
|
11
|
+
constructor(windowMinutes: number, similarityThreshold?: number);
|
|
12
|
+
/** Prune entries older than the window. */
|
|
13
|
+
private prune;
|
|
14
|
+
/** Check if text is a near-duplicate of a recent save. */
|
|
15
|
+
isDuplicate(text: string): boolean;
|
|
16
|
+
/** Record a save so future checks can detect duplicates. */
|
|
17
|
+
record(text: string): void;
|
|
18
|
+
/** Number of entries currently in the window (for testing/stats). */
|
|
19
|
+
get size(): number;
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=dedupe.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dedupe.d.ts","sourceRoot":"","sources":["../../src/internal/dedupe.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AA+BH,qBAAa,WAAW;IACtB,OAAO,CAAC,OAAO,CAAmB;IAClC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,mBAAmB,CAAS;gBAEjC,aAAa,EAAE,MAAM,EAAE,mBAAmB,SAAM;IAK5D,2CAA2C;IAC3C,OAAO,CAAC,KAAK;IAKb,0DAA0D;IAC1D,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO;IAYlC,4DAA4D;IAC5D,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAK1B,qEAAqE;IACrE,IAAI,IAAI,IAAI,MAAM,CAGjB;CACF"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client-side deduplication for memory saves.
|
|
3
|
+
*
|
|
4
|
+
* Maintains a rolling window of recent saves and checks new text
|
|
5
|
+
* against them using word-set Jaccard similarity.
|
|
6
|
+
*/
|
|
7
|
+
/** Normalize and tokenize text into a word set for similarity comparison. */
|
|
8
|
+
function toWordSet(text) {
|
|
9
|
+
return new Set(text
|
|
10
|
+
.toLowerCase()
|
|
11
|
+
.replace(/\[type:\w+\]/g, "")
|
|
12
|
+
.replace(/\[importance:\w+\]/g, "")
|
|
13
|
+
.replace(/[^\w\s]/g, " ")
|
|
14
|
+
.split(/\s+/)
|
|
15
|
+
.filter((w) => w.length > 2));
|
|
16
|
+
}
|
|
17
|
+
/** Jaccard similarity between two word sets (intersection / union). */
|
|
18
|
+
function jaccardSimilarity(a, b) {
|
|
19
|
+
if (a.size === 0 && b.size === 0)
|
|
20
|
+
return 1;
|
|
21
|
+
let intersection = 0;
|
|
22
|
+
for (const word of a) {
|
|
23
|
+
if (b.has(word))
|
|
24
|
+
intersection++;
|
|
25
|
+
}
|
|
26
|
+
const union = a.size + b.size - intersection;
|
|
27
|
+
return union === 0 ? 0 : intersection / union;
|
|
28
|
+
}
|
|
29
|
+
export class RecentSaves {
|
|
30
|
+
entries = [];
|
|
31
|
+
windowMs;
|
|
32
|
+
similarityThreshold;
|
|
33
|
+
constructor(windowMinutes, similarityThreshold = 0.7) {
|
|
34
|
+
this.windowMs = windowMinutes * 60_000;
|
|
35
|
+
this.similarityThreshold = similarityThreshold;
|
|
36
|
+
}
|
|
37
|
+
/** Prune entries older than the window. */
|
|
38
|
+
prune() {
|
|
39
|
+
const cutoff = Date.now() - this.windowMs;
|
|
40
|
+
this.entries = this.entries.filter((e) => e.timestamp >= cutoff);
|
|
41
|
+
}
|
|
42
|
+
/** Check if text is a near-duplicate of a recent save. */
|
|
43
|
+
isDuplicate(text) {
|
|
44
|
+
this.prune();
|
|
45
|
+
const words = toWordSet(text);
|
|
46
|
+
if (words.size === 0)
|
|
47
|
+
return false;
|
|
48
|
+
for (const entry of this.entries) {
|
|
49
|
+
if (jaccardSimilarity(words, entry.words) >= this.similarityThreshold) {
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
/** Record a save so future checks can detect duplicates. */
|
|
56
|
+
record(text) {
|
|
57
|
+
this.prune();
|
|
58
|
+
this.entries.push({ words: toWordSet(text), timestamp: Date.now() });
|
|
59
|
+
}
|
|
60
|
+
/** Number of entries currently in the window (for testing/stats). */
|
|
61
|
+
get size() {
|
|
62
|
+
this.prune();
|
|
63
|
+
return this.entries.length;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
//# sourceMappingURL=dedupe.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dedupe.js","sourceRoot":"","sources":["../../src/internal/dedupe.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAOH,6EAA6E;AAC7E,SAAS,SAAS,CAAC,IAAY;IAC7B,OAAO,IAAI,GAAG,CACZ,IAAI;SACD,WAAW,EAAE;SACb,OAAO,CAAC,eAAe,EAAE,EAAE,CAAC;SAC5B,OAAO,CAAC,qBAAqB,EAAE,EAAE,CAAC;SAClC,OAAO,CAAC,UAAU,EAAE,GAAG,CAAC;SACxB,KAAK,CAAC,KAAK,CAAC;SACZ,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAC/B,CAAC;AACJ,CAAC;AAED,uEAAuE;AACvE,SAAS,iBAAiB,CAAC,CAAc,EAAE,CAAc;IACvD,IAAI,CAAC,CAAC,IAAI,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,KAAK,CAAC;QAAE,OAAO,CAAC,CAAC;IAC3C,IAAI,YAAY,GAAG,CAAC,CAAC;IACrB,KAAK,MAAM,IAAI,IAAI,CAAC,EAAE,CAAC;QACrB,IAAI,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC;YAAE,YAAY,EAAE,CAAC;IAClC,CAAC;IACD,MAAM,KAAK,GAAG,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC,IAAI,GAAG,YAAY,CAAC;IAC7C,OAAO,KAAK,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,YAAY,GAAG,KAAK,CAAC;AAChD,CAAC;AAED,MAAM,OAAO,WAAW;IACd,OAAO,GAAgB,EAAE,CAAC;IACjB,QAAQ,CAAS;IACjB,mBAAmB,CAAS;IAE7C,YAAY,aAAqB,EAAE,mBAAmB,GAAG,GAAG;QAC1D,IAAI,CAAC,QAAQ,GAAG,aAAa,GAAG,MAAM,CAAC;QACvC,IAAI,CAAC,mBAAmB,GAAG,mBAAmB,CAAC;IACjD,CAAC;IAED,2CAA2C;IACnC,KAAK;QACX,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,QAAQ,CAAC;QAC1C,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,SAAS,IAAI,MAAM,CAAC,CAAC;IACnE,CAAC;IAED,0DAA0D;IAC1D,WAAW,CAAC,IAAY;QACtB,IAAI,CAAC,KAAK,EAAE,CAAC;QACb,MAAM,KAAK,GAAG,SAAS,CAAC,IAAI,CAAC,CAAC;QAC9B,IAAI,KAAK,CAAC,IAAI,KAAK,CAAC;YAAE,OAAO,KAAK,CAAC;QACnC,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;YACjC,IAAI,iBAAiB,CAAC,KAAK,EAAE,KAAK,CAAC,KAAK,CAAC,IAAI,IAAI,CAAC,mBAAmB,EAAE,CAAC;gBACtE,OAAO,IAAI,CAAC;YACd,CAAC;QACH,CAAC;QACD,OAAO,KAAK,CAAC;IACf,CAAC;IAED,4DAA4D;IAC5D,MAAM,CAAC,IAAY;QACjB,IAAI,CAAC,KAAK,EAAE,CAAC;QACb,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,SAAS,CAAC,IAAI,CAAC,EAAE,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;IACvE,CAAC;IAED,qEAAqE;IACrE,IAAI,IAAI;QACN,IAAI,CAAC,KAAK,EAAE,CAAC;QACb,OAAO,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC;IAC7B,CAAC;CACF"}
|
|
@@ -14,6 +14,8 @@ export declare const CortexConfigSchema: z.ZodObject<{
|
|
|
14
14
|
transcriptSync: z.ZodDefault<z.ZodBoolean>;
|
|
15
15
|
captureMaxPayloadBytes: z.ZodDefault<z.ZodNumber>;
|
|
16
16
|
captureFilter: z.ZodDefault<z.ZodBoolean>;
|
|
17
|
+
dedupeWindowMinutes: z.ZodDefault<z.ZodNumber>;
|
|
18
|
+
noveltyThreshold: z.ZodDefault<z.ZodNumber>;
|
|
17
19
|
auditLog: z.ZodDefault<z.ZodBoolean>;
|
|
18
20
|
namespace: z.ZodDefault<z.ZodString>;
|
|
19
21
|
/**
|
|
@@ -41,6 +43,8 @@ export declare const CortexConfigSchema: z.ZodObject<{
|
|
|
41
43
|
transcriptSync: boolean;
|
|
42
44
|
captureMaxPayloadBytes: number;
|
|
43
45
|
captureFilter: boolean;
|
|
46
|
+
dedupeWindowMinutes: number;
|
|
47
|
+
noveltyThreshold: number;
|
|
44
48
|
auditLog: boolean;
|
|
45
49
|
namespace: string;
|
|
46
50
|
userId?: string | undefined;
|
|
@@ -60,6 +64,8 @@ export declare const CortexConfigSchema: z.ZodObject<{
|
|
|
60
64
|
transcriptSync?: boolean | undefined;
|
|
61
65
|
captureMaxPayloadBytes?: number | undefined;
|
|
62
66
|
captureFilter?: boolean | undefined;
|
|
67
|
+
dedupeWindowMinutes?: number | undefined;
|
|
68
|
+
noveltyThreshold?: number | undefined;
|
|
63
69
|
auditLog?: boolean | undefined;
|
|
64
70
|
namespace?: string | undefined;
|
|
65
71
|
recallReferenceDate?: string | undefined;
|
|
@@ -85,6 +91,8 @@ export declare const configSchema: {
|
|
|
85
91
|
transcriptSync?: boolean | undefined;
|
|
86
92
|
captureMaxPayloadBytes?: number | undefined;
|
|
87
93
|
captureFilter?: boolean | undefined;
|
|
94
|
+
dedupeWindowMinutes?: number | undefined;
|
|
95
|
+
noveltyThreshold?: number | undefined;
|
|
88
96
|
auditLog?: boolean | undefined;
|
|
89
97
|
namespace?: string | undefined;
|
|
90
98
|
recallReferenceDate?: string | undefined;
|
|
@@ -102,6 +110,8 @@ export declare const configSchema: {
|
|
|
102
110
|
transcriptSync: boolean;
|
|
103
111
|
captureMaxPayloadBytes: number;
|
|
104
112
|
captureFilter: boolean;
|
|
113
|
+
dedupeWindowMinutes: number;
|
|
114
|
+
noveltyThreshold: number;
|
|
105
115
|
auditLog: boolean;
|
|
106
116
|
namespace: string;
|
|
107
117
|
userId?: string | undefined;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../../src/plugin/config/schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAwBxB,eAAO,MAAM,kBAAkB
|
|
1
|
+
{"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../../src/plugin/config/schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAwBxB,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;;IAqB7B;;;;;;;;;OASG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAEH,CAAC;AAEH,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAE9D;;;GAGG;AACH,eAAO,MAAM,YAAY;qBACN,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAGzB,CAAC"}
|
|
@@ -31,6 +31,8 @@ export const CortexConfigSchema = z.object({
|
|
|
31
31
|
transcriptSync: z.boolean().default(true),
|
|
32
32
|
captureMaxPayloadBytes: z.number().int().min(1024).max(1_048_576).default(262_144),
|
|
33
33
|
captureFilter: z.boolean().default(true),
|
|
34
|
+
dedupeWindowMinutes: z.number().int().min(0).max(1440).default(30),
|
|
35
|
+
noveltyThreshold: z.number().min(0).max(1).default(0.85),
|
|
34
36
|
auditLog: z.boolean().default(false),
|
|
35
37
|
namespace: z.string().min(1).default("openclaw"),
|
|
36
38
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"schema.js","sourceRoot":"","sources":["../../../src/plugin/config/schema.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"schema.js","sourceRoot":"","sources":["../../../src/plugin/config/schema.ts"],"names":[],"mappings":"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"}
|
package/dist/plugin/index.d.ts
CHANGED
|
@@ -113,6 +113,8 @@ declare const plugin: {
|
|
|
113
113
|
transcriptSync?: boolean | undefined;
|
|
114
114
|
captureMaxPayloadBytes?: number | undefined;
|
|
115
115
|
captureFilter?: boolean | undefined;
|
|
116
|
+
dedupeWindowMinutes?: number | undefined;
|
|
117
|
+
noveltyThreshold?: number | undefined;
|
|
116
118
|
auditLog?: boolean | undefined;
|
|
117
119
|
namespace?: string | undefined;
|
|
118
120
|
recallReferenceDate?: string | undefined;
|
|
@@ -130,6 +132,8 @@ declare const plugin: {
|
|
|
130
132
|
transcriptSync: boolean;
|
|
131
133
|
captureMaxPayloadBytes: number;
|
|
132
134
|
captureFilter: boolean;
|
|
135
|
+
dedupeWindowMinutes: number;
|
|
136
|
+
noveltyThreshold: number;
|
|
133
137
|
auditLog: boolean;
|
|
134
138
|
namespace: string;
|
|
135
139
|
userId?: string | undefined;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/plugin/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/plugin/index.ts"],"names":[],"mappings":"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"}
|
package/dist/plugin/index.js
CHANGED
|
@@ -12,6 +12,7 @@ import { loadOrCreateUserId } from "../internal/identity/user-id.js";
|
|
|
12
12
|
import { BAKED_API_KEY } from "../internal/identity/api-key.js";
|
|
13
13
|
import { formatMemories } from "../features/recall/formatter.js";
|
|
14
14
|
import { AuditLogger } from "../internal/audit/audit-logger.js";
|
|
15
|
+
import { RecentSaves } from "../internal/dedupe.js";
|
|
15
16
|
import { injectAgentInstructions } from "../internal/agent-instructions.js";
|
|
16
17
|
import { createCheckpointHandler } from "../features/checkpoint/handler.js";
|
|
17
18
|
import { createHeartbeatHandler } from "../features/heartbeat/handler.js";
|
|
@@ -39,6 +40,21 @@ function mergePrependContext(recoveryContext, recallContext) {
|
|
|
39
40
|
return `${recoveryContext}\n\n${recallContext}`;
|
|
40
41
|
return recoveryContext ?? recallContext;
|
|
41
42
|
}
|
|
43
|
+
function isAbortError(err) {
|
|
44
|
+
if (typeof err === "object" && err !== null && "name" in err) {
|
|
45
|
+
return err.name === "AbortError";
|
|
46
|
+
}
|
|
47
|
+
return String(err).includes("AbortError");
|
|
48
|
+
}
|
|
49
|
+
async function resetCompletedAfterAbort(client, userId) {
|
|
50
|
+
try {
|
|
51
|
+
const knowledge = await client.knowledge(undefined, userId);
|
|
52
|
+
return knowledge.total_memories === 0 && knowledge.total_sessions === 0;
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
42
58
|
async function bootstrapClient(client, logger, knowledgeState, userId) {
|
|
43
59
|
try {
|
|
44
60
|
const healthy = await client.healthCheck();
|
|
@@ -100,14 +116,13 @@ const plugin = {
|
|
|
100
116
|
return;
|
|
101
117
|
}
|
|
102
118
|
// BUILD_API_KEY must be injected into dist at build/publish time.
|
|
103
|
-
// If the placeholder is still present, Cortex
|
|
104
|
-
//
|
|
105
|
-
|
|
106
|
-
if
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
}
|
|
119
|
+
// If the placeholder "__OPENCLAW_API_KEY__" is still present, Cortex
|
|
120
|
+
// calls will fail — but we allow it so unit tests (which mock the
|
|
121
|
+
// client) can exercise registration. Only bail on a truly empty key
|
|
122
|
+
// (which can happen if inject-api-key.mjs runs with an empty var).
|
|
123
|
+
if (!BAKED_API_KEY) {
|
|
124
|
+
api.logger.error("Cortex plugin misconfigured: empty API key. Rebuild with BUILD_API_KEY=... npm run build, or install the published package.");
|
|
125
|
+
return;
|
|
111
126
|
}
|
|
112
127
|
const config = parsed.data;
|
|
113
128
|
const client = new CortexClient(config.baseUrl, BAKED_API_KEY);
|
|
@@ -165,7 +180,11 @@ const plugin = {
|
|
|
165
180
|
// Track last messages for /checkpoint command (populated by agent_end wrapper)
|
|
166
181
|
let lastMessages = [];
|
|
167
182
|
const recoveryCheckedSessions = new Set();
|
|
168
|
-
const recallHandler = createRecallHandler(client, config, api.logger, recallMetrics, knowledgeState, () => userId, auditLoggerProxy)
|
|
183
|
+
const recallHandler = createRecallHandler(client, config, api.logger, recallMetrics, knowledgeState, () => userId, auditLoggerProxy, (stats) => {
|
|
184
|
+
sessionStats.recallCount++;
|
|
185
|
+
sessionStats.recallMemoriesTotal += stats.memoriesReturned;
|
|
186
|
+
sessionStats.recallDuplicatesCollapsed += stats.collapsedCount;
|
|
187
|
+
});
|
|
169
188
|
// Auto-Recall: inject relevant memories before every agent turn
|
|
170
189
|
registerHookCompat(api, "before_agent_start", async (event, ctx) => {
|
|
171
190
|
const activeSessionKey = resolveSessionKey(ctx, sessionId);
|
|
@@ -221,11 +240,24 @@ const plugin = {
|
|
|
221
240
|
name: "openclaw-cortex.heartbeat",
|
|
222
241
|
description: "Periodic health check and knowledge state refresh",
|
|
223
242
|
});
|
|
243
|
+
// --- Session Stats ---
|
|
244
|
+
const sessionStats = {
|
|
245
|
+
saves: 0,
|
|
246
|
+
savesSkippedDedupe: 0,
|
|
247
|
+
savesSkippedNovelty: 0,
|
|
248
|
+
searches: 0,
|
|
249
|
+
recallCount: 0,
|
|
250
|
+
recallMemoriesTotal: 0,
|
|
251
|
+
recallDuplicatesCollapsed: 0,
|
|
252
|
+
};
|
|
224
253
|
// --- Agent Tools ---
|
|
254
|
+
const recentSaves = config.dedupeWindowMinutes > 0
|
|
255
|
+
? new RecentSaves(config.dedupeWindowMinutes)
|
|
256
|
+
: null;
|
|
225
257
|
if (api.registerTool) {
|
|
226
258
|
api.registerTool({
|
|
227
259
|
name: "cortex_search_memory",
|
|
228
|
-
description: "Search long-term memory for facts, preferences, and past context. Use when you need to recall something the user mentioned before or retrieve stored knowledge.",
|
|
260
|
+
description: "Search long-term memory for facts, preferences, and past context. Use when you need to recall something the user mentioned before or retrieve stored knowledge. Use 'mode' to narrow results to a specific category.",
|
|
229
261
|
parameters: {
|
|
230
262
|
type: "object",
|
|
231
263
|
properties: {
|
|
@@ -238,28 +270,55 @@ const plugin = {
|
|
|
238
270
|
description: "Maximum number of memories to return (1-50)",
|
|
239
271
|
default: 10,
|
|
240
272
|
},
|
|
273
|
+
mode: {
|
|
274
|
+
type: "string",
|
|
275
|
+
enum: ["all", "decisions", "preferences", "facts", "recent"],
|
|
276
|
+
description: "Filter memories by category. 'all' returns everything (default), 'decisions' returns architectural/design choices, 'preferences' returns user likes/settings, 'facts' returns durable knowledge, 'recent' prioritizes recency over relevance.",
|
|
277
|
+
},
|
|
241
278
|
},
|
|
242
279
|
required: ["query"],
|
|
243
280
|
},
|
|
244
281
|
async execute(_id, params) {
|
|
245
282
|
const query = String(params.query ?? "");
|
|
246
283
|
const limit = Math.min(Math.max(Number(params.limit) || 10, 1), 50);
|
|
284
|
+
const mode = typeof params.mode === "string" ? params.mode : "all";
|
|
247
285
|
await userIdReady;
|
|
248
|
-
|
|
286
|
+
// Augment query based on mode to improve retrieval precision
|
|
287
|
+
let effectiveQuery = query;
|
|
288
|
+
let queryType = "combined";
|
|
289
|
+
switch (mode) {
|
|
290
|
+
case "decisions":
|
|
291
|
+
effectiveQuery = `[type:decision] ${query}`;
|
|
292
|
+
queryType = "factual";
|
|
293
|
+
break;
|
|
294
|
+
case "preferences":
|
|
295
|
+
effectiveQuery = `[type:preference] ${query}`;
|
|
296
|
+
queryType = "factual";
|
|
297
|
+
break;
|
|
298
|
+
case "facts":
|
|
299
|
+
effectiveQuery = `[type:fact] ${query}`;
|
|
300
|
+
queryType = "factual";
|
|
301
|
+
break;
|
|
302
|
+
case "recent":
|
|
303
|
+
queryType = "combined";
|
|
304
|
+
break;
|
|
305
|
+
}
|
|
306
|
+
api.logger.debug?.(`Cortex search: "${effectiveQuery.slice(0, 80)}" (limit=${limit}, mode=${mode})`);
|
|
307
|
+
sessionStats.searches++;
|
|
249
308
|
void auditLoggerProxy.log({
|
|
250
309
|
feature: "tool-search-memory",
|
|
251
310
|
method: "POST",
|
|
252
311
|
endpoint: "/v1/recall",
|
|
253
|
-
payload:
|
|
312
|
+
payload: effectiveQuery,
|
|
254
313
|
userId,
|
|
255
314
|
});
|
|
256
315
|
try {
|
|
257
316
|
const doRecall = async (attempt = 0) => {
|
|
258
317
|
try {
|
|
259
|
-
return await client.recall(
|
|
318
|
+
return await client.recall(effectiveQuery, config.toolTimeoutMs, {
|
|
260
319
|
limit,
|
|
261
320
|
userId: userId,
|
|
262
|
-
queryType
|
|
321
|
+
queryType,
|
|
263
322
|
});
|
|
264
323
|
}
|
|
265
324
|
catch (err) {
|
|
@@ -286,7 +345,7 @@ const plugin = {
|
|
|
286
345
|
});
|
|
287
346
|
api.registerTool({
|
|
288
347
|
name: "cortex_save_memory",
|
|
289
|
-
description: "Explicitly save a fact, preference, or piece of information to long-term memory. Use when the user asks you to remember something specific.",
|
|
348
|
+
description: "Explicitly save a fact, preference, or piece of information to long-term memory. Use when the user asks you to remember something specific. Provide type and importance to help organize memories for better retrieval.",
|
|
290
349
|
parameters: {
|
|
291
350
|
type: "object",
|
|
292
351
|
properties: {
|
|
@@ -294,6 +353,20 @@ const plugin = {
|
|
|
294
353
|
type: "string",
|
|
295
354
|
description: "The information to save to memory (a fact, preference, or context)",
|
|
296
355
|
},
|
|
356
|
+
type: {
|
|
357
|
+
type: "string",
|
|
358
|
+
enum: ["preference", "decision", "fact", "transient"],
|
|
359
|
+
description: "Category of this memory. 'preference' for user likes/dislikes/settings, 'decision' for architectural or design choices, 'fact' for durable knowledge, 'transient' for temporary state that may change soon.",
|
|
360
|
+
},
|
|
361
|
+
importance: {
|
|
362
|
+
type: "string",
|
|
363
|
+
enum: ["high", "normal", "low"],
|
|
364
|
+
description: "How important this memory is for future recall. 'high' for critical preferences or decisions, 'normal' for general facts, 'low' for minor context.",
|
|
365
|
+
},
|
|
366
|
+
checkNovelty: {
|
|
367
|
+
type: "boolean",
|
|
368
|
+
description: "When true, checks if a similar memory already exists before saving. Skips the save if a near-duplicate is found. Defaults to false.",
|
|
369
|
+
},
|
|
297
370
|
},
|
|
298
371
|
required: ["text"],
|
|
299
372
|
},
|
|
@@ -302,27 +375,69 @@ const plugin = {
|
|
|
302
375
|
if (!text || text.length < 5) {
|
|
303
376
|
return { content: [{ type: "text", text: "Text too short to save as a memory." }] };
|
|
304
377
|
}
|
|
378
|
+
const memoryType = typeof params.type === "string" ? params.type : undefined;
|
|
379
|
+
const importance = typeof params.importance === "string" ? params.importance : undefined;
|
|
380
|
+
const checkNovelty = params.checkNovelty === true;
|
|
305
381
|
await userIdReady;
|
|
306
382
|
if (!userId) {
|
|
307
383
|
api.logger.warn("Cortex save: missing user_id");
|
|
308
384
|
return { content: [{ type: "text", text: "Failed to save memory: Cortex ingest requires user_id." }] };
|
|
309
385
|
}
|
|
310
|
-
|
|
386
|
+
// Prepend metadata tags so they're stored with the memory text
|
|
387
|
+
const metaTags = [];
|
|
388
|
+
if (memoryType)
|
|
389
|
+
metaTags.push(`[type:${memoryType}]`);
|
|
390
|
+
if (importance)
|
|
391
|
+
metaTags.push(`[importance:${importance}]`);
|
|
392
|
+
const enrichedText = metaTags.length > 0 ? `${metaTags.join(" ")} ${text}` : text;
|
|
393
|
+
// Item 5: Client-side dedupe — skip if near-duplicate was saved recently
|
|
394
|
+
if (recentSaves?.isDuplicate(text)) {
|
|
395
|
+
api.logger.debug?.(`Cortex save skipped (duplicate within window): "${text.slice(0, 60)}"`);
|
|
396
|
+
sessionStats.savesSkippedDedupe++;
|
|
397
|
+
return {
|
|
398
|
+
content: [{ type: "text", text: "This memory is very similar to one saved recently. Skipped to avoid duplication." }],
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
// Item 7: Novelty check — query existing memories to see if this is already stored
|
|
402
|
+
if (checkNovelty) {
|
|
403
|
+
try {
|
|
404
|
+
const existing = await client.retrieve(text, 1, "fast", config.toolTimeoutMs, "factual", { userId });
|
|
405
|
+
const topScore = existing.results?.[0]?.score ?? 0;
|
|
406
|
+
if (topScore >= config.noveltyThreshold) {
|
|
407
|
+
api.logger.debug?.(`Cortex save skipped (not novel, score=${topScore.toFixed(2)}): "${text.slice(0, 60)}"`);
|
|
408
|
+
sessionStats.savesSkippedNovelty++;
|
|
409
|
+
recentSaves?.record(text);
|
|
410
|
+
return {
|
|
411
|
+
content: [{
|
|
412
|
+
type: "text",
|
|
413
|
+
text: `This memory already exists (similarity ${(topScore * 100).toFixed(0)}%). Skipped to avoid duplication.`,
|
|
414
|
+
}],
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
catch (err) {
|
|
419
|
+
// Novelty check is best-effort — proceed with save on failure
|
|
420
|
+
api.logger.debug?.(`Cortex novelty check failed, proceeding with save: ${String(err)}`);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
api.logger.debug?.(`Cortex save: "${enrichedText.slice(0, 80)}"`);
|
|
311
424
|
void auditLoggerProxy.log({
|
|
312
425
|
feature: "tool-save-memory",
|
|
313
426
|
method: "POST",
|
|
314
427
|
endpoint: "/v1/remember",
|
|
315
|
-
payload:
|
|
428
|
+
payload: enrichedText,
|
|
316
429
|
sessionId,
|
|
317
430
|
userId,
|
|
318
431
|
});
|
|
319
432
|
try {
|
|
320
433
|
const now = new Date();
|
|
321
434
|
const referenceDate = now.toISOString().slice(0, 10);
|
|
322
|
-
await client.remember(
|
|
435
|
+
await client.remember(enrichedText, sessionId, config.toolTimeoutMs, referenceDate, userId, "openclaw", "OpenClaw");
|
|
323
436
|
if (knowledgeState) {
|
|
324
437
|
knowledgeState.hasMemories = true;
|
|
325
438
|
}
|
|
439
|
+
recentSaves?.record(text);
|
|
440
|
+
sessionStats.saves++;
|
|
326
441
|
api.logger.debug?.("Cortex remember accepted");
|
|
327
442
|
return {
|
|
328
443
|
content: [{
|
|
@@ -335,10 +450,12 @@ const plugin = {
|
|
|
335
450
|
api.logger.warn(`Cortex save failed, falling back to async ingest: ${String(err)}`);
|
|
336
451
|
try {
|
|
337
452
|
const referenceDate = new Date().toISOString();
|
|
338
|
-
const job = await client.submitIngest(
|
|
453
|
+
const job = await client.submitIngest(enrichedText, sessionId, referenceDate, userId, "openclaw", "OpenClaw");
|
|
339
454
|
if (knowledgeState) {
|
|
340
455
|
knowledgeState.hasMemories = true;
|
|
341
456
|
}
|
|
457
|
+
recentSaves?.record(text);
|
|
458
|
+
sessionStats.saves++;
|
|
342
459
|
return {
|
|
343
460
|
content: [{
|
|
344
461
|
type: "text",
|
|
@@ -550,6 +667,25 @@ const plugin = {
|
|
|
550
667
|
console.log(` Auto-Recall: ${config.autoRecall ? "on" : "off"}`);
|
|
551
668
|
console.log(` Auto-Capture: ${config.autoCapture ? "on" : "off"}`);
|
|
552
669
|
console.log(` File Sync: ${config.fileSync ? "on" : "off"}`);
|
|
670
|
+
console.log(` Dedupe Window: ${config.dedupeWindowMinutes > 0 ? `${config.dedupeWindowMinutes}min` : "off"}`);
|
|
671
|
+
// Session activity stats
|
|
672
|
+
const totalSkipped = sessionStats.savesSkippedDedupe + sessionStats.savesSkippedNovelty;
|
|
673
|
+
const avgRecallMemories = sessionStats.recallCount > 0
|
|
674
|
+
? (sessionStats.recallMemoriesTotal / sessionStats.recallCount).toFixed(1)
|
|
675
|
+
: "0";
|
|
676
|
+
console.log("");
|
|
677
|
+
console.log("Session Activity");
|
|
678
|
+
console.log("-".repeat(50));
|
|
679
|
+
console.log(` Saves: ${sessionStats.saves}`);
|
|
680
|
+
if (totalSkipped > 0) {
|
|
681
|
+
console.log(` Skipped: ${totalSkipped} (${sessionStats.savesSkippedDedupe} dedupe, ${sessionStats.savesSkippedNovelty} novelty)`);
|
|
682
|
+
}
|
|
683
|
+
console.log(` Searches: ${sessionStats.searches}`);
|
|
684
|
+
console.log(` Recalls: ${sessionStats.recallCount}`);
|
|
685
|
+
console.log(` Avg memories/recall: ${avgRecallMemories}`);
|
|
686
|
+
if (sessionStats.recallDuplicatesCollapsed > 0) {
|
|
687
|
+
console.log(` Duplicates collapsed: ${sessionStats.recallDuplicatesCollapsed}`);
|
|
688
|
+
}
|
|
553
689
|
});
|
|
554
690
|
cortex
|
|
555
691
|
.command("memories")
|
|
@@ -710,6 +846,13 @@ const plugin = {
|
|
|
710
846
|
console.log(` Suppressions: ${d.codex_suggestion_suppressions}`);
|
|
711
847
|
}
|
|
712
848
|
catch (err) {
|
|
849
|
+
if (isAbortError(err) && await resetCompletedAfterAbort(client, userId)) {
|
|
850
|
+
console.log("");
|
|
851
|
+
console.log(" Memory reset complete.");
|
|
852
|
+
console.log("");
|
|
853
|
+
console.log(" The server finished the reset, but the request ended before deletion stats were returned.");
|
|
854
|
+
return;
|
|
855
|
+
}
|
|
713
856
|
console.error(`\n Reset failed: ${String(err)}`);
|
|
714
857
|
process.exitCode = 1;
|
|
715
858
|
}
|