@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.
@@ -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;;;;;;;;;;;;;;;;;IAmB7B;;;;;;;;;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"}
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":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB;;GAEG;AACH,MAAM,QAAQ,GAAG,CAAC;KACf,MAAM,EAAE;KACR,GAAG,CAAC,6BAA6B,CAAC;KAClC,MAAM,CACL,CAAC,GAAG,EAAE,EAAE;IACN,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC;IAC5B,IAAI,MAAM,CAAC,QAAQ,KAAK,QAAQ;QAAE,OAAO,IAAI,CAAC;IAC9C,wDAAwD;IACxD,IACE,MAAM,CAAC,QAAQ,KAAK,OAAO;QAC3B,CAAC,MAAM,CAAC,QAAQ,KAAK,WAAW,IAAI,MAAM,CAAC,QAAQ,KAAK,WAAW,CAAC,EACpE,CAAC;QACD,OAAO,IAAI,CAAC;IACd,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC,EACD,EAAE,OAAO,EAAE,0DAA0D,EAAE,CACxE,CAAC;AAEJ,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAC,CAAC,MAAM,CAAC;IACzC,OAAO,EAAE,QAAQ,CAAC,OAAO,CACvB,6DAA6D,CAC9D;IACD,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,EAAE;IACpC,UAAU,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC;IACrC,WAAW,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC;IACtC,WAAW,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC;IACxD,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC;IACvD,eAAe,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,SAAS,EAAE,WAAW,EAAE,UAAU,EAAE,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,UAAU,CAAC;IAC1F,aAAa,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,UAAU,EAAE,UAAU,EAAE,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC;IACxG,eAAe,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC;IACrE,aAAa,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC;IACpE,QAAQ,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC;IACnC,cAAc,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC;IACzC,sBAAsB,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC;IAClF,aAAa,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC;IACxC,mBAAmB,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC;IAClE,gBAAgB,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC;IACxD,QAAQ,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC;IACpC,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,UAAU,CAAC;IAChD;;;;;;;;;OASG;IACH,mBAAmB,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,EAAE;CAClD,CAAC,CAAC;AAIH;;;GAGG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG;IAC1B,SAAS,CAAC,KAAc;QACtB,OAAO,kBAAkB,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;IAC7C,CAAC;CACF,CAAC"}
@@ -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":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/plugin/index.ts"],"names":[],"mappings":"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"}
@@ -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 calls will all fail at runtime.
104
- // Keep unit tests working by allowing placeholder in test environment only.
105
- const hasInjectedApiKey = BAKED_API_KEY !== "__OPENCLAW_API_KEY__";
106
- if (!hasInjectedApiKey) {
107
- api.logger.error("Cortex plugin misconfigured: build-time API key placeholder detected. Rebuild with BUILD_API_KEY=... npm run build, or install the published package.");
108
- if (process.env.NODE_ENV !== "test") {
109
- return;
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
- api.logger.debug?.(`Cortex search: "${query.slice(0, 80)}" (limit=${limit})`);
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: query,
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(query, config.toolTimeoutMs, {
318
+ return await client.recall(effectiveQuery, config.toolTimeoutMs, {
260
319
  limit,
261
320
  userId: userId,
262
- queryType: "combined",
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
- api.logger.debug?.(`Cortex save: "${text.slice(0, 80)}"`);
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: text,
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(text, sessionId, config.toolTimeoutMs, referenceDate, userId, "openclaw", "OpenClaw");
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(text, sessionId, referenceDate, userId, "openclaw", "OpenClaw");
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
  }