@remnic/core 9.69.53 → 9.69.54

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.
@@ -112,6 +112,7 @@ import {
112
112
  normalizeEntitySchemas
113
113
  } from "./chunk-ZIKENA2C.js";
114
114
  import {
115
+ normalizeRecallTokenSet,
115
116
  normalizeRecallTokens
116
117
  } from "./chunk-YCVB7X56.js";
117
118
  import {
@@ -148,12 +149,15 @@ import {
148
149
  readEnvVar,
149
150
  resolveHomeDir
150
151
  } from "./chunk-P2QRMMVQ.js";
152
+ import {
153
+ ALL_CATEGORY_DIRS
154
+ } from "./chunk-VS2IYZRU.js";
151
155
  import {
152
156
  log
153
157
  } from "./chunk-3WCASNQZ.js";
154
158
 
155
159
  // src/config.ts
156
- import path from "path";
160
+ import path2 from "path";
157
161
 
158
162
  // src/procedural/maintenance-config.ts
159
163
  function parseProceduralMaintenanceConfig(raw) {
@@ -335,6 +339,415 @@ function parseSessionExperienceConfig(raw) {
335
339
  };
336
340
  }
337
341
 
342
+ // src/dedup/index.ts
343
+ import crypto from "crypto";
344
+ import fs from "fs";
345
+ import path from "path";
346
+
347
+ // src/dedup/merge-provenance.ts
348
+ var MERGE_DERIVED_VIA = "merge";
349
+ var ISO_TIMESTAMP = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?(?:Z|[+-]\d{2}:\d{2})$/;
350
+ function assertIsoInstant(value, field) {
351
+ if (typeof value !== "string" || !ISO_TIMESTAMP.test(value)) {
352
+ throw new RangeError(`${field} must be a full ISO 8601 instant, got ${JSON.stringify(value)}`);
353
+ }
354
+ const ms2 = Date.parse(value);
355
+ if (Number.isNaN(ms2)) {
356
+ throw new RangeError(`${field} must be a full ISO 8601 instant, got ${JSON.stringify(value)}`);
357
+ }
358
+ if (!new Date(ms2).toISOString().startsWith(value.slice(0, 10))) {
359
+ throw new RangeError(`${field} is not a real calendar date: ${JSON.stringify(value)}`);
360
+ }
361
+ return value;
362
+ }
363
+ function assertOffset(value, field) {
364
+ if (value === void 0) return;
365
+ if (typeof value !== "number" || !Number.isInteger(value) || value < 0) {
366
+ throw new RangeError(`${field} must be a non-negative integer when present, got ${String(value)}`);
367
+ }
368
+ }
369
+ function normalizeSource(source, index) {
370
+ const label = `sources[${index}]`;
371
+ for (const field of ["sessionKey", "quote"]) {
372
+ const value = source[field];
373
+ if (typeof value !== "string" || value.trim() === "") {
374
+ throw new RangeError(`${label}.${field} must be a non-blank string`);
375
+ }
376
+ }
377
+ assertIsoInstant(source.observedAt, `${label}.observedAt`);
378
+ if (source.turnId !== void 0 && (typeof source.turnId !== "string" || source.turnId.trim() === "")) {
379
+ throw new RangeError(`${label}.turnId must be a non-blank string when present`);
380
+ }
381
+ assertOffset(source.charStart, `${label}.charStart`);
382
+ assertOffset(source.charEnd, `${label}.charEnd`);
383
+ if (source.charStart !== void 0 && source.charEnd !== void 0 && source.charEnd < source.charStart) {
384
+ throw new RangeError(`${label}.charEnd must be >= charStart`);
385
+ }
386
+ const copy = {
387
+ sessionKey: source.sessionKey,
388
+ observedAt: source.observedAt,
389
+ quote: source.quote
390
+ };
391
+ if (source.turnId !== void 0) copy.turnId = source.turnId;
392
+ if (source.charStart !== void 0) copy.charStart = source.charStart;
393
+ if (source.charEnd !== void 0) copy.charEnd = source.charEnd;
394
+ return copy;
395
+ }
396
+ function sourceKey(source) {
397
+ return JSON.stringify([
398
+ source.sessionKey,
399
+ source.turnId ?? null,
400
+ source.observedAt,
401
+ source.quote,
402
+ source.charStart ?? null,
403
+ source.charEnd ?? null
404
+ ]);
405
+ }
406
+ function buildMergeFrontmatterUpdate(input) {
407
+ const updated = assertIsoInstant(input.nowIso, "nowIso");
408
+ const previous = input.targetReinforcementCount;
409
+ if (previous !== void 0 && (typeof previous !== "number" || !Number.isInteger(previous) || previous < 0)) {
410
+ throw new RangeError(
411
+ `reinforcement_count must be a non-negative integer when supplied, got ${String(previous)}`
412
+ );
413
+ }
414
+ const seen = /* @__PURE__ */ new Set();
415
+ const sources = [];
416
+ const append = (source, index) => {
417
+ const copy = normalizeSource(source, index);
418
+ const key = sourceKey(copy);
419
+ if (seen.has(key)) return;
420
+ seen.add(key);
421
+ sources.push(copy);
422
+ };
423
+ const target = input.targetSources ?? [];
424
+ target.forEach(append);
425
+ input.incomingSources.forEach((source, i) => append(source, target.length + i));
426
+ return {
427
+ updated,
428
+ derived_via: MERGE_DERIVED_VIA,
429
+ reinforcement_count: (previous ?? 0) + 1,
430
+ sources
431
+ };
432
+ }
433
+
434
+ // src/dedup/merge-content.ts
435
+ var MERGE_CONTENT_LENGTH_FACTOR = 4;
436
+ function requireString(value, field) {
437
+ if (typeof value !== "string") {
438
+ throw new RangeError(`merge ${field} must be a string`);
439
+ }
440
+ return value;
441
+ }
442
+ function checkMergedContent(input) {
443
+ const incoming = requireString(input.incomingContent, "incomingContent");
444
+ const target = requireString(input.targetContent, "targetContent");
445
+ const limit = MERGE_CONTENT_LENGTH_FACTOR * (incoming.length + target.length);
446
+ const merged = input.mergedContent;
447
+ if (typeof merged !== "string" || merged.trim() === "") {
448
+ return { ok: false, reason: "empty", limit };
449
+ }
450
+ if (!sanitizeMemoryContent(merged).clean) {
451
+ return { ok: false, reason: "unsafe", limit };
452
+ }
453
+ if (merged.length > limit) {
454
+ return { ok: false, reason: "oversized", limit };
455
+ }
456
+ return { ok: true, content: merged };
457
+ }
458
+
459
+ // src/dedup/index.ts
460
+ var DEFAULT_DEDUP_THRESHOLD = 0.85;
461
+ var DEFAULT_MAX_LOAD = 1e4;
462
+ function normalizeThreshold(value, defaultValue) {
463
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 && value <= 1 ? value : defaultValue;
464
+ }
465
+ function normalizeMaxLoad(value, defaultValue) {
466
+ return typeof value === "number" && Number.isInteger(value) && value >= 0 ? value : defaultValue;
467
+ }
468
+ function findDuplicates(options) {
469
+ const startTime = Date.now();
470
+ const { memoryDir } = options;
471
+ const threshold = normalizeThreshold(options.threshold, DEFAULT_DEDUP_THRESHOLD);
472
+ const maxLoad = normalizeMaxLoad(options.maxLoad, DEFAULT_MAX_LOAD);
473
+ const memories = loadMemories(memoryDir, options.categories, maxLoad);
474
+ const duplicates = [];
475
+ for (let i = 0; i < memories.length; i++) {
476
+ for (let j = i + 1; j < memories.length; j++) {
477
+ const sim = computeSimilarity(memories[i].content, memories[j].content);
478
+ if (sim >= threshold) {
479
+ duplicates.push({
480
+ left: memories[i],
481
+ right: memories[j],
482
+ similarity: sim,
483
+ action: sim >= 0.98 ? "merge" : sim >= 0.9 ? "keep_right" : "keep_left"
484
+ });
485
+ }
486
+ }
487
+ }
488
+ return {
489
+ scanned: memories.length,
490
+ duplicates,
491
+ durationMs: Date.now() - startTime
492
+ };
493
+ }
494
+ function findContradictions(options) {
495
+ const startTime = Date.now();
496
+ const { memoryDir } = options;
497
+ const maxLoad = normalizeMaxLoad(options.maxLoad, DEFAULT_MAX_LOAD);
498
+ const memories = loadMemories(memoryDir, options.categories, maxLoad);
499
+ const contradictions = [];
500
+ for (let i = 0; i < memories.length; i++) {
501
+ for (let j = i + 1; j < memories.length; j++) {
502
+ const contra = detectContradiction(memories[i], memories[j]);
503
+ if (contra) {
504
+ contradictions.push(contra);
505
+ }
506
+ }
507
+ }
508
+ return {
509
+ scanned: memories.length,
510
+ contradictions,
511
+ durationMs: Date.now() - startTime
512
+ };
513
+ }
514
+ function computeSimilarity(a, b) {
515
+ const normA = normalize(a);
516
+ const normB = normalize(b);
517
+ const punctuationBoundariesA = embeddedPunctuationSignature(a);
518
+ const punctuationBoundariesB = embeddedPunctuationSignature(b);
519
+ if (punctuationBoundariesA !== punctuationBoundariesB) return 0;
520
+ if (normA === normB) return 1;
521
+ if (hashContent(normA) === hashContent(normB)) return 0.99;
522
+ if (normA.length > 50 && normB.length > 50) {
523
+ if (normA.includes(normB.slice(0, 40)) || normB.includes(normA.slice(0, 40))) {
524
+ return 0.9;
525
+ }
526
+ }
527
+ const wordsA = tokenizeForSimilarity(normA);
528
+ const wordsB = tokenizeForSimilarity(normB);
529
+ const intersection = new Set([...wordsA].filter((w) => wordsB.has(w)));
530
+ const union = /* @__PURE__ */ new Set([...wordsA, ...wordsB]);
531
+ if (union.size === 0) return 0;
532
+ return intersection.size / union.size;
533
+ }
534
+ function embeddedPunctuationSignature(text) {
535
+ const chars = [...text.normalize("NFC")];
536
+ const significant = /[\p{L}\p{M}\p{N}]/u;
537
+ const ignored = /[\s']/u;
538
+ const boundaries = [];
539
+ let position = 0;
540
+ for (let index = 0; index < chars.length; ) {
541
+ const char = chars[index];
542
+ if (char === void 0) break;
543
+ if (significant.test(char)) {
544
+ position++;
545
+ index++;
546
+ continue;
547
+ }
548
+ if (ignored.test(char)) {
549
+ index++;
550
+ continue;
551
+ }
552
+ const start = index;
553
+ while (index < chars.length) {
554
+ const next = chars[index];
555
+ if (next === void 0 || significant.test(next) || ignored.test(next)) break;
556
+ index++;
557
+ }
558
+ const before = chars[start - 1];
559
+ const after = chars[index];
560
+ if (before && after && significant.test(before) && significant.test(after)) {
561
+ boundaries.push(`${position}:${chars.slice(start, index).join("")}`);
562
+ }
563
+ }
564
+ return boundaries.join("\0");
565
+ }
566
+ function normalize(text) {
567
+ return text.normalize("NFC").toLowerCase().replace(/[^\p{ASCII}\p{L}\p{M}\p{N}\s']/gu, " ").replace(/[^\p{L}\p{M}\p{N}\s']/gu, "").replace(/\s+/g, " ").trim();
568
+ }
569
+ function tokenizeForSimilarity(text) {
570
+ if (/\p{Script=Hangul}/u.test(text)) {
571
+ const tokens = normalizeRecallTokenSet(text);
572
+ for (const segment of text.match(/\p{Script=Hangul}+/gu) ?? []) {
573
+ const chars = [...segment];
574
+ for (const size of [1, 2, 3, 4]) {
575
+ for (let index = 0; index <= chars.length - size; index += 1) {
576
+ tokens.add(chars.slice(index, index + size).join(""));
577
+ }
578
+ }
579
+ }
580
+ return tokens;
581
+ }
582
+ if (/[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}]/u.test(text)) {
583
+ return normalizeRecallTokenSet(text);
584
+ }
585
+ return text.length === 0 ? /* @__PURE__ */ new Set() : new Set(text.split(/\s+/));
586
+ }
587
+ var NEGATION_WORDS = /* @__PURE__ */ new Set([
588
+ "not",
589
+ "don't",
590
+ "doesn't",
591
+ "isn't",
592
+ "aren't",
593
+ "won't",
594
+ "can't",
595
+ "cannot",
596
+ "never",
597
+ "no",
598
+ "none",
599
+ "neither",
600
+ "nor",
601
+ "nothing",
602
+ "nowhere"
603
+ ]);
604
+ function detectContradiction(a, b) {
605
+ const normA = normalize(a.content);
606
+ const normB = normalize(b.content);
607
+ const aHasNegation = containsNegation(normA);
608
+ const bHasNegation = containsNegation(normB);
609
+ if (aHasNegation === bHasNegation) return null;
610
+ const strippedA = stripNegation(normA);
611
+ const strippedB = stripNegation(normB);
612
+ const sim = computeSimilarity(strippedA, strippedB);
613
+ if (sim < 0.7) return null;
614
+ const oppQuantifiers = [
615
+ ["always", "never"],
616
+ ["all", "none"],
617
+ ["every", "no"],
618
+ ["must", "must not"],
619
+ ["should", "should not"],
620
+ ["can", "cannot"]
621
+ ];
622
+ for (const [pos, neg] of oppQuantifiers) {
623
+ if (a.content.toLowerCase().includes(pos) && b.content.toLowerCase().includes(neg) || a.content.toLowerCase().includes(neg) && b.content.toLowerCase().includes(pos)) {
624
+ return {
625
+ left: a,
626
+ right: b,
627
+ severity: "high",
628
+ reason: `Opposite quantifiers: "${pos}" vs "${neg}"`
629
+ };
630
+ }
631
+ }
632
+ return {
633
+ left: a,
634
+ right: b,
635
+ severity: sim >= 0.85 ? "high" : "medium",
636
+ reason: "Negated version of similar content"
637
+ };
638
+ }
639
+ function containsNegation(text) {
640
+ const words = text.split(/\s+/);
641
+ return words.some((w) => NEGATION_WORDS.has(w));
642
+ }
643
+ function escapeRegExp(value) {
644
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
645
+ }
646
+ var stripNegationPattern = null;
647
+ var stripNegationPatternSource = null;
648
+ function stripNegationRegex() {
649
+ const alternation = [...NEGATION_WORDS].sort((a, b) => b.length - a.length).map(escapeRegExp).join("|");
650
+ if (stripNegationPattern === null || stripNegationPatternSource !== alternation) {
651
+ stripNegationPattern = new RegExp(`\\b(?:${alternation})\\b`, "gi");
652
+ stripNegationPatternSource = alternation;
653
+ }
654
+ return stripNegationPattern;
655
+ }
656
+ function stripNegation(text) {
657
+ return text.replace(stripNegationRegex(), "").replace(/\s+/g, " ").trim();
658
+ }
659
+ function loadMemories(memoryDir, categories, maxLoad = 1e4) {
660
+ const result = [];
661
+ const allCategories = categories ?? ALL_CATEGORY_DIRS;
662
+ if (!fs.existsSync(memoryDir)) return result;
663
+ const memoryRootReal = fs.realpathSync(memoryDir);
664
+ for (const category of allCategories) {
665
+ if (result.length >= maxLoad) break;
666
+ const dir = path.join(memoryDir, category);
667
+ if (!fs.existsSync(dir)) continue;
668
+ const categoryStat = fs.lstatSync(dir);
669
+ if (categoryStat.isSymbolicLink()) {
670
+ throw new Error(`Refusing to scan symlinked memory category directory: ${dir}`);
671
+ }
672
+ if (!categoryStat.isDirectory()) continue;
673
+ const categoryRootReal = fs.realpathSync(dir);
674
+ assertPathInsideRoot(memoryRootReal, categoryRootReal, dir);
675
+ walkMdFiles(dir, memoryRootReal, categoryRootReal, (filePath) => {
676
+ if (result.length >= maxLoad) return;
677
+ const content = readFileSafe(filePath, memoryRootReal, categoryRootReal);
678
+ if (!content) return;
679
+ const fm = parseFrontmatter(content);
680
+ const body = extractBody(content);
681
+ if (!fm?.id || !body) return;
682
+ result.push({
683
+ id: fm.id,
684
+ content: body,
685
+ category: fm.category ?? category.slice(0, -1),
686
+ filePath
687
+ });
688
+ });
689
+ }
690
+ return result;
691
+ }
692
+ function hashContent(content) {
693
+ return crypto.createHash("sha256").update(content).digest("hex").slice(0, 16);
694
+ }
695
+ function readFileSafe(filePath, memoryRootReal, categoryRootReal) {
696
+ try {
697
+ const fileStat = fs.lstatSync(filePath);
698
+ if (fileStat.isSymbolicLink()) {
699
+ throw new Error(`Refusing to read symlinked memory file: ${filePath}`);
700
+ }
701
+ const fileReal = fs.realpathSync(filePath);
702
+ assertPathInsideRoot(memoryRootReal, fileReal, filePath);
703
+ assertPathInsideRoot(categoryRootReal, fileReal, filePath);
704
+ return fs.readFileSync(filePath, "utf8");
705
+ } catch {
706
+ return null;
707
+ }
708
+ }
709
+ function parseFrontmatter(content) {
710
+ const match = content.match(/^---\n([\s\S]*?)\n---/);
711
+ if (!match) return null;
712
+ const fm = {};
713
+ for (const line of match[1].split("\n")) {
714
+ const colonIdx = line.indexOf(":");
715
+ if (colonIdx === -1) continue;
716
+ const key = line.slice(0, colonIdx).trim();
717
+ const value = line.slice(colonIdx + 1).trim();
718
+ fm[key] = value;
719
+ }
720
+ return fm;
721
+ }
722
+ function extractBody(content) {
723
+ const match = content.match(/^---\n[\s\S]*?\n---\n([\s\S]*)/);
724
+ return match ? match[1].trim() : content.trim();
725
+ }
726
+ function assertPathInsideRoot(rootReal, targetReal, sourcePath) {
727
+ const rel = path.relative(rootReal, targetReal);
728
+ if (rel === "" || !rel.startsWith("..") && !path.isAbsolute(rel)) {
729
+ return;
730
+ }
731
+ throw new Error(`Refusing to scan memory path outside root: ${sourcePath}`);
732
+ }
733
+ function walkMdFiles(dir, memoryRootReal, categoryRootReal, callback) {
734
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
735
+ const fullPath = path.join(dir, entry.name);
736
+ const entryStat = fs.lstatSync(fullPath);
737
+ if (entryStat.isSymbolicLink()) {
738
+ throw new Error(`Refusing to scan symlinked memory path: ${fullPath}`);
739
+ }
740
+ const entryReal = fs.realpathSync(fullPath);
741
+ assertPathInsideRoot(memoryRootReal, entryReal, fullPath);
742
+ assertPathInsideRoot(categoryRootReal, entryReal, fullPath);
743
+ if (entryStat.isDirectory()) {
744
+ walkMdFiles(fullPath, memoryRootReal, categoryRootReal, callback);
745
+ } else if (entry.name.endsWith(".md")) {
746
+ callback(fullPath);
747
+ }
748
+ }
749
+ }
750
+
338
751
  // src/lifecycle/seed-graduation.ts
339
752
  var SEED_GRADUATION_DEFAULTS = {
340
753
  enabled: false,
@@ -418,14 +831,29 @@ function isIndependentProvenance(seed, evidence) {
418
831
  return evidence.frontmatter.source !== seed.frontmatter.source;
419
832
  }
420
833
  function coverageOf(seedTokens, evidence) {
834
+ return tokenCoverageOf(
835
+ seedTokens,
836
+ new Set(normalizeRecallTokens(stripAttributesSuffix(evidence.content)))
837
+ );
838
+ }
839
+ function tokenCoverageOf(seedTokens, evidenceTokens) {
421
840
  if (seedTokens.size === 0) return 0;
422
- const evidenceTokens = new Set(normalizeRecallTokens(stripAttributesSuffix(evidence.content)));
423
841
  let matches = 0;
424
842
  for (const token of seedTokens) {
425
843
  if (evidenceTokens.has(token)) matches += 1;
426
844
  }
427
845
  return matches / seedTokens.size;
428
846
  }
847
+ var NEGATION_WORD_SPLIT = /[^\p{L}\p{M}\p{N}']+/u;
848
+ function hasNegationWord(text) {
849
+ for (const word of text.toLowerCase().split(NEGATION_WORD_SPLIT)) {
850
+ if (NEGATION_WORDS.has(word)) return true;
851
+ }
852
+ return false;
853
+ }
854
+ function stripNegationWords(text) {
855
+ return text.toLowerCase().split(NEGATION_WORD_SPLIT).filter((word) => !NEGATION_WORDS.has(word)).join(" ");
856
+ }
429
857
  function evaluateSeedGraduation(seed, evidence, options = {}) {
430
858
  const config = options.config ?? SEED_GRADUATION_DEFAULTS;
431
859
  if (!config.enabled) {
@@ -433,22 +861,28 @@ function evaluateSeedGraduation(seed, evidence, options = {}) {
433
861
  decision: "hold",
434
862
  corroborationCount: 0,
435
863
  corroborating: [],
864
+ contradicting: [],
436
865
  echoSuppressedCount: 0,
437
866
  reasons: ["seed-graduation-disabled"]
438
867
  };
439
868
  }
440
- const seedTokens = new Set(normalizeRecallTokens(stripAttributesSuffix(seed.content)));
869
+ const seedText = stripAttributesSuffix(seed.content);
870
+ const seedTokens = new Set(normalizeRecallTokens(seedText));
441
871
  if (seedTokens.size < MIN_SEED_TOKENS) {
442
872
  return {
443
873
  decision: "hold",
444
874
  corroborationCount: 0,
445
875
  corroborating: [],
876
+ contradicting: [],
446
877
  echoSuppressedCount: 0,
447
878
  reasons: ["seed-too-short-for-deterministic-corroboration"]
448
879
  };
449
880
  }
881
+ const seedNegated = hasNegationWord(seedText);
882
+ const seedCoreTokens = new Set(normalizeRecallTokens(stripNegationWords(seedText)));
450
883
  const seedCreated = createdMs(seed);
451
884
  const corroborating = [];
885
+ const contradicting = [];
452
886
  const reasons = [];
453
887
  let echoSuppressedCount = 0;
454
888
  for (const candidate of evidence) {
@@ -457,6 +891,19 @@ function evaluateSeedGraduation(seed, evidence, options = {}) {
457
891
  if (!Number.isFinite(seedCreated) || !Number.isFinite(candidateCreated)) continue;
458
892
  if (candidateCreated <= seedCreated) continue;
459
893
  if (!isEvidenceStatus(candidate.frontmatter.status)) continue;
894
+ const candidateText = stripAttributesSuffix(candidate.content);
895
+ if (hasNegationWord(candidateText) !== seedNegated) {
896
+ if (seedCoreTokens.size >= MIN_SEED_TOKENS && isIndependentProvenance(seed, candidate)) {
897
+ const coreTokens = new Set(normalizeRecallTokens(stripNegationWords(candidateText)));
898
+ if (tokenCoverageOf(seedCoreTokens, coreTokens) >= SEED_CORROBORATION_COVERAGE) {
899
+ contradicting.push({
900
+ memoryId: candidate.frontmatter.id,
901
+ source: candidate.frontmatter.source
902
+ });
903
+ }
904
+ }
905
+ continue;
906
+ }
460
907
  if (coverageOf(seedTokens, candidate) < SEED_CORROBORATION_COVERAGE) continue;
461
908
  if (!isIndependentProvenance(seed, candidate)) {
462
909
  echoSuppressedCount += 1;
@@ -470,11 +917,23 @@ function evaluateSeedGraduation(seed, evidence, options = {}) {
470
917
  corroborating.push({ memoryId: candidate.frontmatter.id, source: candidate.frontmatter.source });
471
918
  }
472
919
  if (echoSuppressedCount > 0) reasons.push(`echo-suppressed:${echoSuppressedCount}`);
920
+ if (contradicting.length > 0) {
921
+ reasons.push(`contradiction-observed:${contradicting.length}`);
922
+ return {
923
+ decision: "hold",
924
+ corroborationCount: corroborating.length,
925
+ corroborating,
926
+ contradicting,
927
+ echoSuppressedCount,
928
+ reasons
929
+ };
930
+ }
473
931
  if (corroborating.length >= config.minCorroborations) {
474
932
  return {
475
933
  decision: "promote",
476
934
  corroborationCount: corroborating.length,
477
935
  corroborating,
936
+ contradicting,
478
937
  echoSuppressedCount,
479
938
  reasons
480
939
  };
@@ -484,6 +943,7 @@ function evaluateSeedGraduation(seed, evidence, options = {}) {
484
943
  decision: "hold",
485
944
  corroborationCount: corroborating.length,
486
945
  corroborating,
946
+ contradicting,
487
947
  echoSuppressedCount,
488
948
  reasons
489
949
  };
@@ -499,12 +959,13 @@ function graduationAttributes(evaluation) {
499
959
  async function runSeedGraduationPass(input) {
500
960
  const config = input.config ?? SEED_GRADUATION_DEFAULTS;
501
961
  if (!config.enabled) {
502
- return { evaluated: 0, promoted: 0, held: 0, echoSuppressed: 0, disabled: true };
962
+ return { evaluated: 0, promoted: 0, held: 0, contradictionHeld: 0, echoSuppressed: 0, disabled: true };
503
963
  }
504
964
  const corpus = excludeSupportPassportPrivateMemories(input.memories);
505
965
  const evidencePool = corpus.filter((memory) => isEvidenceStatus(memory.frontmatter.status));
506
966
  let promoted = 0;
507
967
  let held = 0;
968
+ let contradictionHeld = 0;
508
969
  let echoSuppressed = 0;
509
970
  let evaluated = 0;
510
971
  for (const seed of corpus) {
@@ -518,6 +979,7 @@ async function runSeedGraduationPass(input) {
518
979
  echoSuppressed += evaluation.echoSuppressedCount;
519
980
  if (evaluation.decision === "hold") {
520
981
  held += 1;
982
+ if (evaluation.contradicting.length > 0) contradictionHeld += 1;
521
983
  continue;
522
984
  }
523
985
  const wrote = await input.storage.promoteWearableMemory(
@@ -527,7 +989,7 @@ async function runSeedGraduationPass(input) {
527
989
  if (wrote) promoted += 1;
528
990
  else held += 1;
529
991
  }
530
- return { evaluated, promoted, held, echoSuppressed, disabled: false };
992
+ return { evaluated, promoted, held, contradictionHeld, echoSuppressed, disabled: false };
531
993
  }
532
994
 
533
995
  // src/dedup/connector-scope.ts
@@ -662,31 +1124,6 @@ var REFUSED_MERGE_CATEGORIES = [
662
1124
  "correction"
663
1125
  ];
664
1126
 
665
- // src/dedup/merge-content.ts
666
- var MERGE_CONTENT_LENGTH_FACTOR = 4;
667
- function requireString(value, field) {
668
- if (typeof value !== "string") {
669
- throw new RangeError(`merge ${field} must be a string`);
670
- }
671
- return value;
672
- }
673
- function checkMergedContent(input) {
674
- const incoming = requireString(input.incomingContent, "incomingContent");
675
- const target = requireString(input.targetContent, "targetContent");
676
- const limit = MERGE_CONTENT_LENGTH_FACTOR * (incoming.length + target.length);
677
- const merged = input.mergedContent;
678
- if (typeof merged !== "string" || merged.trim() === "") {
679
- return { ok: false, reason: "empty", limit };
680
- }
681
- if (!sanitizeMemoryContent(merged).clean) {
682
- return { ok: false, reason: "unsafe", limit };
683
- }
684
- if (merged.length > limit) {
685
- return { ok: false, reason: "oversized", limit };
686
- }
687
- return { ok: true, content: merged };
688
- }
689
-
690
1127
  // src/dedup/merge.ts
691
1128
  function compareMergeCandidates(a, b) {
692
1129
  if (a.similarity !== b.similarity) return b.similarity - a.similarity;
@@ -2220,14 +2657,14 @@ function validateScopeProfileTeamReferences(profiles, teams) {
2220
2657
  }
2221
2658
 
2222
2659
  // src/config.ts
2223
- var DEFAULT_MEMORY_DIR = path.join(
2660
+ var DEFAULT_MEMORY_DIR = path2.join(
2224
2661
  resolveHomeDir(),
2225
2662
  ".openclaw",
2226
2663
  "workspace",
2227
2664
  "memory",
2228
2665
  "local"
2229
2666
  );
2230
- var DEFAULT_WORKSPACE_DIR = path.join(
2667
+ var DEFAULT_WORKSPACE_DIR = path2.join(
2231
2668
  resolveHomeDir(),
2232
2669
  ".openclaw",
2233
2670
  "workspace"
@@ -3721,18 +4158,18 @@ function parseConfig(raw, rawOperatorConfig, runtimeSet) {
3721
4158
  benchmarkBaselineSnapshotsEnabled: cfg.benchmarkBaselineSnapshotsEnabled === true,
3722
4159
  benchmarkDeltaReporterEnabled: cfg.benchmarkDeltaReporterEnabled === true,
3723
4160
  benchmarkStoredBaselineEnabled: cfg.benchmarkStoredBaselineEnabled === true,
3724
- evalStoreDir: typeof cfg.evalStoreDir === "string" && cfg.evalStoreDir.trim().length > 0 ? cfg.evalStoreDir.trim() : path.join(memoryDir, "state", "evals"),
4161
+ evalStoreDir: typeof cfg.evalStoreDir === "string" && cfg.evalStoreDir.trim().length > 0 ? cfg.evalStoreDir.trim() : path2.join(memoryDir, "state", "evals"),
3725
4162
  objectiveStateMemoryEnabled: cfg.objectiveStateMemoryEnabled === true,
3726
4163
  objectiveStateSnapshotWritesEnabled: cfg.objectiveStateSnapshotWritesEnabled === true,
3727
4164
  objectiveStateRecallEnabled: cfg.objectiveStateRecallEnabled === true,
3728
- objectiveStateStoreDir: typeof cfg.objectiveStateStoreDir === "string" && cfg.objectiveStateStoreDir.trim().length > 0 ? cfg.objectiveStateStoreDir.trim() : path.join(memoryDir, "state", "objective-state"),
4165
+ objectiveStateStoreDir: typeof cfg.objectiveStateStoreDir === "string" && cfg.objectiveStateStoreDir.trim().length > 0 ? cfg.objectiveStateStoreDir.trim() : path2.join(memoryDir, "state", "objective-state"),
3729
4166
  causalTrajectoryMemoryEnabled: cfg.causalTrajectoryMemoryEnabled === true,
3730
- causalTrajectoryStoreDir: typeof cfg.causalTrajectoryStoreDir === "string" && cfg.causalTrajectoryStoreDir.trim().length > 0 ? cfg.causalTrajectoryStoreDir.trim() : path.join(memoryDir, "state", "causal-trajectories"),
4167
+ causalTrajectoryStoreDir: typeof cfg.causalTrajectoryStoreDir === "string" && cfg.causalTrajectoryStoreDir.trim().length > 0 ? cfg.causalTrajectoryStoreDir.trim() : path2.join(memoryDir, "state", "causal-trajectories"),
3731
4168
  causalTrajectoryRecallEnabled: cfg.causalTrajectoryRecallEnabled === true,
3732
4169
  actionGraphRecallEnabled: cfg.actionGraphRecallEnabled === true,
3733
4170
  trustZonesEnabled: cfg.trustZonesEnabled === true,
3734
4171
  quarantinePromotionEnabled: cfg.quarantinePromotionEnabled === true,
3735
- trustZoneStoreDir: typeof cfg.trustZoneStoreDir === "string" && cfg.trustZoneStoreDir.trim().length > 0 ? cfg.trustZoneStoreDir.trim() : path.join(memoryDir, "state", "trust-zones"),
4172
+ trustZoneStoreDir: typeof cfg.trustZoneStoreDir === "string" && cfg.trustZoneStoreDir.trim().length > 0 ? cfg.trustZoneStoreDir.trim() : path2.join(memoryDir, "state", "trust-zones"),
3736
4173
  trustZoneRecallEnabled: cfg.trustZoneRecallEnabled === true,
3737
4174
  memoryPoisoningDefenseEnabled: cfg.memoryPoisoningDefenseEnabled === true,
3738
4175
  memoryRedTeamBenchEnabled: cfg.memoryRedTeamBenchEnabled === true,
@@ -3862,22 +4299,22 @@ function parseConfig(raw, rawOperatorConfig, runtimeSet) {
3862
4299
  commitmentLedgerEnabled: cfg.commitmentLedgerEnabled === true,
3863
4300
  commitmentLifecycleEnabled: cfg.commitmentLifecycleEnabled === true,
3864
4301
  commitmentStaleDays: typeof cfg.commitmentStaleDays === "number" ? cfg.commitmentStaleDays : 14,
3865
- commitmentLedgerDir: typeof cfg.commitmentLedgerDir === "string" && cfg.commitmentLedgerDir.trim().length > 0 ? cfg.commitmentLedgerDir.trim() : path.join(memoryDir, "state", "commitment-ledger"),
4302
+ commitmentLedgerDir: typeof cfg.commitmentLedgerDir === "string" && cfg.commitmentLedgerDir.trim().length > 0 ? cfg.commitmentLedgerDir.trim() : path2.join(memoryDir, "state", "commitment-ledger"),
3866
4303
  resumeBundlesEnabled: cfg.resumeBundlesEnabled === true,
3867
- resumeBundleDir: typeof cfg.resumeBundleDir === "string" && cfg.resumeBundleDir.trim().length > 0 ? cfg.resumeBundleDir.trim() : path.join(memoryDir, "state", "resume-bundles"),
4304
+ resumeBundleDir: typeof cfg.resumeBundleDir === "string" && cfg.resumeBundleDir.trim().length > 0 ? cfg.resumeBundleDir.trim() : path2.join(memoryDir, "state", "resume-bundles"),
3868
4305
  workProductRecallEnabled: cfg.workProductRecallEnabled === true,
3869
4306
  workTasksEnabled: cfg.workTasksEnabled === true,
3870
4307
  workProjectsEnabled: cfg.workProjectsEnabled === true,
3871
- workTasksDir: typeof cfg.workTasksDir === "string" && cfg.workTasksDir.trim().length > 0 ? cfg.workTasksDir.trim() : path.join(memoryDir, "work", "tasks"),
3872
- workProjectsDir: typeof cfg.workProjectsDir === "string" && cfg.workProjectsDir.trim().length > 0 ? cfg.workProjectsDir.trim() : path.join(memoryDir, "work", "projects"),
4308
+ workTasksDir: typeof cfg.workTasksDir === "string" && cfg.workTasksDir.trim().length > 0 ? cfg.workTasksDir.trim() : path2.join(memoryDir, "work", "tasks"),
4309
+ workProjectsDir: typeof cfg.workProjectsDir === "string" && cfg.workProjectsDir.trim().length > 0 ? cfg.workProjectsDir.trim() : path2.join(memoryDir, "work", "projects"),
3873
4310
  workIndexEnabled: cfg.workIndexEnabled === true,
3874
- workIndexDir: typeof cfg.workIndexDir === "string" && cfg.workIndexDir.trim().length > 0 ? cfg.workIndexDir.trim() : path.join(memoryDir, "work", "index"),
4311
+ workIndexDir: typeof cfg.workIndexDir === "string" && cfg.workIndexDir.trim().length > 0 ? cfg.workIndexDir.trim() : path2.join(memoryDir, "work", "index"),
3875
4312
  workTaskIndexEnabled: cfg.workTaskIndexEnabled === true,
3876
4313
  workProjectIndexEnabled: cfg.workProjectIndexEnabled === true,
3877
4314
  workIndexAutoRebuildEnabled: cfg.workIndexAutoRebuildEnabled === true,
3878
4315
  workIndexAutoRebuildDebounceMs: typeof cfg.workIndexAutoRebuildDebounceMs === "number" ? cfg.workIndexAutoRebuildDebounceMs : 1e3,
3879
- workProductLedgerDir: typeof cfg.workProductLedgerDir === "string" && cfg.workProductLedgerDir.trim().length > 0 ? cfg.workProductLedgerDir.trim() : path.join(memoryDir, "state", "work-product-ledger"),
3880
- abstractionNodeStoreDir: typeof cfg.abstractionNodeStoreDir === "string" && cfg.abstractionNodeStoreDir.trim().length > 0 ? cfg.abstractionNodeStoreDir.trim() : path.join(memoryDir, "state", "abstraction-nodes"),
4316
+ workProductLedgerDir: typeof cfg.workProductLedgerDir === "string" && cfg.workProductLedgerDir.trim().length > 0 ? cfg.workProductLedgerDir.trim() : path2.join(memoryDir, "state", "work-product-ledger"),
4317
+ abstractionNodeStoreDir: typeof cfg.abstractionNodeStoreDir === "string" && cfg.abstractionNodeStoreDir.trim().length > 0 ? cfg.abstractionNodeStoreDir.trim() : path2.join(memoryDir, "state", "abstraction-nodes"),
3881
4318
  ...parseLocalLlmConfig(cfg, localLlmApiKeyEnv, resolveEnvVars),
3882
4319
  // Observability (disabled by default to avoid log spam)
3883
4320
  slowLogEnabled: cfg.slowLogEnabled === true,
@@ -3886,7 +4323,7 @@ function parseConfig(raw, rawOperatorConfig, runtimeSet) {
3886
4323
  traceRecallContent: cfg.traceRecallContent === true,
3887
4324
  // Performance profiling (opt-in, disabled by default)
3888
4325
  profilingEnabled: cfg.profilingEnabled === true,
3889
- profilingStorageDir: typeof cfg.profilingStorageDir === "string" && cfg.profilingStorageDir.length > 0 ? cfg.profilingStorageDir : path.join(memoryDir, "profiling"),
4326
+ profilingStorageDir: typeof cfg.profilingStorageDir === "string" && cfg.profilingStorageDir.length > 0 ? cfg.profilingStorageDir : path2.join(memoryDir, "profiling"),
3890
4327
  profilingMaxTraces: typeof cfg.profilingMaxTraces === "number" && Number.isFinite(cfg.profilingMaxTraces) ? Math.max(0, cfg.profilingMaxTraces) : 100,
3891
4328
  // Extraction stability guards (P0/P1)
3892
4329
  extractionDedupeEnabled: cfg.extractionDedupeEnabled !== false,
@@ -4163,7 +4600,7 @@ function parseConfig(raw, rawOperatorConfig, runtimeSet) {
4163
4600
  remoteSearchTimeoutMs: typeof cfg.remoteSearchTimeoutMs === "number" ? cfg.remoteSearchTimeoutMs : 3e4,
4164
4601
  // LanceDB backend
4165
4602
  lancedbEnabled: cfg.lancedbEnabled === true,
4166
- lanceDbPath: typeof cfg.lanceDbPath === "string" ? cfg.lanceDbPath : path.join(memoryDir, "lancedb"),
4603
+ lanceDbPath: typeof cfg.lanceDbPath === "string" ? cfg.lanceDbPath : path2.join(memoryDir, "lancedb"),
4167
4604
  lanceEmbeddingDimension: typeof cfg.lanceEmbeddingDimension === "number" ? cfg.lanceEmbeddingDimension : 1536,
4168
4605
  // Meilisearch backend
4169
4606
  meilisearchEnabled: cfg.meilisearchEnabled === true,
@@ -4173,7 +4610,7 @@ function parseConfig(raw, rawOperatorConfig, runtimeSet) {
4173
4610
  meilisearchAutoIndex: cfg.meilisearchAutoIndex === true,
4174
4611
  // Orama backend
4175
4612
  oramaEnabled: cfg.oramaEnabled === true,
4176
- oramaDbPath: typeof cfg.oramaDbPath === "string" ? cfg.oramaDbPath : path.join(memoryDir, "orama"),
4613
+ oramaDbPath: typeof cfg.oramaDbPath === "string" ? cfg.oramaDbPath : path2.join(memoryDir, "orama"),
4177
4614
  oramaEmbeddingDimension: typeof cfg.oramaEmbeddingDimension === "number" ? cfg.oramaEmbeddingDimension : 1536,
4178
4615
  // CJK/Thai lexical segmentation (issue #2187) — default on; string forms
4179
4616
  // from CLI `--config` are coerced like neighboring boolean flags.
@@ -4343,7 +4780,7 @@ function parseConfig(raw, rawOperatorConfig, runtimeSet) {
4343
4780
  graphRecallEntityHintsEnabled: cfg.graphRecallEntityHintsEnabled === true,
4344
4781
  graphRecallEntityHintMax: typeof cfg.graphRecallEntityHintMax === "number" ? cfg.graphRecallEntityHintMax : 3,
4345
4782
  graphRecallEntityHintMaxChars: typeof cfg.graphRecallEntityHintMaxChars === "number" ? cfg.graphRecallEntityHintMaxChars : 200,
4346
- graphRecallSnapshotDir: typeof cfg.graphRecallSnapshotDir === "string" && cfg.graphRecallSnapshotDir.trim().length > 0 ? cfg.graphRecallSnapshotDir.trim() : path.join(memoryDir, "state", "graph"),
4783
+ graphRecallSnapshotDir: typeof cfg.graphRecallSnapshotDir === "string" && cfg.graphRecallSnapshotDir.trim().length > 0 ? cfg.graphRecallSnapshotDir.trim() : path2.join(memoryDir, "state", "graph"),
4347
4784
  graphRecallEnableTrace: cfg.graphRecallEnableTrace === true,
4348
4785
  graphRecallEnableDebug: cfg.graphRecallEnableDebug === true,
4349
4786
  graphExpandedIntentEnabled: cfg.graphExpandedIntentEnabled !== false,
@@ -5029,6 +5466,9 @@ function parseTrustScoreWeights(value) {
5029
5466
  export {
5030
5467
  DRIFT_DETECTION_DEFAULTS,
5031
5468
  parseDriftDetectionConfig,
5469
+ buildMergeFrontmatterUpdate,
5470
+ findDuplicates,
5471
+ findContradictions,
5032
5472
  runSeedGraduationPass,
5033
5473
  normalizeConnectorScope,
5034
5474
  connectorMatchesScope,
@@ -5062,4 +5502,4 @@ export {
5062
5502
  VALID_MEMORY_CATEGORIES,
5063
5503
  parseConfig
5064
5504
  };
5065
- //# sourceMappingURL=chunk-UVZBDIJX.js.map
5505
+ //# sourceMappingURL=chunk-KGY6F4VJ.js.map