clawvault 2.6.1 → 2.6.4

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 (125) hide show
  1. package/README.md +352 -20
  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-VXEOHTSL.js → chunk-2JQ3O2YL.js} +1 -1
  8. package/dist/{chunk-VR5NE7PZ.js → chunk-2RAZ4ZFE.js} +1 -1
  9. package/dist/chunk-2ZDO52B4.js +52 -0
  10. package/dist/chunk-4BQTQMJP.js +93 -0
  11. package/dist/{chunk-MAKNAHAW.js → chunk-5PJ4STIC.js} +98 -8
  12. package/dist/{chunk-IEVLHNLU.js → chunk-627Q3QWK.js} +3 -3
  13. package/dist/{chunk-R6SXNSFD.js → chunk-6NYYDNNG.js} +3 -3
  14. package/dist/chunk-ECRZL5XR.js +50 -0
  15. package/dist/chunk-GNJL4YGR.js +79 -0
  16. package/dist/{chunk-OZ7RIXTO.js → chunk-IIOU45CK.js} +1 -1
  17. package/dist/chunk-L4HSSQ6T.js +152 -0
  18. package/dist/{chunk-XAVB4GB4.js → chunk-LIGHWOH6.js} +1 -1
  19. package/dist/{chunk-PBEE567J.js → chunk-LUBZXECN.js} +2 -2
  20. package/dist/{chunk-UEOUADMO.js → chunk-MFL6EEPF.js} +204 -35
  21. package/dist/chunk-MM6QGW3P.js +207 -0
  22. package/dist/{chunk-T76H47ZS.js → chunk-MNPUYCHQ.js} +1 -1
  23. package/dist/{chunk-TLGBDTYT.js → chunk-MPOSMDMU.js} +6 -6
  24. package/dist/{chunk-RVYA52PY.js → chunk-NJYJL5AA.js} +1 -1
  25. package/dist/{chunk-Q2J5YTUF.js → chunk-OQGYFZ4A.js} +669 -33
  26. package/dist/{chunk-ME37YNW3.js → chunk-P7SY3D4E.js} +3 -3
  27. package/dist/chunk-RHISK3SZ.js +189 -0
  28. package/dist/{chunk-3BTHWPMB.js → chunk-S5OJEGFG.js} +2 -2
  29. package/dist/{chunk-MGDEINGP.js → chunk-SS4B7P7V.js} +1 -1
  30. package/dist/chunk-U4O6C46S.js +154 -0
  31. package/dist/{chunk-ITPEXLHA.js → chunk-URXDAUVH.js} +24 -5
  32. package/dist/chunk-WIOLLGAD.js +190 -0
  33. package/dist/chunk-WMGIIABP.js +15 -0
  34. package/dist/{chunk-QVMXF7FY.js → chunk-X3SPPUFG.js} +50 -0
  35. package/dist/{chunk-THRJVD4L.js → chunk-Y6VJKXGL.js} +1 -1
  36. package/dist/{chunk-KL4NAOMO.js → chunk-YDWHS4LJ.js} +49 -9
  37. package/dist/{chunk-4VRIMU4O.js → chunk-YNIPYN4F.js} +4 -4
  38. package/dist/{chunk-HIHOUSXS.js → chunk-YXQCA6B7.js} +105 -1
  39. package/dist/cli/index.js +18 -16
  40. package/dist/commands/archive.js +3 -2
  41. package/dist/commands/backlog.js +1 -0
  42. package/dist/commands/blocked.js +1 -0
  43. package/dist/commands/canvas.js +1 -0
  44. package/dist/commands/checkpoint.js +1 -0
  45. package/dist/commands/compat.js +2 -1
  46. package/dist/commands/context.js +5 -3
  47. package/dist/commands/doctor.d.ts +10 -1
  48. package/dist/commands/doctor.js +11 -8
  49. package/dist/commands/embed.js +5 -3
  50. package/dist/commands/entities.js +2 -1
  51. package/dist/commands/graph.js +3 -2
  52. package/dist/commands/inject.d.ts +1 -1
  53. package/dist/commands/inject.js +4 -3
  54. package/dist/commands/kanban.js +1 -0
  55. package/dist/commands/link.js +2 -1
  56. package/dist/commands/migrate-observations.js +3 -2
  57. package/dist/commands/observe.js +8 -6
  58. package/dist/commands/project.js +1 -0
  59. package/dist/commands/rebuild-embeddings.d.ts +21 -0
  60. package/dist/commands/rebuild-embeddings.js +91 -0
  61. package/dist/commands/rebuild.js +6 -4
  62. package/dist/commands/recover.js +1 -0
  63. package/dist/commands/reflect.js +5 -4
  64. package/dist/commands/repair-session.js +1 -0
  65. package/dist/commands/replay.js +7 -6
  66. package/dist/commands/session-recap.js +1 -0
  67. package/dist/commands/setup.js +3 -2
  68. package/dist/commands/shell-init.js +2 -0
  69. package/dist/commands/sleep.d.ts +1 -1
  70. package/dist/commands/sleep.js +8 -6
  71. package/dist/commands/status.d.ts +2 -0
  72. package/dist/commands/status.js +35 -24
  73. package/dist/commands/sync-bd.js +3 -2
  74. package/dist/commands/tailscale.js +3 -2
  75. package/dist/commands/task.js +1 -0
  76. package/dist/commands/template.js +1 -0
  77. package/dist/commands/wake.d.ts +1 -1
  78. package/dist/commands/wake.js +4 -2
  79. package/dist/index.d.ts +333 -10
  80. package/dist/index.js +320 -33
  81. package/dist/{inject-x65KXWPk.d.ts → inject-DYUrDqQO.d.ts} +2 -2
  82. package/dist/ledger-B7g7jhqG.d.ts +44 -0
  83. package/dist/lib/auto-linker.js +1 -0
  84. package/dist/lib/canvas-layout.js +1 -0
  85. package/dist/lib/config.d.ts +27 -3
  86. package/dist/lib/config.js +4 -1
  87. package/dist/lib/entity-index.js +1 -0
  88. package/dist/lib/project-utils.js +1 -0
  89. package/dist/lib/session-repair.js +1 -0
  90. package/dist/lib/session-utils.js +1 -0
  91. package/dist/lib/tailscale.js +1 -0
  92. package/dist/lib/task-utils.js +1 -0
  93. package/dist/lib/template-engine.js +1 -0
  94. package/dist/lib/webdav.js +1 -0
  95. package/dist/onnxruntime_binding-5QEF3SUC.node +0 -0
  96. package/dist/onnxruntime_binding-BKPKNEGC.node +0 -0
  97. package/dist/onnxruntime_binding-FMOXGIUT.node +0 -0
  98. package/dist/onnxruntime_binding-OI2KMXC5.node +0 -0
  99. package/dist/onnxruntime_binding-UX44MLAZ.node +0 -0
  100. package/dist/onnxruntime_binding-Y2W7N7WY.node +0 -0
  101. package/dist/registry-BR4326o0.d.ts +30 -0
  102. package/dist/store-CA-6sKCJ.d.ts +34 -0
  103. package/dist/thread-B9LhXNU0.d.ts +41 -0
  104. package/dist/transformers.node-A2ZRORSQ.js +46775 -0
  105. package/dist/{types-C74wgGL1.d.ts → types-BbWJoC1c.d.ts} +1 -1
  106. package/dist/workgraph/index.d.ts +5 -0
  107. package/dist/workgraph/index.js +23 -0
  108. package/dist/workgraph/ledger.d.ts +2 -0
  109. package/dist/workgraph/ledger.js +25 -0
  110. package/dist/workgraph/registry.d.ts +2 -0
  111. package/dist/workgraph/registry.js +19 -0
  112. package/dist/workgraph/store.d.ts +2 -0
  113. package/dist/workgraph/store.js +25 -0
  114. package/dist/workgraph/thread.d.ts +2 -0
  115. package/dist/workgraph/thread.js +25 -0
  116. package/dist/workgraph/types.d.ts +54 -0
  117. package/dist/workgraph/types.js +7 -0
  118. package/hooks/clawvault/HOOK.md +34 -4
  119. package/hooks/clawvault/handler.js +751 -8
  120. package/hooks/clawvault/handler.test.js +247 -0
  121. package/hooks/clawvault/openclaw.plugin.json +72 -0
  122. package/openclaw.plugin.json +84 -0
  123. package/package.json +8 -4
  124. package/dist/chunk-4QYGFWRM.js +0 -88
  125. 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) {
@@ -444,9 +452,69 @@ function validateVaultPath(vaultPath) {
444
452
  return resolved;
445
453
  }
446
454
 
455
+ // Extract plugin config from event context (set via openclaw config)
456
+ function extractPluginConfig(event) {
457
+ const candidates = [
458
+ event?.pluginConfig,
459
+ event?.context?.pluginConfig,
460
+ event?.config?.plugins?.clawvault?.config,
461
+ event?.context?.config?.plugins?.clawvault?.config
462
+ ];
463
+
464
+ for (const candidate of candidates) {
465
+ if (candidate && typeof candidate === 'object' && !Array.isArray(candidate)) {
466
+ return candidate;
467
+ }
468
+ }
469
+
470
+ return {};
471
+ }
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
+
447
488
  // Find vault by walking up directories
448
- function findVaultPath() {
449
- // Check env first
489
+ // Supports per-agent vault paths via agentVaults config
490
+ function findVaultPath(event, options = {}) {
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)
506
+ if (pluginConfig.vaultPath) {
507
+ const validated = validateVaultPath(pluginConfig.vaultPath);
508
+ if (validated) return validated;
509
+ }
510
+
511
+ // Check OPENCLAW_PLUGIN_CLAWVAULT_VAULTPATH env (injected by OpenClaw from plugin config)
512
+ if (process.env.OPENCLAW_PLUGIN_CLAWVAULT_VAULTPATH) {
513
+ const validated = validateVaultPath(process.env.OPENCLAW_PLUGIN_CLAWVAULT_VAULTPATH);
514
+ if (validated) return validated;
515
+ }
516
+
517
+ // Check CLAWVAULT_PATH env
450
518
  if (process.env.CLAWVAULT_PATH) {
451
519
  return validateVaultPath(process.env.CLAWVAULT_PATH);
452
520
  }
@@ -548,6 +616,679 @@ function runObserverCron(vaultPath, agentId, options = {}) {
548
616
  return true;
549
617
  }
550
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
+
551
1292
  function extractEventTimestamp(event) {
552
1293
  const candidates = [
553
1294
  event?.timestamp,
@@ -571,7 +1312,7 @@ function isSundayMidnightUtc(date) {
571
1312
  }
572
1313
 
573
1314
  async function handleWeeklyReflect(event) {
574
- const vaultPath = findVaultPath();
1315
+ const vaultPath = findVaultPath(event);
575
1316
  if (!vaultPath) {
576
1317
  console.log('[clawvault] No vault found, skipping weekly reflection');
577
1318
  return;
@@ -593,7 +1334,7 @@ async function handleWeeklyReflect(event) {
593
1334
 
594
1335
  // Handle gateway startup - check for context death
595
1336
  async function handleStartup(event) {
596
- const vaultPath = findVaultPath();
1337
+ const vaultPath = findVaultPath(event);
597
1338
  if (!vaultPath) {
598
1339
  console.log('[clawvault] No vault found, skipping recovery check');
599
1340
  return;
@@ -632,7 +1373,7 @@ async function handleStartup(event) {
632
1373
 
633
1374
  // Handle /new command - auto-checkpoint before reset
634
1375
  async function handleNew(event) {
635
- const vaultPath = findVaultPath();
1376
+ const vaultPath = findVaultPath(event);
636
1377
  if (!vaultPath) {
637
1378
  console.log('[clawvault] No vault found, skipping auto-checkpoint');
638
1379
  return;
@@ -667,11 +1408,12 @@ async function handleNew(event) {
667
1408
  minNewBytes: 1,
668
1409
  reason: 'command:new flush'
669
1410
  });
1411
+ runFactExtractionForEvent(vaultPath, event, 'command:new');
670
1412
  }
671
1413
 
672
1414
  // Handle session start - inject dynamic context for first prompt
673
1415
  async function handleSessionStart(event) {
674
- const vaultPath = findVaultPath();
1416
+ const vaultPath = findVaultPath(event);
675
1417
  if (!vaultPath) {
676
1418
  console.log('[clawvault] No vault found, skipping context injection');
677
1419
  return;
@@ -733,7 +1475,7 @@ async function handleSessionStart(event) {
733
1475
 
734
1476
  // Handle heartbeat events - cheap stat-based trigger for active observation
735
1477
  async function handleHeartbeat(event) {
736
- const vaultPath = findVaultPath();
1478
+ const vaultPath = findVaultPath(event);
737
1479
  if (!vaultPath) {
738
1480
  console.log('[clawvault] No vault found, skipping heartbeat observation check');
739
1481
  return;
@@ -750,7 +1492,7 @@ async function handleHeartbeat(event) {
750
1492
 
751
1493
  // Handle context compaction - force flush any pending session deltas
752
1494
  async function handleContextCompaction(event) {
753
- const vaultPath = findVaultPath();
1495
+ const vaultPath = findVaultPath(event);
754
1496
  if (!vaultPath) {
755
1497
  console.log('[clawvault] No vault found, skipping compaction observation');
756
1498
  return;
@@ -761,6 +1503,7 @@ async function handleContextCompaction(event) {
761
1503
  minNewBytes: 1,
762
1504
  reason: 'context compaction'
763
1505
  });
1506
+ runFactExtractionForEvent(vaultPath, event, 'compaction:memoryFlush');
764
1507
  }
765
1508
 
766
1509
  // Main handler - route events