myagentmemory 0.4.11 → 0.4.13

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/src/core.ts CHANGED
@@ -152,6 +152,114 @@ function normalizeContent(content: string): string {
152
152
  return content.trim();
153
153
  }
154
154
 
155
+ const SECRET_PATTERNS: RegExp[] = [
156
+ /\bsk-[A-Za-z0-9_-]{16,}\b/g,
157
+ /\bgh[pousr]_[A-Za-z0-9]{20,}\b/g,
158
+ /\bAKIA[0-9A-Z]{16}\b/g,
159
+ /\b(?:api[_-]?key|access[_-]?token|secret)\s*[:=]\s*["']?[A-Za-z0-9_./+=-]{16,}["']?/gi,
160
+ ];
161
+
162
+ /** Redact common credential shapes before content reaches disk or agent context. */
163
+ export function redactSecrets(content: string): { content: string; redacted: boolean } {
164
+ let redactedContent = content;
165
+ for (const pattern of SECRET_PATTERNS) {
166
+ redactedContent = redactedContent.replace(pattern, "[REDACTED_SECRET]");
167
+ }
168
+ return { content: redactedContent, redacted: redactedContent !== content };
169
+ }
170
+
171
+ function isInactiveMemoryEntry(entry: string, now: Date): boolean {
172
+ const metadataHeader = entry.split(/\n\s*\n/, 1)[0];
173
+ if (/^\s*(?:[-*]\s*)?Trust:\s*untrusted\s*\.?\s*$/im.test(metadataHeader)) return true;
174
+ if (/^\s*(?:[-*]\s*)?Status:\s*(?:expired|superseded|revoked|retired)\s*\.?\s*$/im.test(metadataHeader)) {
175
+ return true;
176
+ }
177
+
178
+ const validUntil = metadataHeader.match(/^\s*(?:[-*]\s*)?Valid until:?\s+(\d{4}-\d{2}-\d{2})\s*\.?\s*$/im)?.[1];
179
+ if (validUntil && validUntil < now.toISOString().slice(0, 10)) return true;
180
+ return false;
181
+ }
182
+
183
+ function splitLogicalMemoryEntries(content: string): { entries: string[]; timestampDelimited: boolean } {
184
+ const normalized = content.replace(/^\uFEFF/, "");
185
+ const marker = /^<!--\s*(?:last updated:\s*)?\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} \[[^\]]+\]\s*-->$/gm;
186
+ const starts = [...normalized.matchAll(marker)].map((match) => match.index ?? 0);
187
+ if (starts.length === 0) {
188
+ return { entries: normalized.split(/\n\s*\n/), timestampDelimited: false };
189
+ }
190
+
191
+ const entries: string[] = [];
192
+ const preamble = normalized.slice(0, starts[0]).trim();
193
+ if (preamble) entries.push(preamble);
194
+ for (let i = 0; i < starts.length; i++) {
195
+ entries.push(normalized.slice(starts[i], starts[i + 1] ?? normalized.length).trim());
196
+ }
197
+ return { entries, timestampDelimited: true };
198
+ }
199
+
200
+ function isUnmarkedInactiveHeader(entry: string, now: Date): boolean {
201
+ if (!isInactiveMemoryEntry(entry, now)) return false;
202
+ return entry.split("\n").every((line) => {
203
+ const trimmed = line.trim();
204
+ return (
205
+ !trimmed ||
206
+ /^#{1,6}\s+/.test(trimmed) ||
207
+ /^<!--.*-->$/.test(trimmed) ||
208
+ /^(?:[-*]\s*)?(?:Trust:|Status:|Valid until:?\s|Source:)/i.test(trimmed)
209
+ );
210
+ });
211
+ }
212
+
213
+ /** Apply trust, lifecycle, and secret policy to complete logical write entries. */
214
+ export function filterMemoryForContext(content: string, now = new Date()): string {
215
+ const { entries, timestampDelimited } = splitLogicalMemoryEntries(content);
216
+ const activeEntries: string[] = [];
217
+ for (const entry of entries) {
218
+ const inactive = isInactiveMemoryEntry(entry, now);
219
+ if (!timestampDelimited && isUnmarkedInactiveHeader(entry, now)) {
220
+ // Without write markers there is no reliable boundary after a metadata-only
221
+ // header. Fail closed instead of treating the following body paragraphs as
222
+ // independent trusted entries.
223
+ break;
224
+ }
225
+ if (!inactive) activeEntries.push(entry);
226
+ }
227
+ return redactSecrets(activeEntries.join("\n\n")).content.trim();
228
+ }
229
+
230
+ function sanitizeSourceUri(sourceUri?: string): string | undefined {
231
+ if (!sourceUri?.trim()) return undefined;
232
+ const singleLine = [...sourceUri]
233
+ .map((char) => {
234
+ const code = char.charCodeAt(0);
235
+ return code <= 31 || code === 127 ? " " : char;
236
+ })
237
+ .join("")
238
+ .trim()
239
+ .slice(0, 2_048);
240
+ return redactSecrets(singleLine).content;
241
+ }
242
+
243
+ function escapeEntryMarkers(content: string): string {
244
+ return content.replace(
245
+ /^<!--\s*(?:last updated:\s*)?\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} \[[^\]]+\]\s*-->$/gm,
246
+ (line) => line.replace("<!--", "&lt;!--"),
247
+ );
248
+ }
249
+
250
+ function formatStoredEntry(
251
+ content: string,
252
+ metadata: string,
253
+ sourceUri?: string,
254
+ ): { entry: string; redacted: boolean } {
255
+ const safeContent = redactSecrets(escapeEntryMarkers(content));
256
+ const source = sanitizeSourceUri(sourceUri);
257
+ return {
258
+ entry: `${metadata}\n${safeContent.content}${source ? `\nSource: ${source}` : ""}`,
259
+ redacted: safeContent.redacted || (!!sourceUri && source !== sourceUri.trim()),
260
+ };
261
+ }
262
+
155
263
  export function truncateLines(lines: string[], maxLines: number, mode: TruncateMode) {
156
264
  if (maxLines <= 0 || lines.length <= maxLines) {
157
265
  return { lines, truncated: false };
@@ -328,7 +436,7 @@ export function buildMemoryContext(searchResults?: string): string {
328
436
  if (scratchpad?.trim()) {
329
437
  const openItems = parseScratchpad(scratchpad).filter((i) => !i.done);
330
438
  if (openItems.length > 0) {
331
- const serialized = serializeScratchpad(openItems);
439
+ const serialized = filterMemoryForContext(serializeScratchpad(openItems));
332
440
  const section = formatContextSection(
333
441
  "## SCRATCHPAD.md (working context)",
334
442
  serialized,
@@ -347,21 +455,23 @@ export function buildMemoryContext(searchResults?: string): string {
347
455
  const yesterday = yesterdayStr();
348
456
 
349
457
  const todayContent = readFileSafe(dailyPath(today));
350
- if (todayContent?.trim()) {
458
+ const safeTodayContent = todayContent ? filterMemoryForContext(todayContent) : "";
459
+ if (safeTodayContent) {
351
460
  const section = formatContextSection(
352
461
  `## Daily log: ${today} (today)`,
353
- todayContent,
354
- "end",
462
+ safeTodayContent,
463
+ "middle",
355
464
  CONTEXT_DAILY_MAX_LINES,
356
465
  CONTEXT_DAILY_MAX_CHARS,
357
466
  );
358
467
  if (section) sections.push(section);
359
468
  }
360
469
 
361
- if (searchResults?.trim()) {
470
+ const safeSearchResults = searchResults ? filterMemoryForContext(searchResults) : "";
471
+ if (safeSearchResults) {
362
472
  const section = formatContextSection(
363
473
  "## Relevant memories (auto-retrieved)",
364
- searchResults,
474
+ safeSearchResults,
365
475
  "start",
366
476
  CONTEXT_SEARCH_MAX_LINES,
367
477
  CONTEXT_SEARCH_MAX_CHARS,
@@ -370,10 +480,11 @@ export function buildMemoryContext(searchResults?: string): string {
370
480
  }
371
481
 
372
482
  const longTerm = readFileSafe(MEMORY_FILE);
373
- if (longTerm?.trim()) {
483
+ const safeLongTerm = longTerm ? filterMemoryForContext(longTerm) : "";
484
+ if (safeLongTerm) {
374
485
  const section = formatContextSection(
375
486
  "## MEMORY.md (long-term)",
376
- longTerm,
487
+ safeLongTerm,
377
488
  "middle",
378
489
  CONTEXT_LONG_TERM_MAX_LINES,
379
490
  CONTEXT_LONG_TERM_MAX_CHARS,
@@ -382,10 +493,11 @@ export function buildMemoryContext(searchResults?: string): string {
382
493
  }
383
494
 
384
495
  const yesterdayContent = readFileSafe(dailyPath(yesterday));
385
- if (yesterdayContent?.trim()) {
496
+ const safeYesterdayContent = yesterdayContent ? filterMemoryForContext(yesterdayContent) : "";
497
+ if (safeYesterdayContent) {
386
498
  const section = formatContextSection(
387
499
  `## Daily log: ${yesterday} (yesterday)`,
388
- yesterdayContent,
500
+ safeYesterdayContent,
389
501
  "end",
390
502
  CONTEXT_DAILY_MAX_LINES,
391
503
  CONTEXT_DAILY_MAX_CHARS,
@@ -399,15 +511,8 @@ export function buildMemoryContext(searchResults?: string): string {
399
511
 
400
512
  const context = `# Memory\n\n${sections.join("\n\n---\n\n")}`;
401
513
  if (context.length > CONTEXT_MAX_CHARS) {
402
- const result = buildPreview(context, {
403
- maxLines: Number.POSITIVE_INFINITY,
404
- maxChars: CONTEXT_MAX_CHARS,
405
- mode: "start",
406
- });
407
- const note = result.truncated
408
- ? `\n\n[truncated overall context: showing ${result.previewChars}/${result.totalChars} chars]`
409
- : "";
410
- return `${result.preview}${note}`;
514
+ const note = "\n\n[truncated overall context to 16000 chars]";
515
+ return context.slice(0, CONTEXT_MAX_CHARS - note.length).trimEnd() + note;
411
516
  }
412
517
 
413
518
  return context;
@@ -430,10 +535,11 @@ function buildTopicsContextSection(): string | null {
430
535
  for (const file of topicFiles) {
431
536
  const slug = file.replace(/\.md$/, "");
432
537
  const content = readFileSafe(path.join(TOPICS_DIR, file));
433
- if (!content?.trim()) continue;
434
- const titleMatch = content.match(/^# Topic:\s*(.+)$/m);
538
+ const safeContent = content ? filterMemoryForContext(content) : "";
539
+ if (!safeContent) continue;
540
+ const titleMatch = safeContent.match(/^# Topic:\s*(.+)$/m);
435
541
  const title = titleMatch?.[1]?.trim() || slug;
436
- entries.push(...parseTopicEntries(title, slug, content));
542
+ entries.push(...parseTopicEntries(title, slug, safeContent));
437
543
  }
438
544
 
439
545
  if (entries.length === 0) return null;
@@ -1056,9 +1162,61 @@ export async function getQmdHealth(): Promise<QmdHealthInfo | null> {
1056
1162
  });
1057
1163
  }
1058
1164
 
1165
+ function resolveCaseInsensitivePath(root: string, relativePath: string): string | null {
1166
+ let current = root;
1167
+ for (const segment of relativePath.split(/[/\\]+/).filter(Boolean)) {
1168
+ if (segment === "." || segment === "..") return null;
1169
+ let children: string[];
1170
+ try {
1171
+ children = fs.readdirSync(current);
1172
+ } catch {
1173
+ return null;
1174
+ }
1175
+ const child =
1176
+ children.find((name) => name === segment) ??
1177
+ children.find((name) => name.toLowerCase() === segment.toLowerCase());
1178
+ if (!child) return null;
1179
+ current = path.join(current, child);
1180
+ }
1181
+ return current;
1182
+ }
1183
+
1184
+ function resolveQmdSourcePath(filePath: string): string | null {
1185
+ const qmdUri = filePath.match(/^qmd:\/\/([^/]+)\/?(.*)$/i);
1186
+ let relativePath = qmdUri ? qmdUri[2] : filePath;
1187
+ if (relativePath.toLowerCase().startsWith(`${QMD_COLLECTION_NAME.toLowerCase()}/`)) {
1188
+ relativePath = relativePath.slice(QMD_COLLECTION_NAME.length + 1);
1189
+ }
1190
+
1191
+ const root = path.resolve(MEMORY_DIR);
1192
+ if (path.isAbsolute(relativePath)) {
1193
+ const candidate = path.resolve(relativePath);
1194
+ if (candidate !== root && !candidate.startsWith(`${root}${path.sep}`)) return null;
1195
+ return fs.existsSync(candidate) ? candidate : null;
1196
+ }
1197
+ return resolveCaseInsensitivePath(root, relativePath.replace(/^\/+/, ""));
1198
+ }
1199
+
1200
+ function qmdResultPassesSourcePolicy(filePath: string | undefined, snippet: string): boolean {
1201
+ if (!filePath) return false;
1202
+ const sourcePath = resolveQmdSourcePath(filePath);
1203
+ if (!sourcePath) return false;
1204
+ const source = readFileSafe(sourcePath);
1205
+ if (!source) return false;
1206
+
1207
+ const activeSource = filterMemoryForContext(source);
1208
+ const snippetLines = snippet
1209
+ .split("\n")
1210
+ .map((line) => line.trim())
1211
+ .filter((line) => line.length >= 8);
1212
+ return snippetLines.length > 0 && snippetLines.every((line) => activeSource.includes(line));
1213
+ }
1214
+
1059
1215
  /** Search for memories relevant to the user's prompt. Returns formatted markdown or empty string on error. */
1060
1216
  export async function searchRelevantMemories(prompt: string): Promise<string> {
1061
1217
  if (!qmdAvailable || !prompt.trim()) return "";
1218
+ let timer: ReturnType<typeof setTimeout> | undefined;
1219
+ const controller = new AbortController();
1062
1220
 
1063
1221
  // Sanitize: strip control chars, limit to 200 chars for the search query
1064
1222
  const sanitized = prompt
@@ -1073,19 +1231,25 @@ export async function searchRelevantMemories(prompt: string): Promise<string> {
1073
1231
  if (!hasCollection) return "";
1074
1232
 
1075
1233
  const results = await Promise.race([
1076
- runQmdSearch("keyword", sanitized, 3),
1077
- new Promise<never>((_, reject) => setTimeout(() => reject(new Error("timeout")), 3_000)),
1234
+ runQmdSearch("keyword", sanitized, 3, { signal: controller.signal }),
1235
+ new Promise<never>((_, reject) => {
1236
+ timer = setTimeout(() => {
1237
+ controller.abort();
1238
+ reject(new Error("timeout"));
1239
+ }, 3_000);
1240
+ }),
1078
1241
  ]);
1079
1242
 
1080
1243
  if (!results || results.results.length === 0) return "";
1081
1244
 
1082
1245
  const snippets = results.results
1083
1246
  .map((r) => {
1084
- const text = getQmdResultText(r);
1085
- if (!text.trim()) return null;
1247
+ const text = filterMemoryForContext(getQmdResultText(r));
1248
+ if (!text) return null;
1086
1249
  const filePath = getQmdResultPath(r);
1250
+ if (!qmdResultPassesSourcePolicy(filePath, text)) return null;
1087
1251
  const filePart = filePath ? `_${filePath}_` : "";
1088
- return filePart ? `${filePart}\n${text.trim()}` : text.trim();
1252
+ return filePart ? `${filePart}\n${text}` : text;
1089
1253
  })
1090
1254
  .filter(Boolean);
1091
1255
 
@@ -1093,12 +1257,15 @@ export async function searchRelevantMemories(prompt: string): Promise<string> {
1093
1257
  return snippets.join("\n\n---\n\n");
1094
1258
  } catch {
1095
1259
  return "";
1260
+ } finally {
1261
+ clearTimeout(timer);
1096
1262
  }
1097
1263
  }
1098
1264
 
1099
1265
  export interface QmdSearchResult {
1100
1266
  path?: string;
1101
1267
  file?: string;
1268
+ context?: string;
1102
1269
  score?: number;
1103
1270
  content?: string;
1104
1271
  chunk?: string;
@@ -1112,7 +1279,20 @@ export function getQmdResultPath(r: QmdSearchResult): string | undefined {
1112
1279
  }
1113
1280
 
1114
1281
  export function getQmdResultText(r: QmdSearchResult): string {
1115
- return r.content ?? r.chunk ?? r.snippet ?? "";
1282
+ const text = r.content ?? r.chunk ?? r.snippet ?? "";
1283
+ const folderContext = r.context?.trim();
1284
+ let normalized = text.trimStart();
1285
+ if (folderContext) {
1286
+ const prefix = `Folder Context: ${folderContext}`;
1287
+ if (normalized.startsWith(prefix)) {
1288
+ const remainder = normalized.slice(prefix.length);
1289
+ if (/^(?:\r?\n){2}/.test(remainder)) {
1290
+ normalized = remainder.replace(/^(?:\r?\n){2}/, "");
1291
+ }
1292
+ }
1293
+ }
1294
+
1295
+ return normalized.replace(/^@@ -\d+(?:,\d+)?(?: \+\d+(?:,\d+)?)? @@ \(\d+ before, \d+ after\)(?:\r?\n){1,2}/, "");
1116
1296
  }
1117
1297
 
1118
1298
  function stripAnsi(text: string): string {
@@ -1146,12 +1326,13 @@ export function runQmdSearch(
1146
1326
  mode: "keyword" | "semantic" | "deep",
1147
1327
  query: string,
1148
1328
  limit: number,
1329
+ options: { signal?: AbortSignal } = {},
1149
1330
  ): Promise<{ results: QmdSearchResult[]; stderr: string }> {
1150
1331
  const subcommand = mode === "keyword" ? "search" : mode === "semantic" ? "vsearch" : "query";
1151
1332
  const args = [subcommand, "--json", "-c", QMD_COLLECTION_NAME, "-n", String(limit), query];
1152
1333
 
1153
1334
  return new Promise((resolve, reject) => {
1154
- execFileFn("qmd", args, { timeout: 60_000 }, (err, stdout, stderr) => {
1335
+ execFileFn("qmd", args, { timeout: 60_000, signal: options.signal }, (err, stdout, stderr) => {
1155
1336
  if (err) {
1156
1337
  reject(new Error(stderr?.trim() || err.message));
1157
1338
  return;
@@ -1171,6 +1352,39 @@ export function runQmdSearch(
1171
1352
  });
1172
1353
  }
1173
1354
 
1355
+ /**
1356
+ * Best-effort check of whether vector embeddings are actually usable for
1357
+ * semantic/deep search right now. Runs a tiny semantic probe and looks for
1358
+ * qmd's "need embeddings" warning. Bounded by a short timeout because the very
1359
+ * first semantic query can trigger a model download — returns "unknown" rather
1360
+ * than blocking on it. "ready" means the probe ran without the warning; it does
1361
+ * not prove the index has content.
1362
+ */
1363
+ export async function probeEmbeddings(): Promise<"ready" | "missing" | "unknown"> {
1364
+ let timer: ReturnType<typeof setTimeout> | undefined;
1365
+ // Abort the underlying qmd child when the timeout fires so it does not keep
1366
+ // the event loop open until its own 60s timeout and hang the CLI.
1367
+ const controller = new AbortController();
1368
+ try {
1369
+ const { stderr } = await Promise.race([
1370
+ runQmdSearch("semantic", "memory", 1, { signal: controller.signal }),
1371
+ new Promise<never>((_, reject) => {
1372
+ timer = setTimeout(() => {
1373
+ controller.abort();
1374
+ reject(new Error("timeout"));
1375
+ }, 4_000);
1376
+ }),
1377
+ ]);
1378
+ return /need embeddings/i.test(stderr ?? "") ? "missing" : "ready";
1379
+ } catch (err) {
1380
+ const msg = err instanceof Error ? err.message : String(err);
1381
+ if (/need embeddings/i.test(msg)) return "missing";
1382
+ return "unknown";
1383
+ } finally {
1384
+ clearTimeout(timer);
1385
+ }
1386
+ }
1387
+
1174
1388
  // ---------------------------------------------------------------------------
1175
1389
  // Standalone tool functions
1176
1390
  // ---------------------------------------------------------------------------
@@ -1188,6 +1402,7 @@ export async function memoryWrite(params: {
1188
1402
  sessionId?: string;
1189
1403
  topic?: string;
1190
1404
  date?: string;
1405
+ sourceUri?: string;
1191
1406
  }): Promise<ToolResult> {
1192
1407
  ensureDirs();
1193
1408
  const target = params.target ?? "daily";
@@ -1198,18 +1413,19 @@ export async function memoryWrite(params: {
1198
1413
  if (target === "daily") {
1199
1414
  const filePath = dailyPath(todayStr());
1200
1415
  const existing = readFileSafe(filePath) ?? "";
1201
- const existingPreview = buildPreview(existing, {
1416
+ const safeExisting = redactSecrets(existing).content;
1417
+ const existingPreview = buildPreview(safeExisting, {
1202
1418
  maxLines: RESPONSE_PREVIEW_MAX_LINES,
1203
1419
  maxChars: RESPONSE_PREVIEW_MAX_CHARS,
1204
1420
  mode: "end",
1205
1421
  });
1206
1422
  const existingSnippet = existingPreview.preview
1207
- ? `\n\n${formatPreviewBlock("Existing daily log preview", existing, "end")}`
1423
+ ? `\n\n${formatPreviewBlock("Existing daily log preview", safeExisting, "end")}`
1208
1424
  : "\n\nDaily log was empty.";
1209
1425
 
1210
1426
  const separator = existing.trim() ? "\n\n" : "";
1211
- const stamped = `<!-- ${ts} [${sid}] -->\n${content}`;
1212
- fs.writeFileSync(filePath, existing + separator + stamped, "utf-8");
1427
+ const stored = formatStoredEntry(content, `<!-- ${ts} [${sid}] -->`, params.sourceUri);
1428
+ fs.writeFileSync(filePath, existing + separator + stored.entry, "utf-8");
1213
1429
  await ensureQmdAvailableForUpdate();
1214
1430
  scheduleQmdUpdate();
1215
1431
  return {
@@ -1220,6 +1436,8 @@ export async function memoryWrite(params: {
1220
1436
  mode: "append",
1221
1437
  sessionId: sid,
1222
1438
  timestamp: ts,
1439
+ sourceUri: sanitizeSourceUri(params.sourceUri),
1440
+ redacted: stored.redacted,
1223
1441
  qmdUpdateMode: getQmdUpdateMode(),
1224
1442
  existingPreview,
1225
1443
  },
@@ -1237,21 +1455,26 @@ export async function memoryWrite(params: {
1237
1455
  }
1238
1456
  const filePath = topicPath(slug);
1239
1457
  const existing = readFileSafe(filePath) ?? "";
1240
- const existingPreview = buildPreview(existing, {
1458
+ const safeExisting = redactSecrets(existing).content;
1459
+ const existingPreview = buildPreview(safeExisting, {
1241
1460
  maxLines: RESPONSE_PREVIEW_MAX_LINES,
1242
1461
  maxChars: RESPONSE_PREVIEW_MAX_CHARS,
1243
1462
  mode: "end",
1244
1463
  });
1245
1464
  const existingSnippet = existingPreview.preview
1246
- ? `\n\n${formatPreviewBlock("Existing topic preview", existing, "end")}`
1465
+ ? `\n\n${formatPreviewBlock("Existing topic preview", safeExisting, "end")}`
1247
1466
  : "\n\nTopic file was empty.";
1248
1467
 
1249
1468
  const linkDate = params.date?.trim() || todayStr();
1250
1469
  const header = `# Topic: ${topic}\n\n<!-- created: ${ts} [${sid}] -->\n`;
1251
1470
  const separator = existing.trim() ? "\n\n" : "";
1252
1471
  const base = existing.trim() ? existing : header.trimEnd();
1253
- const stamped = `<!-- ${ts} [${sid}] -->\n${content.trim()}\nDaily: [[${linkDate}]]`;
1254
- fs.writeFileSync(filePath, `${base}${separator}${stamped}`, "utf-8");
1472
+ const stored = formatStoredEntry(
1473
+ `${content.trim()}\nDaily: [[${linkDate}]]`,
1474
+ `<!-- ${ts} [${sid}] -->`,
1475
+ params.sourceUri,
1476
+ );
1477
+ fs.writeFileSync(filePath, `${base}${separator}${stored.entry}`, "utf-8");
1255
1478
  await ensureQmdAvailableForUpdate();
1256
1479
  scheduleQmdUpdate();
1257
1480
  return {
@@ -1265,6 +1488,8 @@ export async function memoryWrite(params: {
1265
1488
  topic,
1266
1489
  slug,
1267
1490
  date: linkDate,
1491
+ sourceUri: sanitizeSourceUri(params.sourceUri),
1492
+ redacted: stored.redacted,
1268
1493
  qmdUpdateMode: getQmdUpdateMode(),
1269
1494
  existingPreview,
1270
1495
  },
@@ -1274,18 +1499,19 @@ export async function memoryWrite(params: {
1274
1499
  // long_term
1275
1500
  const memFile = getMemoryFile();
1276
1501
  const existing = readFileSafe(memFile) ?? "";
1277
- const existingPreview = buildPreview(existing, {
1502
+ const safeExisting = redactSecrets(existing).content;
1503
+ const existingPreview = buildPreview(safeExisting, {
1278
1504
  maxLines: RESPONSE_PREVIEW_MAX_LINES,
1279
1505
  maxChars: RESPONSE_PREVIEW_MAX_CHARS,
1280
1506
  mode: "middle",
1281
1507
  });
1282
1508
  const existingSnippet = existingPreview.preview
1283
- ? `\n\n${formatPreviewBlock("Existing MEMORY.md preview", existing, "middle")}`
1509
+ ? `\n\n${formatPreviewBlock("Existing MEMORY.md preview", safeExisting, "middle")}`
1284
1510
  : "\n\nMEMORY.md was empty.";
1285
1511
 
1286
1512
  if (mode === "overwrite") {
1287
- const stamped = `<!-- last updated: ${ts} [${sid}] -->\n${content}`;
1288
- fs.writeFileSync(memFile, stamped, "utf-8");
1513
+ const stored = formatStoredEntry(content, `<!-- last updated: ${ts} [${sid}] -->`, params.sourceUri);
1514
+ fs.writeFileSync(memFile, stored.entry, "utf-8");
1289
1515
  await ensureQmdAvailableForUpdate();
1290
1516
  scheduleQmdUpdate();
1291
1517
  return {
@@ -1296,6 +1522,8 @@ export async function memoryWrite(params: {
1296
1522
  mode: "overwrite",
1297
1523
  sessionId: sid,
1298
1524
  timestamp: ts,
1525
+ sourceUri: sanitizeSourceUri(params.sourceUri),
1526
+ redacted: stored.redacted,
1299
1527
  qmdUpdateMode: getQmdUpdateMode(),
1300
1528
  existingPreview,
1301
1529
  },
@@ -1304,8 +1532,8 @@ export async function memoryWrite(params: {
1304
1532
 
1305
1533
  // append (default)
1306
1534
  const separator = existing.trim() ? "\n\n" : "";
1307
- const stamped = `<!-- ${ts} [${sid}] -->\n${content}`;
1308
- fs.writeFileSync(memFile, existing + separator + stamped, "utf-8");
1535
+ const stored = formatStoredEntry(content, `<!-- ${ts} [${sid}] -->`, params.sourceUri);
1536
+ fs.writeFileSync(memFile, existing + separator + stored.entry, "utf-8");
1309
1537
  await ensureQmdAvailableForUpdate();
1310
1538
  scheduleQmdUpdate();
1311
1539
  return {
@@ -1316,6 +1544,8 @@ export async function memoryWrite(params: {
1316
1544
  mode: "append",
1317
1545
  sessionId: sid,
1318
1546
  timestamp: ts,
1547
+ sourceUri: sanitizeSourceUri(params.sourceUri),
1548
+ redacted: stored.redacted,
1319
1549
  qmdUpdateMode: getQmdUpdateMode(),
1320
1550
  existingPreview,
1321
1551
  },
@@ -1334,7 +1564,11 @@ export async function scratchpadAction(params: {
1334
1564
  const spFile = getScratchpadFile();
1335
1565
 
1336
1566
  const existing = readFileSafe(spFile) ?? "";
1337
- let items = parseScratchpad(existing);
1567
+ let items = parseScratchpad(existing).map((item) => ({
1568
+ ...item,
1569
+ text: redactSecrets(item.text).content,
1570
+ meta: redactSecrets(item.meta).content,
1571
+ }));
1338
1572
 
1339
1573
  if (action === "list") {
1340
1574
  if (items.length === 0) {
@@ -1360,7 +1594,8 @@ export async function scratchpadAction(params: {
1360
1594
  if (!text) {
1361
1595
  return { text: "Error: 'text' is required for add.", details: {} };
1362
1596
  }
1363
- items.push({ done: false, text, meta: `<!-- ${ts} [${sid}] -->` });
1597
+ const safeText = redactSecrets(text).content;
1598
+ items.push({ done: false, text: safeText, meta: `<!-- ${ts} [${sid}] -->` });
1364
1599
  const serialized = serializeScratchpad(items);
1365
1600
  const preview = buildPreview(serialized, {
1366
1601
  maxLines: RESPONSE_PREVIEW_MAX_LINES,
@@ -1371,7 +1606,7 @@ export async function scratchpadAction(params: {
1371
1606
  await ensureQmdAvailableForUpdate();
1372
1607
  scheduleQmdUpdate();
1373
1608
  return {
1374
- text: `Added: - [ ] ${text}\n\n${formatPreviewBlock("Scratchpad preview", serialized, "start")}`,
1609
+ text: `Added: - [ ] ${safeText}\n\n${formatPreviewBlock("Scratchpad preview", serialized, "start")}`,
1375
1610
  details: {
1376
1611
  action,
1377
1612
  sessionId: sid,
@@ -1871,7 +2106,9 @@ export async function distilMemories(params?: { dryRun?: boolean; sessionId?: st
1871
2106
  const date = file.replace(/\.md$/, "");
1872
2107
  const content = readFileSafe(path.join(DAILY_DIR, file));
1873
2108
  if (!content?.trim()) continue;
1874
- allEntries.push(...parseDailyEntries(date, content));
2109
+ const safeContent = filterMemoryForContext(content);
2110
+ if (!safeContent) continue;
2111
+ allEntries.push(...parseDailyEntries(date, safeContent));
1875
2112
  }
1876
2113
  const topicEntriesByTopic = new Map<string, TopicEntry[]>();
1877
2114
  let totalTopicEntries = 0;
@@ -1879,9 +2116,11 @@ export async function distilMemories(params?: { dryRun?: boolean; sessionId?: st
1879
2116
  const slug = file.replace(/\.md$/, "");
1880
2117
  const content = readFileSafe(path.join(TOPICS_DIR, file));
1881
2118
  if (!content?.trim()) continue;
1882
- const titleMatch = content.match(/^# Topic:\s*(.+)$/m);
2119
+ const safeContent = filterMemoryForContext(content);
2120
+ if (!safeContent) continue;
2121
+ const titleMatch = safeContent.match(/^# Topic:\s*(.+)$/m);
1883
2122
  const title = titleMatch?.[1]?.trim() || slug;
1884
- const entries = parseTopicEntries(title, slug, content);
2123
+ const entries = parseTopicEntries(title, slug, safeContent);
1885
2124
  if (entries.length === 0) continue;
1886
2125
  totalTopicEntries += entries.length;
1887
2126
  allEntries.push(...entries);
@@ -1929,7 +2168,8 @@ export async function distilMemories(params?: { dryRun?: boolean; sessionId?: st
1929
2168
  let pinnedSection = "";
1930
2169
  const existingMemory = readFileSafe(MEMORY_FILE);
1931
2170
  if (existingMemory) {
1932
- const pinnedMatch = existingMemory.match(/## Pinned\n([\s\S]*?)(?=\n## |\n# |$)/);
2171
+ const safeExistingMemory = filterMemoryForContext(existingMemory);
2172
+ const pinnedMatch = safeExistingMemory.match(/## Pinned\n([\s\S]*?)(?=\n## |\n# |$)/);
1933
2173
  if (pinnedMatch) {
1934
2174
  pinnedSection = pinnedMatch[1].trim();
1935
2175
  }
package/dist/agent-memory DELETED
Binary file