@openclaw/memory-lancedb 2026.5.31-beta.4 → 2026.6.1-beta.1

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/dist/index.js CHANGED
@@ -89,6 +89,7 @@ const DEFAULT_TOOL_RECALL_TIMEOUT_MS = 15e3;
89
89
  const DEFAULT_TOOL_RECALL_COOLDOWN_MS = 6e4;
90
90
  const DEFAULT_AUTO_RECALL_OVERFETCH_LIMIT = 10;
91
91
  const DEFAULT_AUTO_RECALL_RESULT_CAP = 3;
92
+ const DUPLICATE_SEARCH_LIMIT = 5;
92
93
  function parsePositiveIntegerOption(value, flag) {
93
94
  if (value === void 0) return;
94
95
  const parsed = parseStrictPositiveInteger(value);
@@ -393,6 +394,9 @@ function sanitizeRecallMemoryText(text) {
393
394
  if (!stripped.trim()) return null;
394
395
  return looksLikeEnvelopeSludge(stripped) ? null : stripped;
395
396
  }
397
+ async function findCleanDuplicateMemory(db, vector) {
398
+ return (await db.search(vector, DUPLICATE_SEARCH_LIMIT, .95)).find((result) => sanitizeRecallMemoryText(result.entry.text) !== null);
399
+ }
396
400
  /**
397
401
  * Explicit sentinel strings used by `sanitizeForMemoryCapture` to locate and
398
402
  * surgically strip individual blocks. Canonical source:
@@ -419,10 +423,21 @@ const INBOUND_META_SENTINELS = [
419
423
  "Nearby reply target window (untrusted, chronological, around replied-to message):",
420
424
  "Chat history since last reply (untrusted, for context):"
421
425
  ];
426
+ const INBOUND_META_SENTINEL_LINE_RE = new RegExp(`^(?:${INBOUND_META_SENTINELS.map((sentinel) => sentinel.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|")})[^\\n]*$`, "m");
422
427
  const MESSAGE_TOOL_DELIVERY_HINTS = ["Delivery: to send a message, use the `message` tool.", "Delivery: Final assistant text is not automatically delivered in this run. Use the `message` tool to send user-visible output."];
423
428
  const MESSAGE_TOOL_DELIVERY_HINT_RE = new RegExp(`^\\s*(?:${MESSAGE_TOOL_DELIVERY_HINTS.map((hint) => hint.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|")})\\s*$`, "m");
424
429
  const HISTORY_CONTEXT_MARKER = "[Chat messages since your last reply - for context]";
425
430
  const CURRENT_MESSAGE_MARKER = "[Current message - respond to this]";
431
+ const HISTORY_CONTEXT_MARKERS = [
432
+ HISTORY_CONTEXT_MARKER,
433
+ "[Chat messages since your last reply — CONTEXT ONLY]",
434
+ "[Merged earlier messages — CONTEXT ONLY]"
435
+ ];
436
+ const CURRENT_MESSAGE_MARKERS = [
437
+ CURRENT_MESSAGE_MARKER,
438
+ "[CURRENT MESSAGE — reply to this]",
439
+ "[CURRENT MESSAGE — reply using the context above]"
440
+ ];
426
441
  const ACTIVE_TURN_RECOVERY_RE = /active-turn-recovery/i;
427
442
  /**
428
443
  * Line-anchored pattern matching any inbound-meta block header injected by
@@ -436,15 +451,17 @@ const ACTIVE_TURN_RECOVERY_RE = /active-turn-recovery/i;
436
451
  * so requiring `):` to terminate the line catches every real injection while
437
452
  * sidestepping the false-positive risk.
438
453
  *
439
- * Label segment is capped at 100 chars to avoid catastrophic backtracking on
440
- * pathological inputs.
454
+ * The producer does not truncate custom structured-context labels, so the
455
+ * label segment is newline-bound rather than length-bound. The expression uses
456
+ * only linear character classes; avoid nested wildcards here.
441
457
  */
442
- const INBOUND_META_LABEL_RE = /^[^\n]{1,100}\((?:untrusted metadata|untrusted, for context|untrusted, nearest first|untrusted, chronological,[^\n)]{1,80})\):[ \t]*$/m;
443
- const INBOUND_META_LABEL_JSON_BLOCK_RE = /^[^\n]{1,100}\((?:untrusted metadata|untrusted, for context|untrusted, nearest first|untrusted, chronological,[^\n)]{1,80})\):[ \t]*\n[ \t]*```json[ \t]*\n[\s\S]*?\n[ \t]*```[ \t]*\n?/gm;
458
+ const INBOUND_META_LABEL_RE = /^[^\n]+\((?:untrusted metadata|untrusted, for context|untrusted, nearest first|untrusted, chronological,[^\n)]{1,80})\):[ \t]*$/m;
459
+ const INBOUND_META_LABEL_JSON_BLOCK_RE = /^[^\n]+\((?:untrusted metadata|untrusted, for context|untrusted, nearest first|untrusted, chronological,[^\n)]{1,80})\):[ \t]*\n[ \t]*```json[ \t]*\n[\s\S]*?\n[ \t]*```[ \t]*\n?/gm;
444
460
  const LEADING_CHRONOLOGICAL_CONTEXT_LABEL_RE = /^\s*[^\n]{1,100}\(untrusted, chronological,[^\n)]{1,80}\):[ \t]*(?:\n|$)/;
445
- const BRACKETED_LINE_PREFIX_RE = /^\[[^\]\n]{1,500}\]\s/gm;
446
461
  const BRACKETED_PREFIX_RE = /\[[^\]\n]{1,500}\]\s/g;
447
462
  const LEADING_CURRENT_MESSAGE_CONTEXT_RE = /^\s*Current message:[ \t]*(?:\n|$)/;
463
+ const LEADING_CURRENT_MESSAGE_REPLY_LINE_RE = /^\s*\[Replying to:[^\n]{0,1000}\]\s*\n/;
464
+ const LEADING_CURRENT_MESSAGE_ID_SENDER_RE = /^#\d+\s+[^\n:]{1,100}:\s*/;
448
465
  const UNTRUSTED_CONTEXT_HEADER_RE = /^Untrusted context \(metadata/m;
449
466
  /**
450
467
  * Matches JSON blobs that look like OpenClaw transport envelope metadata.
@@ -484,12 +501,11 @@ const ENVELOPE_JSON_LINE_RE = /^\s*\{\s*(?:\n\s*)?"(?:chat_id|message_id|reply_t
484
501
  */
485
502
  const INBOUND_ENVELOPE_PREFIX_RE = /^\[([^\]\n]{0,300}?(?:\s\+(?:\d+[smhdwy]|just now)\b|\s[A-Za-z]{3}\s\d{4}-\d{2}-\d{2})[^\]\n]{0,200})\]\s/;
486
503
  /**
487
- * Marker-free leading envelope header, e.g. `[telegram alice] hello`. The
488
- * elapsed/date marker regex above misses this shape because `formatAgentEnvelope`
489
- * drops `+<elapsed>`, host, ip, and the absolute timestamp when their inputs are
490
- * absent. The minimum real shape is then `[<channel> <from>]` with no markers,
491
- * which is indistinguishable from arbitrary user `[label ...]` prose without a
492
- * channel-id anchor.
504
+ * Marker-free leading envelope header. The elapsed/date marker regex above
505
+ * misses envelopes where `formatAgentEnvelope` drops every optional marker.
506
+ * Because channel labels can also be ordinary words, callers only accept this
507
+ * match after `matchKnownChannelMarkerFreeEnvelopePrefix` finds a stronger
508
+ * group/thread or body-sender signal.
493
509
  *
494
510
  * Anchoring on a known bundled/official channel prefix from
495
511
  * `BUNDLED_CHAT_CHANNEL_ENVELOPE_PREFIXES` keeps the detector and formatter in
@@ -523,25 +539,35 @@ const INBOUND_ENVELOPE_KNOWN_CHANNEL_PREFIX_RE = ENVELOPE_KNOWN_CHANNEL_PATTERN
523
539
  * regex bounded and matches realistic display names).
524
540
  */
525
541
  const ENVELOPE_BODY_SENDER_PREFIX_RE = /^([^\n:]{1,120}):\s/;
542
+ const ENVELOPE_BODY_DIRECT_PREFIX = "(sender)";
526
543
  const ENVELOPE_BODY_SELF_PREFIX = "(self)";
527
544
  const SENDER_PREFIXED_ENVELOPE_CHANNEL_RE = /^(?:discord|imessage|line|mattermost|qqbot|signal|slack|telegram|whatsapp)(?:\s|$)/i;
528
545
  const NON_DIRECT_ENVELOPE_HEADER_RE = /(?:^|\s)(?:#[^\s]+|group:[^\s]+|group\s+id:[^\s]+|room:[^\s]+|channel\s+id:[^\s]+|id:-[^\s]+|unknown-group|[^\s]+@g\.us)(?:\s|$)/i;
529
546
  const USER_AUTHORED_BODY_LABEL_RE = /^(?:action|decision|fixme|note|question|reminder|todo)$/i;
547
+ function matchKnownChannelMarkerFreeEnvelopePrefix(text, options) {
548
+ const match = INBOUND_ENVELOPE_KNOWN_CHANNEL_PREFIX_RE?.exec(text);
549
+ if (!match) return null;
550
+ const headerInside = match[1] ?? "";
551
+ if (NON_DIRECT_ENVELOPE_HEADER_RE.test(headerInside)) return match;
552
+ const body = text.slice(match[0].length);
553
+ if (stripEnvelopeBodySenderPrefix(body, headerInside) !== body) return match;
554
+ return options?.allowAmbiguousDirect ? match : null;
555
+ }
530
556
  /**
531
557
  * Returns true if `text` looks like it contains OpenClaw-injected envelope or
532
558
  * transport metadata that should never be persisted as a long-term memory.
533
559
  */
534
560
  function looksLikeEnvelopeSludge(text) {
535
561
  if (!text) return false;
536
- if (INBOUND_META_LABEL_RE.test(text)) return true;
562
+ if (INBOUND_META_SENTINEL_LINE_RE.test(text) || INBOUND_META_LABEL_RE.test(text)) return true;
537
563
  if (UNTRUSTED_CONTEXT_HEADER_RE.test(text)) return true;
538
564
  if (MESSAGE_TOOL_DELIVERY_HINT_RE.test(text)) return true;
539
- if (text.includes(HISTORY_CONTEXT_MARKER) || text.includes(CURRENT_MESSAGE_MARKER)) return true;
565
+ if (HISTORY_CONTEXT_MARKERS.some((marker) => text.includes(marker)) || CURRENT_MESSAGE_MARKERS.some((marker) => text.includes(marker))) return true;
540
566
  if (ACTIVE_TURN_RECOVERY_RE.test(text)) return true;
541
567
  if (MEDIA_ATTACHED_PATTERN_TEST.test(text)) return true;
542
568
  if (ENVELOPE_JSON_LINE_RE.test(text)) return true;
543
569
  if (INBOUND_ENVELOPE_PREFIX_RE.test(text)) return true;
544
- if (INBOUND_ENVELOPE_KNOWN_CHANNEL_PREFIX_RE?.test(text)) return true;
570
+ if (matchKnownChannelMarkerFreeEnvelopePrefix(text)) return true;
545
571
  return false;
546
572
  }
547
573
  /**
@@ -567,9 +593,9 @@ function stripEnvelopeBodySenderPrefix(body, headerInside) {
567
593
  const match = body.match(ENVELOPE_BODY_SENDER_PREFIX_RE);
568
594
  if (!match) return body;
569
595
  const label = match[1];
570
- if (label === ENVELOPE_BODY_SELF_PREFIX) return body.slice(match[0].length);
596
+ if (label === ENVELOPE_BODY_SELF_PREFIX || label === ENVELOPE_BODY_DIRECT_PREFIX) return body.slice(match[0].length);
571
597
  if (SENDER_PREFIXED_ENVELOPE_CHANNEL_RE.test(headerInside) && NON_DIRECT_ENVELOPE_HEADER_RE.test(headerInside) && !USER_AUTHORED_BODY_LABEL_RE.test(label)) return body.slice(match[0].length);
572
- if (headerInside.split(/\s+/).includes(label)) return body.slice(match[0].length);
598
+ if (headerInside.split(/\s+/).includes(label) || headerInside.includes(label)) return body.slice(match[0].length);
573
599
  return body;
574
600
  }
575
601
  function stripLeadingMessageToolDeliveryHints(text) {
@@ -596,27 +622,50 @@ function findFirstInboundEnvelopeIndex(text, options) {
596
622
  if (text.slice(lineStart, index).includes("[Replying to:")) continue;
597
623
  }
598
624
  const candidate = text.slice(index);
599
- if (INBOUND_ENVELOPE_PREFIX_RE.test(candidate) || INBOUND_ENVELOPE_KNOWN_CHANNEL_PREFIX_RE?.test(candidate)) return index;
625
+ if (INBOUND_ENVELOPE_PREFIX_RE.test(candidate) || matchKnownChannelMarkerFreeEnvelopePrefix(candidate, { allowAmbiguousDirect: options?.allowAmbiguousMarkerFree })) return index;
600
626
  }
601
627
  return -1;
602
628
  }
603
629
  function stripPendingHistoryContextBeforeCurrentMessage(text) {
604
630
  const candidateText = text.trimStart();
605
- if (!candidateText.startsWith(HISTORY_CONTEXT_MARKER)) return text;
606
- const currentMessageIndex = candidateText.lastIndexOf(CURRENT_MESSAGE_MARKER);
607
- if (currentMessageIndex === -1) return text;
608
- return candidateText.slice(currentMessageIndex + 35);
631
+ if (!HISTORY_CONTEXT_MARKERS.some((marker) => candidateText.startsWith(marker))) return text;
632
+ const currentMarker = findLastContextMarker(candidateText, CURRENT_MESSAGE_MARKERS);
633
+ if (!currentMarker) return text;
634
+ return candidateText.slice(currentMarker.index + currentMarker.marker.length);
609
635
  }
610
636
  function stripToCurrentMessageMarker(text) {
611
- const currentMessageIndex = text.lastIndexOf(CURRENT_MESSAGE_MARKER);
612
- if (currentMessageIndex === -1) return null;
613
- return text.slice(currentMessageIndex + 35);
637
+ const currentMarker = findLastContextMarker(text, CURRENT_MESSAGE_MARKERS);
638
+ if (!currentMarker) return null;
639
+ return text.slice(currentMarker.index + currentMarker.marker.length);
640
+ }
641
+ function findLastContextMarker(text, markers) {
642
+ let result = null;
643
+ for (const marker of markers) {
644
+ const index = text.lastIndexOf(marker);
645
+ if (index !== -1 && (!result || index > result.index)) result = {
646
+ index,
647
+ marker
648
+ };
649
+ }
650
+ return result;
614
651
  }
615
652
  function stripLeadingCurrentMessageContextBeforeEnvelope(text) {
616
653
  const candidateText = text.trimStart();
617
654
  if (!LEADING_CURRENT_MESSAGE_CONTEXT_RE.test(candidateText)) return text;
618
- const envelopeIndex = findFirstInboundEnvelopeIndex(candidateText, { skipReplyQuoteLine: true });
619
- if (envelopeIndex === -1) return text;
655
+ const envelopeIndex = findFirstInboundEnvelopeIndex(candidateText, {
656
+ allowAmbiguousMarkerFree: true,
657
+ skipReplyQuoteLine: true
658
+ });
659
+ if (envelopeIndex === -1) {
660
+ let plainBody = candidateText.replace(LEADING_CURRENT_MESSAGE_CONTEXT_RE, "").trimStart();
661
+ for (let pass = 0; pass < 4; pass += 1) {
662
+ const replyLineMatch = plainBody.match(LEADING_CURRENT_MESSAGE_REPLY_LINE_RE);
663
+ if (!replyLineMatch) break;
664
+ plainBody = plainBody.slice(replyLineMatch[0].length).trimStart();
665
+ }
666
+ const currentMessagePrefixMatch = plainBody.match(LEADING_CURRENT_MESSAGE_ID_SENDER_RE);
667
+ return currentMessagePrefixMatch ? plainBody.slice(currentMessagePrefixMatch[0].length) : text;
668
+ }
620
669
  return candidateText.slice(envelopeIndex);
621
670
  }
622
671
  function stripLeadingPlainTextMetadataBody(text) {
@@ -626,21 +675,15 @@ function stripLeadingPlainTextMetadataBody(text) {
626
675
  const currentMessageBody = stripLeadingCurrentMessageContextBeforeEnvelope(candidateText);
627
676
  return currentMessageBody === candidateText ? "" : currentMessageBody;
628
677
  }
629
- function stripLeadingInboundEnvelope(text) {
630
- const candidateText = stripLeadingCurrentMessageContextBeforeEnvelope(stripPendingHistoryContextBeforeCurrentMessage(stripLeadingMessageToolDeliveryHints(text))).trimStart();
631
- const envelopePrefixMatch = candidateText.match(INBOUND_ENVELOPE_PREFIX_RE) ?? (INBOUND_ENVELOPE_KNOWN_CHANNEL_PREFIX_RE ? candidateText.match(INBOUND_ENVELOPE_KNOWN_CHANNEL_PREFIX_RE) : null);
632
- if (!envelopePrefixMatch) return text;
678
+ function stripLeadingInboundEnvelope(text, options) {
679
+ const strippedCandidate = stripLeadingCurrentMessageContextBeforeEnvelope(stripPendingHistoryContextBeforeCurrentMessage(stripLeadingMessageToolDeliveryHints(text)));
680
+ const candidateText = strippedCandidate.trimStart();
681
+ const allowAmbiguousMarkerFree = options?.allowAmbiguousMarkerFree || strippedCandidate !== text;
682
+ const envelopePrefixMatch = candidateText.match(INBOUND_ENVELOPE_PREFIX_RE) ?? matchKnownChannelMarkerFreeEnvelopePrefix(candidateText, { allowAmbiguousDirect: allowAmbiguousMarkerFree });
683
+ if (!envelopePrefixMatch) return strippedCandidate === text ? text : candidateText;
633
684
  const headerInside = envelopePrefixMatch[1] ?? "";
634
685
  return stripEnvelopeBodySenderPrefix(candidateText.slice(envelopePrefixMatch[0].length), headerInside);
635
686
  }
636
- function findFirstInboundEnvelopeLineIndex(text) {
637
- for (const match of text.matchAll(BRACKETED_LINE_PREFIX_RE)) {
638
- const index = match.index;
639
- const candidate = text.slice(index);
640
- if (INBOUND_ENVELOPE_PREFIX_RE.test(candidate) || INBOUND_ENVELOPE_KNOWN_CHANNEL_PREFIX_RE?.test(candidate)) return index;
641
- }
642
- return -1;
643
- }
644
687
  function stripLeadingChronologicalContextBlocks(text) {
645
688
  let cleaned = text;
646
689
  let remainingPasses = INBOUND_META_SENTINELS.length;
@@ -649,8 +692,27 @@ function stripLeadingChronologicalContextBlocks(text) {
649
692
  const match = cleaned.match(LEADING_CHRONOLOGICAL_CONTEXT_LABEL_RE);
650
693
  if (!match) return cleaned;
651
694
  const afterLabel = cleaned.slice(match[0].length);
652
- const envelopeIndex = findFirstInboundEnvelopeLineIndex(afterLabel);
653
- cleaned = envelopeIndex === -1 ? "" : afterLabel.slice(envelopeIndex);
695
+ const bodyStart = afterLabel.search(/\S/);
696
+ if (bodyStart === -1) return "";
697
+ const bodyLineEnd = afterLabel.indexOf("\n", bodyStart);
698
+ const firstBodyLine = bodyLineEnd === -1 ? afterLabel.slice(bodyStart) : afterLabel.slice(bodyStart, bodyLineEnd);
699
+ let lineEnvelopeIndex = firstBodyLine.trimStart().startsWith("[") ? findFirstInboundEnvelopeIndex(firstBodyLine, {
700
+ allowAmbiguousMarkerFree: true,
701
+ skipReplyQuoteLine: true
702
+ }) : -1;
703
+ if (lineEnvelopeIndex === -1 && match[0].includes("selected for current message")) {
704
+ const inlineEnvelopeIndex = findFirstInboundEnvelopeIndex(firstBodyLine, {
705
+ allowAmbiguousMarkerFree: true,
706
+ skipReplyQuoteLine: true
707
+ });
708
+ const prefix = inlineEnvelopeIndex === -1 ? "" : firstBodyLine.slice(0, inlineEnvelopeIndex);
709
+ lineEnvelopeIndex = /^#\d+\s/.test(prefix.trimStart()) ? inlineEnvelopeIndex : -1;
710
+ }
711
+ const envelopeIndex = lineEnvelopeIndex === -1 ? -1 : bodyStart + lineEnvelopeIndex;
712
+ if (envelopeIndex === -1) {
713
+ const separatorMatch = /\n[ \t]*\n/.exec(afterLabel);
714
+ cleaned = separatorMatch ? afterLabel.slice(separatorMatch.index + separatorMatch[0].length) : "";
715
+ } else cleaned = afterLabel.slice(envelopeIndex);
654
716
  if (!cleaned) return "";
655
717
  }
656
718
  return cleaned;
@@ -664,14 +726,24 @@ function sanitizeForMemoryCapture(text) {
664
726
  if (!text) return "";
665
727
  const MAX_SANITIZE_CHARS = 1e4;
666
728
  let cleaned = text.length > MAX_SANITIZE_CHARS ? text.slice(0, MAX_SANITIZE_CHARS) : text;
729
+ let strippedInjectedContext = false;
667
730
  cleaned = cleaned.replace(LEADING_TIMESTAMP_PREFIX_RE, "");
668
- cleaned = cleaned.replace(INBOUND_META_LABEL_JSON_BLOCK_RE, "");
731
+ const afterDeliveryHints = stripLeadingMessageToolDeliveryHints(cleaned);
732
+ strippedInjectedContext ||= afterDeliveryHints !== cleaned;
733
+ cleaned = afterDeliveryHints;
734
+ const afterJsonMetaBlocks = cleaned.replace(INBOUND_META_LABEL_JSON_BLOCK_RE, "");
735
+ strippedInjectedContext ||= afterJsonMetaBlocks !== cleaned;
736
+ cleaned = afterJsonMetaBlocks;
669
737
  for (const sentinel of INBOUND_META_SENTINELS) {
670
738
  const escapedSentinel = sentinel.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
671
739
  const blockRe = new RegExp(`${escapedSentinel}\\s*\\n\\s*\`\`\`json\\s*\\n[\\s\\S]*?\\n\\s*\`\`\`\\s*\\n?`, "g");
672
- cleaned = cleaned.replace(blockRe, "");
740
+ const afterSentinelBlock = cleaned.replace(blockRe, "");
741
+ strippedInjectedContext ||= afterSentinelBlock !== cleaned;
742
+ cleaned = afterSentinelBlock;
673
743
  }
674
- cleaned = stripLeadingChronologicalContextBlocks(cleaned);
744
+ const afterChronologicalContext = stripLeadingChronologicalContextBlocks(cleaned);
745
+ strippedInjectedContext ||= afterChronologicalContext !== cleaned;
746
+ cleaned = afterChronologicalContext;
675
747
  for (let pass = 0; pass < INBOUND_META_SENTINELS.length + 1; pass += 1) {
676
748
  let earliestMetaIndex = -1;
677
749
  let earliestMetaRe = null;
@@ -699,15 +771,25 @@ function sanitizeForMemoryCapture(text) {
699
771
  const lineEnd = cleaned.indexOf("\n");
700
772
  const afterHeader = lineEnd === -1 ? "" : cleaned.slice(lineEnd + 1);
701
773
  if (!afterHeader.trimStart().startsWith("```json")) {
702
- cleaned = stripLeadingPlainTextMetadataBody(afterHeader);
774
+ const afterPlainTextMetadata = stripLeadingPlainTextMetadataBody(afterHeader);
775
+ strippedInjectedContext ||= afterPlainTextMetadata !== cleaned;
776
+ cleaned = afterPlainTextMetadata;
703
777
  continue;
704
778
  }
705
779
  }
706
- cleaned = cleaned.replace(earliestMetaRe, "");
780
+ const afterMetaHeader = cleaned.replace(earliestMetaRe, "");
781
+ strippedInjectedContext ||= afterMetaHeader !== cleaned;
782
+ cleaned = afterMetaHeader;
707
783
  }
784
+ const afterActiveMemoryContext = cleaned.replace(/^Untrusted context \(metadata[^\n]*\n<active_memory_plugin>[\s\S]*?<\/active_memory_plugin>\s*/gm, "");
785
+ strippedInjectedContext ||= afterActiveMemoryContext !== cleaned;
786
+ cleaned = afterActiveMemoryContext;
708
787
  const untrustedLineMatch = /^Untrusted context \(metadata/m.exec(cleaned);
709
- if (untrustedLineMatch) cleaned = cleaned.slice(0, untrustedLineMatch.index);
710
- cleaned = stripLeadingInboundEnvelope(cleaned);
788
+ if (untrustedLineMatch) {
789
+ strippedInjectedContext = true;
790
+ cleaned = cleaned.slice(0, untrustedLineMatch.index);
791
+ }
792
+ cleaned = stripLeadingInboundEnvelope(cleaned, { allowAmbiguousMarkerFree: strippedInjectedContext });
711
793
  cleaned = cleaned.replace(MEDIA_ATTACHED_PATTERN, "");
712
794
  cleaned = cleaned.replace(/<active_memory_plugin>[\s\S]*?<\/active_memory_plugin>/g, "");
713
795
  cleaned = cleaned.replace(/\n{3,}/g, "\n\n").replace(/[ \t]{2,}/g, " ").trim();
@@ -926,16 +1008,16 @@ var memory_lancedb_default = definePluginEntry({
926
1008
  }
927
1009
  };
928
1010
  const vector = await embeddings.embed(text);
929
- const existing = await db.search(vector, 1, .95);
930
- if (existing.length > 0) return {
1011
+ const existing = await findCleanDuplicateMemory(db, vector);
1012
+ if (existing) return {
931
1013
  content: [{
932
1014
  type: "text",
933
- text: `Similar memory already exists: "${existing[0].entry.text}"`
1015
+ text: `Similar memory already exists: "${existing.entry.text}"`
934
1016
  }],
935
1017
  details: {
936
1018
  action: "duplicate",
937
- existingId: existing[0].entry.id,
938
- existingText: existing[0].entry.text
1019
+ existingId: existing.entry.id,
1020
+ existingText: existing.entry.text
939
1021
  }
940
1022
  };
941
1023
  const entry = await db.store({
@@ -1154,7 +1236,7 @@ var memory_lancedb_default = definePluginEntry({
1154
1236
  if (capturableSeen > 3) continue;
1155
1237
  const category = detectCategory(sanitized);
1156
1238
  const vector = await embeddings.embed(sanitized);
1157
- if ((await db.search(vector, 1, .95)).length > 0) continue;
1239
+ if (await findCleanDuplicateMemory(db, vector)) continue;
1158
1240
  await db.store({
1159
1241
  text: sanitized,
1160
1242
  vector,
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@openclaw/memory-lancedb",
3
- "version": "2026.5.31-beta.4",
3
+ "version": "2026.6.1-beta.1",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@openclaw/memory-lancedb",
9
- "version": "2026.5.31-beta.4",
9
+ "version": "2026.6.1-beta.1",
10
10
  "dependencies": {
11
11
  "@lancedb/lancedb": "0.30.0",
12
12
  "apache-arrow": "21.1.0",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openclaw/memory-lancedb",
3
- "version": "2026.5.31-beta.4",
3
+ "version": "2026.6.1-beta.1",
4
4
  "description": "OpenClaw LanceDB-backed long-term memory plugin with auto-recall, auto-capture, and vector search.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -23,13 +23,13 @@
23
23
  "install": {
24
24
  "npmSpec": "@openclaw/memory-lancedb",
25
25
  "defaultChoice": "npm",
26
- "minHostVersion": ">=2026.4.10"
26
+ "minHostVersion": ">=2026.5.31"
27
27
  },
28
28
  "compat": {
29
- "pluginApi": ">=2026.5.31-beta.4"
29
+ "pluginApi": ">=2026.6.1-beta.1"
30
30
  },
31
31
  "build": {
32
- "openclawVersion": "2026.5.31-beta.4"
32
+ "openclawVersion": "2026.6.1-beta.1"
33
33
  },
34
34
  "release": {
35
35
  "bundleRuntimeDependencies": false,
@@ -47,7 +47,7 @@
47
47
  "README.md"
48
48
  ],
49
49
  "peerDependencies": {
50
- "openclaw": ">=2026.5.31-beta.4"
50
+ "openclaw": ">=2026.6.1-beta.1"
51
51
  },
52
52
  "peerDependenciesMeta": {
53
53
  "openclaw": {