clawvault 2.6.3 → 2.6.5

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 (127) hide show
  1. package/README.md +351 -21
  2. package/bin/clawvault.js +8 -2
  3. package/bin/command-runtime.js +9 -1
  4. package/bin/register-maintenance-commands.js +19 -0
  5. package/bin/register-query-commands.js +58 -6
  6. package/bin/register-workgraph-commands.js +451 -0
  7. package/dist/chunk-2GKPENIR.js +66 -0
  8. package/dist/{chunk-VXEOHTSL.js → chunk-2JQ3O2YL.js} +1 -1
  9. package/dist/{chunk-VR5NE7PZ.js → chunk-2RAZ4ZFE.js} +1 -1
  10. package/dist/chunk-2ZDO52B4.js +52 -0
  11. package/dist/{chunk-ZZA73MFY.js → chunk-33DOSHTA.js} +176 -36
  12. package/dist/chunk-4BQTQMJP.js +93 -0
  13. package/dist/{chunk-MAKNAHAW.js → chunk-5PJ4STIC.js} +98 -8
  14. package/dist/{chunk-RVYA52PY.js → chunk-5UM4PMMM.js} +1 -1
  15. package/dist/{chunk-4VQTUVH7.js → chunk-7YZWHM36.js} +52 -26
  16. package/dist/{chunk-4VRIMU4O.js → chunk-A4EAUO7T.js} +5 -5
  17. package/dist/{chunk-R6SXNSFD.js → chunk-BV5KWZKR.js} +3 -3
  18. package/dist/chunk-FBITHIZF.js +351 -0
  19. package/dist/{chunk-Q2J5YTUF.js → chunk-FUSLEY6L.js} +751 -34
  20. package/dist/chunk-GNJL4YGR.js +79 -0
  21. package/dist/{chunk-42MXU7A6.js → chunk-K4GFGKFD.js} +51 -47
  22. package/dist/{chunk-PBEE567J.js → chunk-KSZROBFH.js} +2 -2
  23. package/dist/chunk-L4HSSQ6T.js +152 -0
  24. package/dist/{chunk-PZ2AUU2W.js → chunk-LMKQ7NIF.js} +206 -37
  25. package/dist/{chunk-6546Q4OR.js → chunk-M5O6FQ66.js} +6 -6
  26. package/dist/chunk-MM6QGW3P.js +207 -0
  27. package/dist/{chunk-T76H47ZS.js → chunk-MNPUYCHQ.js} +1 -1
  28. package/dist/{chunk-P5EPF6MB.js → chunk-MW5C6ZQA.js} +110 -13
  29. package/dist/{chunk-OZ7RIXTO.js → chunk-QSRRMEYM.js} +2 -2
  30. package/dist/chunk-RHISK3SZ.js +189 -0
  31. package/dist/{chunk-3BTHWPMB.js → chunk-S5OJEGFG.js} +2 -2
  32. package/dist/{chunk-MGDEINGP.js → chunk-SS4B7P7V.js} +1 -1
  33. package/dist/{chunk-ME37YNW3.js → chunk-SV7T4HRE.js} +4 -4
  34. package/dist/{chunk-IEVLHNLU.js → chunk-T3FKSZSN.js} +3 -3
  35. package/dist/{chunk-DTEHFAL7.js → chunk-TS6NDVOU.js} +2 -2
  36. package/dist/chunk-U4O6C46S.js +154 -0
  37. package/dist/{chunk-ITPEXLHA.js → chunk-URXDAUVH.js} +24 -5
  38. package/dist/chunk-WMGIIABP.js +15 -0
  39. package/dist/{chunk-QVMXF7FY.js → chunk-X3SPPUFG.js} +50 -0
  40. package/dist/{chunk-THRJVD4L.js → chunk-Y6VJKXGL.js} +1 -1
  41. package/dist/{chunk-RCBMXTWS.js → chunk-YD7SVXTF.js} +39 -7
  42. package/dist/{chunk-HIHOUSXS.js → chunk-YXQCA6B7.js} +105 -1
  43. package/dist/cli/index.js +20 -18
  44. package/dist/commands/archive.js +3 -2
  45. package/dist/commands/backlog.js +1 -0
  46. package/dist/commands/blocked.js +1 -0
  47. package/dist/commands/canvas.js +2 -1
  48. package/dist/commands/checkpoint.js +1 -0
  49. package/dist/commands/compat.js +2 -1
  50. package/dist/commands/context.js +6 -4
  51. package/dist/commands/doctor.d.ts +10 -1
  52. package/dist/commands/doctor.js +13 -10
  53. package/dist/commands/embed.js +5 -3
  54. package/dist/commands/entities.js +2 -1
  55. package/dist/commands/graph.js +4 -3
  56. package/dist/commands/inject.d.ts +1 -1
  57. package/dist/commands/inject.js +5 -4
  58. package/dist/commands/kanban.js +1 -0
  59. package/dist/commands/link.js +5 -4
  60. package/dist/commands/migrate-observations.js +3 -2
  61. package/dist/commands/observe.js +9 -7
  62. package/dist/commands/project.js +1 -0
  63. package/dist/commands/rebuild-embeddings.d.ts +21 -0
  64. package/dist/commands/rebuild-embeddings.js +91 -0
  65. package/dist/commands/rebuild.js +6 -4
  66. package/dist/commands/recover.js +1 -0
  67. package/dist/commands/reflect.js +5 -4
  68. package/dist/commands/repair-session.js +1 -0
  69. package/dist/commands/replay.js +7 -6
  70. package/dist/commands/session-recap.js +1 -0
  71. package/dist/commands/setup.js +3 -2
  72. package/dist/commands/shell-init.js +2 -0
  73. package/dist/commands/sleep.d.ts +1 -1
  74. package/dist/commands/sleep.js +10 -8
  75. package/dist/commands/status.js +13 -82
  76. package/dist/commands/sync-bd.js +3 -2
  77. package/dist/commands/tailscale.js +3 -2
  78. package/dist/commands/task.js +1 -0
  79. package/dist/commands/template.js +1 -0
  80. package/dist/commands/wake.d.ts +1 -1
  81. package/dist/commands/wake.js +5 -3
  82. package/dist/index.d.ts +254 -10
  83. package/dist/index.js +288 -155
  84. package/dist/{inject-x65KXWPk.d.ts → inject-DYUrDqQO.d.ts} +2 -2
  85. package/dist/ledger-B7g7jhqG.d.ts +44 -0
  86. package/dist/lib/auto-linker.js +2 -1
  87. package/dist/lib/canvas-layout.js +1 -0
  88. package/dist/lib/config.d.ts +27 -3
  89. package/dist/lib/config.js +4 -1
  90. package/dist/lib/entity-index.js +1 -0
  91. package/dist/lib/project-utils.js +1 -0
  92. package/dist/lib/session-repair.js +1 -0
  93. package/dist/lib/session-utils.js +1 -0
  94. package/dist/lib/tailscale.js +1 -0
  95. package/dist/lib/task-utils.js +1 -0
  96. package/dist/lib/template-engine.js +1 -0
  97. package/dist/lib/webdav.js +1 -0
  98. package/dist/onnxruntime_binding-5QEF3SUC.node +0 -0
  99. package/dist/onnxruntime_binding-BKPKNEGC.node +0 -0
  100. package/dist/onnxruntime_binding-FMOXGIUT.node +0 -0
  101. package/dist/onnxruntime_binding-OI2KMXC5.node +0 -0
  102. package/dist/onnxruntime_binding-UX44MLAZ.node +0 -0
  103. package/dist/onnxruntime_binding-Y2W7N7WY.node +0 -0
  104. package/dist/registry-BR4326o0.d.ts +30 -0
  105. package/dist/store-CA-6sKCJ.d.ts +34 -0
  106. package/dist/thread-B9LhXNU0.d.ts +41 -0
  107. package/dist/transformers.node-A2ZRORSQ.js +46775 -0
  108. package/dist/{types-C74wgGL1.d.ts → types-BbWJoC1c.d.ts} +1 -1
  109. package/dist/workgraph/index.d.ts +5 -0
  110. package/dist/workgraph/index.js +23 -0
  111. package/dist/workgraph/ledger.d.ts +2 -0
  112. package/dist/workgraph/ledger.js +25 -0
  113. package/dist/workgraph/registry.d.ts +2 -0
  114. package/dist/workgraph/registry.js +19 -0
  115. package/dist/workgraph/store.d.ts +2 -0
  116. package/dist/workgraph/store.js +25 -0
  117. package/dist/workgraph/thread.d.ts +2 -0
  118. package/dist/workgraph/thread.js +25 -0
  119. package/dist/workgraph/types.d.ts +54 -0
  120. package/dist/workgraph/types.js +7 -0
  121. package/hooks/clawvault/handler.js +714 -2
  122. package/hooks/clawvault/handler.test.js +153 -0
  123. package/hooks/clawvault/openclaw.plugin.json +72 -0
  124. package/openclaw.plugin.json +14 -2
  125. package/package.json +5 -4
  126. package/dist/chunk-4QYGFWRM.js +0 -88
  127. package/dist/chunk-MXSSG3QU.js +0 -42
@@ -12,6 +12,7 @@
12
12
  */
13
13
 
14
14
  import { execFileSync } from 'child_process';
15
+ import { createHash, randomUUID } from 'crypto';
15
16
  import * as fs from 'fs';
16
17
  import * as os from 'os';
17
18
  import * as path from 'path';
@@ -28,6 +29,13 @@ const ONE_MIB = ONE_KIB * ONE_KIB;
28
29
  const SMALL_SESSION_THRESHOLD_BYTES = 50 * ONE_KIB;
29
30
  const MEDIUM_SESSION_THRESHOLD_BYTES = 150 * ONE_KIB;
30
31
  const LARGE_SESSION_THRESHOLD_BYTES = 300 * ONE_KIB;
32
+ const FACTS_FILE = 'facts.jsonl';
33
+ const ENTITY_GRAPH_FILE = 'entity-graph.json';
34
+ const ENTITY_GRAPH_VERSION = 1;
35
+ const MAX_FACT_TEXT_LENGTH = 600;
36
+ const FACT_SENTENCE_SPLIT_RE = /[.!?]+\s+|\r?\n+/;
37
+ const EXCLUSIVE_FACT_RELATIONS = new Set(['lives_in', 'works_at', 'age']);
38
+ const ENTITY_TARGET_RELATIONS = new Set(['works_at', 'lives_in', 'partner_name', 'dog_name', 'parent_name']);
31
39
 
32
40
  // Sanitize string for safe display (prevent prompt injection via control chars)
33
41
  function sanitizeForDisplay(str) {
@@ -462,10 +470,39 @@ function extractPluginConfig(event) {
462
470
  return {};
463
471
  }
464
472
 
473
+ // Resolve vault path for a specific agent from agentVaults config
474
+ function resolveAgentVaultPath(pluginConfig, agentId) {
475
+ if (!agentId || typeof agentId !== 'string') return null;
476
+
477
+ const agentVaults = pluginConfig?.agentVaults;
478
+ if (!agentVaults || typeof agentVaults !== 'object' || Array.isArray(agentVaults)) {
479
+ return null;
480
+ }
481
+
482
+ const agentPath = agentVaults[agentId];
483
+ if (!agentPath || typeof agentPath !== 'string') return null;
484
+
485
+ return validateVaultPath(agentPath);
486
+ }
487
+
465
488
  // Find vault by walking up directories
466
- function findVaultPath(event) {
467
- // Check plugin config first (set via openclaw config set plugins.clawvault.config.vaultPath)
489
+ // Supports per-agent vault paths via agentVaults config
490
+ function findVaultPath(event, options = {}) {
468
491
  const pluginConfig = extractPluginConfig(event);
492
+
493
+ // Determine agent ID for per-agent vault resolution
494
+ const agentId = options.agentId || resolveAgentIdForEvent(event);
495
+
496
+ // Check agentVaults first (per-agent vault paths)
497
+ if (agentId) {
498
+ const agentVaultPath = resolveAgentVaultPath(pluginConfig, agentId);
499
+ if (agentVaultPath) {
500
+ console.log(`[clawvault] Using per-agent vault for ${agentId}: ${agentVaultPath}`);
501
+ return agentVaultPath;
502
+ }
503
+ }
504
+
505
+ // Check plugin config vaultPath (fallback for all agents)
469
506
  if (pluginConfig.vaultPath) {
470
507
  const validated = validateVaultPath(pluginConfig.vaultPath);
471
508
  if (validated) return validated;
@@ -579,6 +616,679 @@ function runObserverCron(vaultPath, agentId, options = {}) {
579
616
  return true;
580
617
  }
581
618
 
619
+ function ensureClawvaultDir(vaultPath) {
620
+ const dir = path.join(vaultPath, '.clawvault');
621
+ if (!fs.existsSync(dir)) {
622
+ fs.mkdirSync(dir, { recursive: true });
623
+ }
624
+ return dir;
625
+ }
626
+
627
+ function getFactsFilePath(vaultPath) {
628
+ return path.join(ensureClawvaultDir(vaultPath), FACTS_FILE);
629
+ }
630
+
631
+ function getEntityGraphFilePath(vaultPath) {
632
+ return path.join(ensureClawvaultDir(vaultPath), ENTITY_GRAPH_FILE);
633
+ }
634
+
635
+ function sanitizeFactText(value, maxLength = MAX_FACT_TEXT_LENGTH) {
636
+ if (typeof value !== 'string') return '';
637
+ return value
638
+ .replace(/[\x00-\x1f\x7f]/g, ' ')
639
+ .replace(/\s+/g, ' ')
640
+ .trim()
641
+ .slice(0, maxLength);
642
+ }
643
+
644
+ function normalizeEntityLabel(value) {
645
+ const cleaned = sanitizeFactText(value, 120).replace(/^[^a-zA-Z0-9]+|[^a-zA-Z0-9]+$/g, '');
646
+ if (!cleaned) return 'User';
647
+ if (/^(i|me|my|mine|we|us|our|ours)$/i.test(cleaned)) {
648
+ return 'User';
649
+ }
650
+ return cleaned;
651
+ }
652
+
653
+ function normalizeEntityToken(value) {
654
+ const normalized = sanitizeFactText(value, 120)
655
+ .toLowerCase()
656
+ .replace(/[^a-z0-9]+/g, '_')
657
+ .replace(/^_+|_+$/g, '');
658
+ return normalized || 'user';
659
+ }
660
+
661
+ function normalizeFactValue(value) {
662
+ return sanitizeFactText(String(value ?? ''), 260)
663
+ .replace(/^[,:;\s-]+|[,:;\s-]+$/g, '')
664
+ .trim();
665
+ }
666
+
667
+ function normalizeFactRelation(value) {
668
+ if (typeof value !== 'string') return '';
669
+ return value
670
+ .trim()
671
+ .toLowerCase()
672
+ .replace(/[^a-z0-9_]+/g, '_')
673
+ .replace(/^_+|_+$/g, '');
674
+ }
675
+
676
+ function clampConfidence(value, fallback = 0.7) {
677
+ const numeric = Number(value);
678
+ if (!Number.isFinite(numeric)) return fallback;
679
+ if (numeric < 0) return 0;
680
+ if (numeric > 1) return 1;
681
+ return numeric;
682
+ }
683
+
684
+ function toIsoTimestamp(value) {
685
+ const date = value instanceof Date ? value : new Date(value);
686
+ if (Number.isNaN(date.getTime())) {
687
+ return new Date().toISOString();
688
+ }
689
+ return date.toISOString();
690
+ }
691
+
692
+ function slugifyForId(value) {
693
+ const base = sanitizeFactText(String(value ?? ''), 180)
694
+ .toLowerCase()
695
+ .replace(/[^a-z0-9]+/g, '-')
696
+ .replace(/^-+|-+$/g, '');
697
+ if (!base) return 'unknown';
698
+ if (base.length <= 80) return base;
699
+ const hash = createHash('sha1').update(base).digest('hex').slice(0, 10);
700
+ return `${base.slice(0, 64)}-${hash}`;
701
+ }
702
+
703
+ function isExclusiveFactRelation(relation) {
704
+ return EXCLUSIVE_FACT_RELATIONS.has(relation) || relation.startsWith('favorite_');
705
+ }
706
+
707
+ function createFactRecord({
708
+ entity,
709
+ relation,
710
+ value,
711
+ validFrom,
712
+ confidence,
713
+ category,
714
+ source,
715
+ rawText
716
+ }) {
717
+ const relationToken = normalizeFactRelation(relation);
718
+ const valueToken = normalizeFactValue(value);
719
+ if (!relationToken || !valueToken) return null;
720
+
721
+ const entityLabel = normalizeEntityLabel(entity || 'User');
722
+ const entityNorm = normalizeEntityToken(entityLabel);
723
+ const factSource = sanitizeFactText(source || 'hook');
724
+ const factRawText = sanitizeFactText(rawText || valueToken);
725
+ const categoryToken = sanitizeFactText(category || 'facts', 40).toLowerCase() || 'facts';
726
+
727
+ return {
728
+ id: randomUUID(),
729
+ entity: entityLabel,
730
+ entityNorm,
731
+ relation: relationToken,
732
+ value: valueToken,
733
+ validFrom: toIsoTimestamp(validFrom),
734
+ validUntil: null,
735
+ confidence: clampConfidence(confidence, 0.7),
736
+ category: categoryToken,
737
+ source: factSource,
738
+ rawText: factRawText
739
+ };
740
+ }
741
+
742
+ function appendPatternFacts(target, sentence, pattern, options = {}) {
743
+ pattern.lastIndex = 0;
744
+ let match;
745
+
746
+ while ((match = pattern.exec(sentence)) !== null) {
747
+ const relation = options.relation;
748
+ const category = options.category || 'facts';
749
+ const confidence = options.confidence ?? 0.7;
750
+ const value = typeof options.value === 'function' ? options.value(match) : match[2];
751
+ const entity = typeof options.entity === 'function'
752
+ ? options.entity(match)
753
+ : options.entity || match[1] || 'User';
754
+
755
+ const record = createFactRecord({
756
+ entity,
757
+ relation,
758
+ value,
759
+ validFrom: options.validFrom,
760
+ confidence,
761
+ category,
762
+ source: options.source,
763
+ rawText: sentence
764
+ });
765
+
766
+ if (record) {
767
+ target.push(record);
768
+ }
769
+ }
770
+ }
771
+
772
+ function extractFactsFromSentence(sentence, options) {
773
+ const source = options.source || 'hook:event';
774
+ const validFrom = options.validFrom || new Date().toISOString();
775
+ const facts = [];
776
+ const subjectPattern = '([A-Za-z][a-z]+(?:\\s+[A-Za-z][a-z]+)?|i|we)';
777
+
778
+ appendPatternFacts(
779
+ facts,
780
+ sentence,
781
+ new RegExp(`\\b${subjectPattern}\\s+(?:really\\s+)?prefer(?:s|red|ring)?\\s+([^.;!?]+)`, 'gi'),
782
+ { relation: 'favorite_preference', category: 'preferences', confidence: 0.86, source, validFrom }
783
+ );
784
+
785
+ appendPatternFacts(
786
+ facts,
787
+ sentence,
788
+ new RegExp(`\\b${subjectPattern}\\s+(?:really\\s+)?like(?:s|d)?\\s+([^.;!?]+)`, 'gi'),
789
+ { relation: 'favorite_preference', category: 'preferences', confidence: 0.8, source, validFrom }
790
+ );
791
+
792
+ appendPatternFacts(
793
+ facts,
794
+ sentence,
795
+ new RegExp(`\\b${subjectPattern}\\s+(?:really\\s+)?(?:hate|dislike(?:s|d)?)\\s+([^.;!?]+)`, 'gi'),
796
+ { relation: 'dislikes', category: 'preferences', confidence: 0.84, source, validFrom }
797
+ );
798
+
799
+ appendPatternFacts(
800
+ facts,
801
+ sentence,
802
+ new RegExp(`\\b${subjectPattern}\\s+(?:am|is|are)?\\s*allergic\\s+to\\s+([^.;!?]+)`, 'gi'),
803
+ { relation: 'allergic_to', category: 'preferences', confidence: 0.92, source, validFrom }
804
+ );
805
+
806
+ appendPatternFacts(
807
+ facts,
808
+ sentence,
809
+ new RegExp(`\\b${subjectPattern}\\s+(?:work|works|working)\\s+at\\s+([^.;!?]+)`, 'gi'),
810
+ { relation: 'works_at', category: 'facts', confidence: 0.92, source, validFrom }
811
+ );
812
+
813
+ appendPatternFacts(
814
+ facts,
815
+ sentence,
816
+ new RegExp(`\\b${subjectPattern}\\s+(?:live|lives|living)\\s+in\\s+([^.;!?]+)`, 'gi'),
817
+ { relation: 'lives_in', category: 'facts', confidence: 0.9, source, validFrom }
818
+ );
819
+
820
+ appendPatternFacts(
821
+ facts,
822
+ sentence,
823
+ new RegExp(`\\b${subjectPattern}\\s+(?:am|is|are)\\s+(\\d{1,3})\\s*(?:years?\\s*old)?\\b`, 'gi'),
824
+ {
825
+ relation: 'age',
826
+ category: 'facts',
827
+ confidence: 0.92,
828
+ source,
829
+ validFrom,
830
+ value: (match) => match[2]
831
+ }
832
+ );
833
+
834
+ appendPatternFacts(
835
+ facts,
836
+ sentence,
837
+ new RegExp(`\\b${subjectPattern}\\s+bought\\s+([^.;!?]+)`, 'gi'),
838
+ { relation: 'bought', category: 'facts', confidence: 0.86, source, validFrom }
839
+ );
840
+
841
+ appendPatternFacts(
842
+ facts,
843
+ sentence,
844
+ new RegExp(`\\b${subjectPattern}\\s+spent\\s+\\$?(\\d+(?:\\.\\d{1,2})?)(?:\\s*(?:usd|dollars?))?(?:\\s+on\\s+([^.;!?]+))?`, 'gi'),
845
+ {
846
+ relation: 'spent',
847
+ category: 'facts',
848
+ confidence: 0.9,
849
+ source,
850
+ validFrom,
851
+ value: (match) => {
852
+ const amount = match[2] ? `$${match[2]}` : '';
853
+ const onWhat = normalizeFactValue(match[3] || '');
854
+ return onWhat ? `${amount} on ${onWhat}` : amount;
855
+ }
856
+ }
857
+ );
858
+
859
+ appendPatternFacts(
860
+ facts,
861
+ sentence,
862
+ new RegExp(`\\b${subjectPattern}\\s+(?:decided|chose)\\s+(?:to\\s+|on\\s+)?([^.;!?]+)`, 'gi'),
863
+ { relation: 'decided', category: 'decisions', confidence: 0.88, source, validFrom }
864
+ );
865
+
866
+ appendPatternFacts(
867
+ facts,
868
+ sentence,
869
+ /\bmy\s+partner\s+is\s+([A-Za-z][a-z]+(?:\s+[A-Za-z][a-z]+)*)\b/gi,
870
+ { relation: 'partner_name', category: 'entities', confidence: 0.9, source, validFrom, entity: 'User', value: (match) => match[1] }
871
+ );
872
+
873
+ appendPatternFacts(
874
+ facts,
875
+ sentence,
876
+ /\b([A-Za-z][a-z]+(?:\s+[A-Za-z][a-z]+)*)\s+is\s+my\s+partner\b/gi,
877
+ { relation: 'partner_name', category: 'entities', confidence: 0.9, source, validFrom, entity: 'User', value: (match) => match[1] }
878
+ );
879
+
880
+ appendPatternFacts(
881
+ facts,
882
+ sentence,
883
+ /\bmy\s+dog\s+is\s+([A-Za-z][a-z]+(?:\s+[A-Za-z][a-z]+)*)\b/gi,
884
+ { relation: 'dog_name', category: 'entities', confidence: 0.9, source, validFrom, entity: 'User', value: (match) => match[1] }
885
+ );
886
+
887
+ appendPatternFacts(
888
+ facts,
889
+ sentence,
890
+ /\bmy\s+(?:mom|mother|dad|father|parent)\s+is\s+([A-Za-z][a-z]+(?:\s+[A-Za-z][a-z]+)*)\b/gi,
891
+ { relation: 'parent_name', category: 'entities', confidence: 0.9, source, validFrom, entity: 'User', value: (match) => match[1] }
892
+ );
893
+
894
+ const deduped = [];
895
+ const seen = new Set();
896
+ for (const fact of facts) {
897
+ const dedupeKey = `${fact.entityNorm}|${fact.relation}|${normalizeFactValue(fact.value).toLowerCase()}`;
898
+ if (seen.has(dedupeKey)) continue;
899
+ seen.add(dedupeKey);
900
+ deduped.push(fact);
901
+ }
902
+
903
+ return deduped;
904
+ }
905
+
906
+ function splitObservedTextIntoSentences(text) {
907
+ return sanitizeFactText(text, 6000)
908
+ .split(FACT_SENTENCE_SPLIT_RE)
909
+ .map((part) => sanitizeFactText(part))
910
+ .filter((part) => part.length >= 8);
911
+ }
912
+
913
+ function collectTextsFromMessageLike(target, value, depth = 0) {
914
+ if (depth > 3 || value === null || value === undefined) return;
915
+
916
+ if (typeof value === 'string') {
917
+ const text = sanitizeFactText(value, 4000);
918
+ if (text) target.push(text);
919
+ return;
920
+ }
921
+
922
+ if (Array.isArray(value)) {
923
+ for (const entry of value) {
924
+ collectTextsFromMessageLike(target, entry, depth + 1);
925
+ }
926
+ return;
927
+ }
928
+
929
+ if (typeof value !== 'object') return;
930
+
931
+ const direct = extractTextFromMessage(value);
932
+ if (direct) {
933
+ target.push(sanitizeFactText(direct, 4000));
934
+ }
935
+
936
+ const directKeys = ['text', 'message', 'content', 'rawText', 'observedText', 'observation', 'prompt'];
937
+ for (const key of directKeys) {
938
+ if (typeof value[key] === 'string') {
939
+ target.push(sanitizeFactText(value[key], 4000));
940
+ }
941
+ }
942
+
943
+ const nestedKeys = ['messages', 'history', 'entries', 'items', 'observations', 'events', 'payload', 'context'];
944
+ for (const key of nestedKeys) {
945
+ if (value[key] !== undefined) {
946
+ collectTextsFromMessageLike(target, value[key], depth + 1);
947
+ }
948
+ }
949
+ }
950
+
951
+ function collectObservedTextsForFactExtraction(event) {
952
+ const collected = [];
953
+
954
+ const directStringCandidates = [
955
+ event?.text,
956
+ event?.message,
957
+ event?.content,
958
+ event?.rawText,
959
+ event?.context?.text,
960
+ event?.context?.message,
961
+ event?.context?.content,
962
+ event?.context?.rawText,
963
+ event?.context?.initialPrompt
964
+ ];
965
+
966
+ for (const candidate of directStringCandidates) {
967
+ if (typeof candidate === 'string') {
968
+ const text = sanitizeFactText(candidate, 4000);
969
+ if (text) collected.push(text);
970
+ }
971
+ }
972
+
973
+ const structuredCandidates = [
974
+ event?.messages,
975
+ event?.context?.messages,
976
+ event?.context?.history,
977
+ event?.context?.initialMessages,
978
+ event?.context?.memoryFlush,
979
+ event?.context?.flush,
980
+ event?.observations,
981
+ event?.context?.observations,
982
+ event?.payload?.messages,
983
+ event?.payload?.events
984
+ ];
985
+
986
+ for (const candidate of structuredCandidates) {
987
+ collectTextsFromMessageLike(collected, candidate);
988
+ }
989
+
990
+ const deduped = [];
991
+ const seen = new Set();
992
+ for (const item of collected) {
993
+ const normalized = sanitizeFactText(item, 4000);
994
+ if (!normalized) continue;
995
+ if (seen.has(normalized)) continue;
996
+ seen.add(normalized);
997
+ deduped.push(normalized);
998
+ }
999
+ return deduped;
1000
+ }
1001
+
1002
+ function extractFactsFromObservedText(observedTexts, options) {
1003
+ const facts = [];
1004
+ const globalSeen = new Set();
1005
+ for (const text of observedTexts) {
1006
+ for (const sentence of splitObservedTextIntoSentences(text)) {
1007
+ const extracted = extractFactsFromSentence(sentence, options);
1008
+ for (const fact of extracted) {
1009
+ const dedupeKey = `${fact.entityNorm}|${fact.relation}|${normalizeFactValue(fact.value).toLowerCase()}`;
1010
+ if (globalSeen.has(dedupeKey)) continue;
1011
+ globalSeen.add(dedupeKey);
1012
+ facts.push(fact);
1013
+ }
1014
+ }
1015
+ }
1016
+ return facts;
1017
+ }
1018
+
1019
+ function normalizeStoredFact(raw) {
1020
+ if (!raw || typeof raw !== 'object') return null;
1021
+ const relation = normalizeFactRelation(raw.relation);
1022
+ const value = normalizeFactValue(raw.value);
1023
+ if (!relation || !value) return null;
1024
+
1025
+ const entity = normalizeEntityLabel(raw.entity || raw.entityNorm || 'User');
1026
+ const entityNorm = normalizeEntityToken(raw.entityNorm || entity);
1027
+ const validFrom = toIsoTimestamp(raw.validFrom || new Date().toISOString());
1028
+ let validUntil = null;
1029
+ if (typeof raw.validUntil === 'string' && raw.validUntil.trim()) {
1030
+ validUntil = toIsoTimestamp(raw.validUntil);
1031
+ }
1032
+
1033
+ const idBase = `${entityNorm}|${relation}|${value}|${validFrom}`;
1034
+ const fallbackId = createHash('sha1').update(idBase).digest('hex').slice(0, 16);
1035
+
1036
+ return {
1037
+ id: typeof raw.id === 'string' && raw.id.trim() ? raw.id.trim() : fallbackId,
1038
+ entity,
1039
+ entityNorm,
1040
+ relation,
1041
+ value,
1042
+ validFrom,
1043
+ validUntil,
1044
+ confidence: clampConfidence(raw.confidence, 0.7),
1045
+ category: sanitizeFactText(raw.category || 'facts', 40).toLowerCase() || 'facts',
1046
+ source: sanitizeFactText(raw.source || 'hook', 120) || 'hook',
1047
+ rawText: sanitizeFactText(raw.rawText || value, MAX_FACT_TEXT_LENGTH)
1048
+ };
1049
+ }
1050
+
1051
+ function readFactsFromVault(vaultPath) {
1052
+ const factsPath = getFactsFilePath(vaultPath);
1053
+ if (!fs.existsSync(factsPath)) {
1054
+ return [];
1055
+ }
1056
+
1057
+ try {
1058
+ const lines = fs.readFileSync(factsPath, 'utf-8')
1059
+ .split(/\r?\n/)
1060
+ .map((line) => line.trim())
1061
+ .filter(Boolean);
1062
+ const facts = [];
1063
+ for (const line of lines) {
1064
+ try {
1065
+ const parsed = JSON.parse(line);
1066
+ const normalized = normalizeStoredFact(parsed);
1067
+ if (normalized) facts.push(normalized);
1068
+ } catch {
1069
+ // Skip malformed lines and keep processing.
1070
+ }
1071
+ }
1072
+ return facts;
1073
+ } catch {
1074
+ return [];
1075
+ }
1076
+ }
1077
+
1078
+ function writeFactsToVault(vaultPath, facts) {
1079
+ const factsPath = getFactsFilePath(vaultPath);
1080
+ const lines = facts.map((fact) => JSON.stringify(fact));
1081
+ const payload = lines.length > 0 ? `${lines.join('\n')}\n` : '';
1082
+ fs.writeFileSync(factsPath, payload, 'utf-8');
1083
+ }
1084
+
1085
+ function mergeFactsWithConflictResolution(existingFacts, incomingFacts) {
1086
+ const merged = [...existingFacts];
1087
+ let added = 0;
1088
+ let superseded = 0;
1089
+ let changed = false;
1090
+
1091
+ for (const incoming of incomingFacts) {
1092
+ const activeSameRelation = merged.filter((fact) =>
1093
+ fact.entityNorm === incoming.entityNorm
1094
+ && fact.relation === incoming.relation
1095
+ && !fact.validUntil
1096
+ );
1097
+
1098
+ const incomingValue = normalizeFactValue(incoming.value).toLowerCase();
1099
+ const hasExactActiveMatch = activeSameRelation.some((fact) =>
1100
+ normalizeFactValue(fact.value).toLowerCase() === incomingValue
1101
+ );
1102
+ if (hasExactActiveMatch) {
1103
+ continue;
1104
+ }
1105
+
1106
+ const shouldSupersede = activeSameRelation.some((fact) =>
1107
+ normalizeFactValue(fact.value).toLowerCase() !== incomingValue
1108
+ );
1109
+ if (shouldSupersede || isExclusiveFactRelation(incoming.relation)) {
1110
+ for (const fact of activeSameRelation) {
1111
+ if (normalizeFactValue(fact.value).toLowerCase() === incomingValue) continue;
1112
+ if (!fact.validUntil) {
1113
+ fact.validUntil = incoming.validFrom;
1114
+ superseded += 1;
1115
+ changed = true;
1116
+ }
1117
+ }
1118
+ }
1119
+
1120
+ merged.push(incoming);
1121
+ added += 1;
1122
+ changed = true;
1123
+ }
1124
+
1125
+ return { facts: merged, added, superseded, changed };
1126
+ }
1127
+
1128
+ function isTimestampAfter(candidate, reference) {
1129
+ const candidateTime = new Date(candidate).getTime();
1130
+ const referenceTime = new Date(reference).getTime();
1131
+ if (Number.isNaN(candidateTime)) return false;
1132
+ if (Number.isNaN(referenceTime)) return true;
1133
+ return candidateTime > referenceTime;
1134
+ }
1135
+
1136
+ function ensureGraphNode(nodesById, descriptor, seenAt) {
1137
+ const existing = nodesById.get(descriptor.id);
1138
+ if (!existing) {
1139
+ nodesById.set(descriptor.id, {
1140
+ id: descriptor.id,
1141
+ name: descriptor.name,
1142
+ displayName: descriptor.displayName,
1143
+ type: descriptor.type,
1144
+ attributes: descriptor.attributes || {},
1145
+ lastSeen: seenAt
1146
+ });
1147
+ return;
1148
+ }
1149
+
1150
+ existing.attributes = { ...existing.attributes, ...(descriptor.attributes || {}) };
1151
+ if (isTimestampAfter(seenAt, existing.lastSeen)) {
1152
+ existing.lastSeen = seenAt;
1153
+ }
1154
+ }
1155
+
1156
+ function inferTargetNodeType(relation) {
1157
+ if (relation === 'works_at') return 'organization';
1158
+ if (relation === 'lives_in') return 'location';
1159
+ if (relation === 'partner_name' || relation === 'parent_name') return 'person';
1160
+ if (relation === 'dog_name') return 'pet';
1161
+ if (relation === 'age' || relation === 'spent') return 'number';
1162
+ if (relation === 'bought') return 'item';
1163
+ if (relation === 'decided') return 'decision';
1164
+ if (relation === 'allergic_to') return 'substance';
1165
+ if (relation === 'favorite_preference' || relation === 'dislikes') return 'preference';
1166
+ return 'attribute';
1167
+ }
1168
+
1169
+ function buildTargetNodeDescriptor(fact) {
1170
+ const relation = normalizeFactRelation(fact.relation);
1171
+ const value = normalizeFactValue(fact.value);
1172
+ if (!relation || !value) return null;
1173
+
1174
+ if (ENTITY_TARGET_RELATIONS.has(relation)) {
1175
+ const normalizedEntityValue = normalizeEntityToken(value);
1176
+ return {
1177
+ id: `entity:${slugifyForId(normalizedEntityValue)}`,
1178
+ name: normalizedEntityValue,
1179
+ displayName: value,
1180
+ type: inferTargetNodeType(relation),
1181
+ attributes: { relation }
1182
+ };
1183
+ }
1184
+
1185
+ return {
1186
+ id: `value:${relation}:${slugifyForId(value)}`,
1187
+ name: value.toLowerCase(),
1188
+ displayName: value,
1189
+ type: inferTargetNodeType(relation),
1190
+ attributes: { relation }
1191
+ };
1192
+ }
1193
+
1194
+ function buildEntityGraphFromFacts(facts) {
1195
+ const nodesById = new Map();
1196
+ const edges = [];
1197
+
1198
+ for (const fact of facts) {
1199
+ const normalized = normalizeStoredFact(fact);
1200
+ if (!normalized) continue;
1201
+
1202
+ const sourceNodeId = `entity:${slugifyForId(normalized.entityNorm)}`;
1203
+ const seenAt = normalized.validFrom || new Date().toISOString();
1204
+ ensureGraphNode(nodesById, {
1205
+ id: sourceNodeId,
1206
+ name: normalized.entityNorm,
1207
+ displayName: normalized.entity,
1208
+ type: 'person',
1209
+ attributes: { entityNorm: normalized.entityNorm }
1210
+ }, seenAt);
1211
+
1212
+ const targetNode = buildTargetNodeDescriptor(normalized);
1213
+ if (!targetNode) continue;
1214
+ ensureGraphNode(nodesById, targetNode, seenAt);
1215
+
1216
+ const edgeHashSource = `${normalized.id}|${sourceNodeId}|${targetNode.id}|${normalized.relation}|${normalized.validFrom}`;
1217
+ const edgeId = `edge:${createHash('sha1').update(edgeHashSource).digest('hex').slice(0, 18)}`;
1218
+
1219
+ edges.push({
1220
+ id: edgeId,
1221
+ source: sourceNodeId,
1222
+ target: targetNode.id,
1223
+ relation: normalized.relation,
1224
+ validFrom: normalized.validFrom,
1225
+ validUntil: normalized.validUntil,
1226
+ confidence: clampConfidence(normalized.confidence, 0.7)
1227
+ });
1228
+ }
1229
+
1230
+ const nodes = [...nodesById.values()].sort((a, b) => a.id.localeCompare(b.id));
1231
+ const sortedEdges = edges.sort((a, b) => a.id.localeCompare(b.id));
1232
+ return {
1233
+ version: ENTITY_GRAPH_VERSION,
1234
+ nodes,
1235
+ edges: sortedEdges
1236
+ };
1237
+ }
1238
+
1239
+ function writeEntityGraphToVault(vaultPath, facts) {
1240
+ const graphPath = getEntityGraphFilePath(vaultPath);
1241
+ const graph = buildEntityGraphFromFacts(facts);
1242
+ fs.writeFileSync(graphPath, JSON.stringify(graph, null, 2), 'utf-8');
1243
+ }
1244
+
1245
+ function persistExtractedFacts(vaultPath, incomingFacts) {
1246
+ const existingFacts = readFactsFromVault(vaultPath);
1247
+ const normalizedIncomingFacts = incomingFacts
1248
+ .map((fact) => normalizeStoredFact(fact))
1249
+ .filter(Boolean);
1250
+
1251
+ if (normalizedIncomingFacts.length === 0) {
1252
+ writeEntityGraphToVault(vaultPath, existingFacts);
1253
+ return { facts: existingFacts, added: 0, superseded: 0 };
1254
+ }
1255
+
1256
+ const { facts, added, superseded, changed } = mergeFactsWithConflictResolution(
1257
+ existingFacts,
1258
+ normalizedIncomingFacts
1259
+ );
1260
+
1261
+ if (changed || !fs.existsSync(getFactsFilePath(vaultPath))) {
1262
+ writeFactsToVault(vaultPath, facts);
1263
+ }
1264
+ writeEntityGraphToVault(vaultPath, facts);
1265
+ return { facts, added, superseded };
1266
+ }
1267
+
1268
+ function runFactExtractionForEvent(vaultPath, event, eventLabel) {
1269
+ try {
1270
+ const observedTexts = collectObservedTextsForFactExtraction(event);
1271
+ if (observedTexts.length === 0) {
1272
+ console.log(`[clawvault] Fact extraction skipped (${eventLabel}: no observed text)`);
1273
+ return;
1274
+ }
1275
+
1276
+ const validFrom = toIsoTimestamp(extractEventTimestamp(event) || new Date());
1277
+ const source = `hook:${eventLabel}`;
1278
+ const extracted = extractFactsFromObservedText(observedTexts, { source, validFrom });
1279
+
1280
+ if (extracted.length === 0) {
1281
+ console.log(`[clawvault] Fact extraction found no matches (${eventLabel})`);
1282
+ return;
1283
+ }
1284
+
1285
+ const { facts, added, superseded } = persistExtractedFacts(vaultPath, extracted);
1286
+ console.log(`[clawvault] Fact extraction complete (${eventLabel}): +${added}, superseded ${superseded}, total ${facts.length}`);
1287
+ } catch (err) {
1288
+ console.warn(`[clawvault] Fact extraction failed (${eventLabel}): ${err?.message || 'unknown error'}`);
1289
+ }
1290
+ }
1291
+
582
1292
  function extractEventTimestamp(event) {
583
1293
  const candidates = [
584
1294
  event?.timestamp,
@@ -698,6 +1408,7 @@ async function handleNew(event) {
698
1408
  minNewBytes: 1,
699
1409
  reason: 'command:new flush'
700
1410
  });
1411
+ runFactExtractionForEvent(vaultPath, event, 'command:new');
701
1412
  }
702
1413
 
703
1414
  // Handle session start - inject dynamic context for first prompt
@@ -792,6 +1503,7 @@ async function handleContextCompaction(event) {
792
1503
  minNewBytes: 1,
793
1504
  reason: 'context compaction'
794
1505
  });
1506
+ runFactExtractionForEvent(vaultPath, event, 'compaction:memoryFlush');
795
1507
  }
796
1508
 
797
1509
  // Main handler - route events