memory-braid 0.4.6 → 0.4.7

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/index.ts +177 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memory-braid",
3
- "version": "0.4.6",
3
+ "version": "0.4.7",
4
4
  "description": "OpenClaw memory plugin that augments local memory with Mem0 capture and recall.",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
package/src/index.ts CHANGED
@@ -63,6 +63,107 @@ function resolveScopeFromHookContext(ctx: {
63
63
  };
64
64
  }
65
65
 
66
+ function extractHookMessageText(content: unknown): string {
67
+ if (typeof content === "string") {
68
+ return normalizeWhitespace(content);
69
+ }
70
+ if (!Array.isArray(content)) {
71
+ return "";
72
+ }
73
+
74
+ const parts: string[] = [];
75
+ for (const block of content) {
76
+ if (!block || typeof block !== "object") {
77
+ continue;
78
+ }
79
+ const item = block as { type?: unknown; text?: unknown };
80
+ if (item.type === "text" && typeof item.text === "string") {
81
+ const normalized = normalizeWhitespace(item.text);
82
+ if (normalized) {
83
+ parts.push(normalized);
84
+ }
85
+ }
86
+ }
87
+ return parts.join(" ");
88
+ }
89
+
90
+ function normalizeHookMessages(messages: unknown[]): Array<{ role: string; text: string }> {
91
+ const out: Array<{ role: string; text: string }> = [];
92
+ for (const entry of messages) {
93
+ if (!entry || typeof entry !== "object") {
94
+ continue;
95
+ }
96
+
97
+ const direct = entry as { role?: unknown; content?: unknown };
98
+ if (typeof direct.role === "string") {
99
+ const text = extractHookMessageText(direct.content);
100
+ if (text) {
101
+ out.push({ role: direct.role, text });
102
+ }
103
+ continue;
104
+ }
105
+
106
+ const wrapped = entry as { message?: { role?: unknown; content?: unknown } };
107
+ if (wrapped.message && typeof wrapped.message.role === "string") {
108
+ const text = extractHookMessageText(wrapped.message.content);
109
+ if (text) {
110
+ out.push({ role: wrapped.message.role, text });
111
+ }
112
+ }
113
+ }
114
+ return out;
115
+ }
116
+
117
+ function resolveLatestUserTurnSignature(messages?: unknown[]): string | undefined {
118
+ if (!Array.isArray(messages) || messages.length === 0) {
119
+ return undefined;
120
+ }
121
+
122
+ const normalized = normalizeHookMessages(messages);
123
+ for (let i = normalized.length - 1; i >= 0; i -= 1) {
124
+ const message = normalized[i];
125
+ if (!message || message.role !== "user") {
126
+ continue;
127
+ }
128
+ const hashSource = normalizeForHash(message.text);
129
+ if (!hashSource) {
130
+ continue;
131
+ }
132
+ return `${i}:${sha256(hashSource)}`;
133
+ }
134
+ return undefined;
135
+ }
136
+
137
+ function resolvePromptTurnSignature(prompt: string): string | undefined {
138
+ const normalized = normalizeForHash(prompt);
139
+ if (!normalized) {
140
+ return undefined;
141
+ }
142
+ return `prompt:${sha256(normalized)}`;
143
+ }
144
+
145
+ function resolveRunScopeKey(ctx: { agentId?: string; sessionKey?: string }): string {
146
+ const agentId = (ctx.agentId ?? "main").trim() || "main";
147
+ const sessionKey = (ctx.sessionKey ?? "main").trim() || "main";
148
+ return `${agentId}|${sessionKey}`;
149
+ }
150
+
151
+ function isExcludedAutoMemorySession(sessionKey?: string): boolean {
152
+ const normalized = (sessionKey ?? "").trim().toLowerCase();
153
+ if (!normalized) {
154
+ return false;
155
+ }
156
+ return (
157
+ normalized.startsWith("cron:") ||
158
+ normalized.includes(":cron:") ||
159
+ normalized.includes(":subagent:") ||
160
+ normalized.startsWith("subagent:") ||
161
+ normalized.includes(":acp:") ||
162
+ normalized.startsWith("acp:") ||
163
+ normalized.startsWith("temp:")
164
+ );
165
+ }
166
+
66
167
  function formatRelevantMemories(results: MemoryBraidResult[], maxChars = 600): string {
67
168
  const lines = results.map((entry, index) => {
68
169
  const sourceLabel = entry.source === "local" ? "local" : "mem0";
@@ -944,6 +1045,8 @@ const memoryBraidPlugin = {
944
1045
  const entityExtraction = new EntityExtractionManager(cfg.entityExtraction, log, {
945
1046
  stateDir: initialStateDir,
946
1047
  });
1048
+ const recallSeenByScope = new Map<string, string>();
1049
+ const captureSeenByScope = new Map<string, string>();
947
1050
 
948
1051
  let lifecycleTimer: NodeJS.Timeout | null = null;
949
1052
  let statePaths: StatePaths | null = null;
@@ -1231,10 +1334,48 @@ const memoryBraidPlugin = {
1231
1334
 
1232
1335
  api.on("before_agent_start", async (event, ctx) => {
1233
1336
  const runId = log.newRunId();
1337
+ const scope = resolveScopeFromHookContext(ctx);
1338
+ if (isExcludedAutoMemorySession(ctx.sessionKey)) {
1339
+ log.debug("memory_braid.search.skip", {
1340
+ runId,
1341
+ reason: "session_scope_excluded",
1342
+ workspaceHash: scope.workspaceHash,
1343
+ agentId: scope.agentId,
1344
+ sessionKey: scope.sessionKey,
1345
+ });
1346
+ return;
1347
+ }
1348
+
1234
1349
  const recallQuery = sanitizeRecallQuery(event.prompt);
1235
1350
  if (!recallQuery) {
1236
1351
  return;
1237
1352
  }
1353
+ const scopeKey = resolveRunScopeKey(ctx);
1354
+ const userTurnSignature =
1355
+ resolveLatestUserTurnSignature(event.messages) ?? resolvePromptTurnSignature(recallQuery);
1356
+ if (!userTurnSignature) {
1357
+ log.debug("memory_braid.search.skip", {
1358
+ runId,
1359
+ reason: "no_user_turn_signature",
1360
+ workspaceHash: scope.workspaceHash,
1361
+ agentId: scope.agentId,
1362
+ sessionKey: scope.sessionKey,
1363
+ });
1364
+ return;
1365
+ }
1366
+ const previousSignature = recallSeenByScope.get(scopeKey);
1367
+ if (previousSignature === userTurnSignature) {
1368
+ log.debug("memory_braid.search.skip", {
1369
+ runId,
1370
+ reason: "no_new_user_turn",
1371
+ workspaceHash: scope.workspaceHash,
1372
+ agentId: scope.agentId,
1373
+ sessionKey: scope.sessionKey,
1374
+ });
1375
+ return;
1376
+ }
1377
+ recallSeenByScope.set(scopeKey, userTurnSignature);
1378
+
1238
1379
  const toolCtx: OpenClawPluginToolContext = {
1239
1380
  config: api.config,
1240
1381
  workspaceDir: ctx.workspaceDir,
@@ -1264,7 +1405,6 @@ const memoryBraidPlugin = {
1264
1405
  limit: cfg.recall.injectTopK,
1265
1406
  });
1266
1407
  if (selected.injected.length === 0) {
1267
- const scope = resolveScopeFromHookContext(ctx);
1268
1408
  log.debug("memory_braid.search.inject", {
1269
1409
  runId,
1270
1410
  agentId: scope.agentId,
@@ -1281,7 +1421,6 @@ const memoryBraidPlugin = {
1281
1421
  }
1282
1422
 
1283
1423
  const prependContext = formatRelevantMemories(selected.injected, cfg.debug.maxSnippetChars);
1284
- const scope = resolveScopeFromHookContext(ctx);
1285
1424
  log.debug("memory_braid.search.inject", {
1286
1425
  runId,
1287
1426
  agentId: scope.agentId,
@@ -1306,6 +1445,42 @@ const memoryBraidPlugin = {
1306
1445
  }
1307
1446
  const runId = log.newRunId();
1308
1447
  const scope = resolveScopeFromHookContext(ctx);
1448
+ if (isExcludedAutoMemorySession(ctx.sessionKey)) {
1449
+ log.debug("memory_braid.capture.skip", {
1450
+ runId,
1451
+ reason: "session_scope_excluded",
1452
+ workspaceHash: scope.workspaceHash,
1453
+ agentId: scope.agentId,
1454
+ sessionKey: scope.sessionKey,
1455
+ });
1456
+ return;
1457
+ }
1458
+
1459
+ const scopeKey = resolveRunScopeKey(ctx);
1460
+ const userTurnSignature = resolveLatestUserTurnSignature(event.messages);
1461
+ if (!userTurnSignature) {
1462
+ log.debug("memory_braid.capture.skip", {
1463
+ runId,
1464
+ reason: "no_user_turn_signature",
1465
+ workspaceHash: scope.workspaceHash,
1466
+ agentId: scope.agentId,
1467
+ sessionKey: scope.sessionKey,
1468
+ });
1469
+ return;
1470
+ }
1471
+ const previousSignature = captureSeenByScope.get(scopeKey);
1472
+ if (previousSignature === userTurnSignature) {
1473
+ log.debug("memory_braid.capture.skip", {
1474
+ runId,
1475
+ reason: "no_new_user_turn",
1476
+ workspaceHash: scope.workspaceHash,
1477
+ agentId: scope.agentId,
1478
+ sessionKey: scope.sessionKey,
1479
+ });
1480
+ return;
1481
+ }
1482
+ captureSeenByScope.set(scopeKey, userTurnSignature);
1483
+
1309
1484
  const candidates = await extractCandidates({
1310
1485
  messages: event.messages,
1311
1486
  cfg,