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.
- package/README.md +351 -21
- package/bin/clawvault.js +8 -2
- package/bin/command-runtime.js +9 -1
- package/bin/register-maintenance-commands.js +19 -0
- package/bin/register-query-commands.js +58 -6
- package/bin/register-workgraph-commands.js +451 -0
- package/dist/chunk-2GKPENIR.js +66 -0
- package/dist/{chunk-VXEOHTSL.js → chunk-2JQ3O2YL.js} +1 -1
- package/dist/{chunk-VR5NE7PZ.js → chunk-2RAZ4ZFE.js} +1 -1
- package/dist/chunk-2ZDO52B4.js +52 -0
- package/dist/{chunk-ZZA73MFY.js → chunk-33DOSHTA.js} +176 -36
- package/dist/chunk-4BQTQMJP.js +93 -0
- package/dist/{chunk-MAKNAHAW.js → chunk-5PJ4STIC.js} +98 -8
- package/dist/{chunk-RVYA52PY.js → chunk-5UM4PMMM.js} +1 -1
- package/dist/{chunk-4VQTUVH7.js → chunk-7YZWHM36.js} +52 -26
- package/dist/{chunk-4VRIMU4O.js → chunk-A4EAUO7T.js} +5 -5
- package/dist/{chunk-R6SXNSFD.js → chunk-BV5KWZKR.js} +3 -3
- package/dist/chunk-FBITHIZF.js +351 -0
- package/dist/{chunk-Q2J5YTUF.js → chunk-FUSLEY6L.js} +751 -34
- package/dist/chunk-GNJL4YGR.js +79 -0
- package/dist/{chunk-42MXU7A6.js → chunk-K4GFGKFD.js} +51 -47
- package/dist/{chunk-PBEE567J.js → chunk-KSZROBFH.js} +2 -2
- package/dist/chunk-L4HSSQ6T.js +152 -0
- package/dist/{chunk-PZ2AUU2W.js → chunk-LMKQ7NIF.js} +206 -37
- package/dist/{chunk-6546Q4OR.js → chunk-M5O6FQ66.js} +6 -6
- package/dist/chunk-MM6QGW3P.js +207 -0
- package/dist/{chunk-T76H47ZS.js → chunk-MNPUYCHQ.js} +1 -1
- package/dist/{chunk-P5EPF6MB.js → chunk-MW5C6ZQA.js} +110 -13
- package/dist/{chunk-OZ7RIXTO.js → chunk-QSRRMEYM.js} +2 -2
- package/dist/chunk-RHISK3SZ.js +189 -0
- package/dist/{chunk-3BTHWPMB.js → chunk-S5OJEGFG.js} +2 -2
- package/dist/{chunk-MGDEINGP.js → chunk-SS4B7P7V.js} +1 -1
- package/dist/{chunk-ME37YNW3.js → chunk-SV7T4HRE.js} +4 -4
- package/dist/{chunk-IEVLHNLU.js → chunk-T3FKSZSN.js} +3 -3
- package/dist/{chunk-DTEHFAL7.js → chunk-TS6NDVOU.js} +2 -2
- package/dist/chunk-U4O6C46S.js +154 -0
- package/dist/{chunk-ITPEXLHA.js → chunk-URXDAUVH.js} +24 -5
- package/dist/chunk-WMGIIABP.js +15 -0
- package/dist/{chunk-QVMXF7FY.js → chunk-X3SPPUFG.js} +50 -0
- package/dist/{chunk-THRJVD4L.js → chunk-Y6VJKXGL.js} +1 -1
- package/dist/{chunk-RCBMXTWS.js → chunk-YD7SVXTF.js} +39 -7
- package/dist/{chunk-HIHOUSXS.js → chunk-YXQCA6B7.js} +105 -1
- package/dist/cli/index.js +20 -18
- package/dist/commands/archive.js +3 -2
- package/dist/commands/backlog.js +1 -0
- package/dist/commands/blocked.js +1 -0
- package/dist/commands/canvas.js +2 -1
- package/dist/commands/checkpoint.js +1 -0
- package/dist/commands/compat.js +2 -1
- package/dist/commands/context.js +6 -4
- package/dist/commands/doctor.d.ts +10 -1
- package/dist/commands/doctor.js +13 -10
- package/dist/commands/embed.js +5 -3
- package/dist/commands/entities.js +2 -1
- package/dist/commands/graph.js +4 -3
- package/dist/commands/inject.d.ts +1 -1
- package/dist/commands/inject.js +5 -4
- package/dist/commands/kanban.js +1 -0
- package/dist/commands/link.js +5 -4
- package/dist/commands/migrate-observations.js +3 -2
- package/dist/commands/observe.js +9 -7
- package/dist/commands/project.js +1 -0
- package/dist/commands/rebuild-embeddings.d.ts +21 -0
- package/dist/commands/rebuild-embeddings.js +91 -0
- package/dist/commands/rebuild.js +6 -4
- package/dist/commands/recover.js +1 -0
- package/dist/commands/reflect.js +5 -4
- package/dist/commands/repair-session.js +1 -0
- package/dist/commands/replay.js +7 -6
- package/dist/commands/session-recap.js +1 -0
- package/dist/commands/setup.js +3 -2
- package/dist/commands/shell-init.js +2 -0
- package/dist/commands/sleep.d.ts +1 -1
- package/dist/commands/sleep.js +10 -8
- package/dist/commands/status.js +13 -82
- package/dist/commands/sync-bd.js +3 -2
- package/dist/commands/tailscale.js +3 -2
- package/dist/commands/task.js +1 -0
- package/dist/commands/template.js +1 -0
- package/dist/commands/wake.d.ts +1 -1
- package/dist/commands/wake.js +5 -3
- package/dist/index.d.ts +254 -10
- package/dist/index.js +288 -155
- package/dist/{inject-x65KXWPk.d.ts → inject-DYUrDqQO.d.ts} +2 -2
- package/dist/ledger-B7g7jhqG.d.ts +44 -0
- package/dist/lib/auto-linker.js +2 -1
- package/dist/lib/canvas-layout.js +1 -0
- package/dist/lib/config.d.ts +27 -3
- package/dist/lib/config.js +4 -1
- package/dist/lib/entity-index.js +1 -0
- package/dist/lib/project-utils.js +1 -0
- package/dist/lib/session-repair.js +1 -0
- package/dist/lib/session-utils.js +1 -0
- package/dist/lib/tailscale.js +1 -0
- package/dist/lib/task-utils.js +1 -0
- package/dist/lib/template-engine.js +1 -0
- package/dist/lib/webdav.js +1 -0
- package/dist/onnxruntime_binding-5QEF3SUC.node +0 -0
- package/dist/onnxruntime_binding-BKPKNEGC.node +0 -0
- package/dist/onnxruntime_binding-FMOXGIUT.node +0 -0
- package/dist/onnxruntime_binding-OI2KMXC5.node +0 -0
- package/dist/onnxruntime_binding-UX44MLAZ.node +0 -0
- package/dist/onnxruntime_binding-Y2W7N7WY.node +0 -0
- package/dist/registry-BR4326o0.d.ts +30 -0
- package/dist/store-CA-6sKCJ.d.ts +34 -0
- package/dist/thread-B9LhXNU0.d.ts +41 -0
- package/dist/transformers.node-A2ZRORSQ.js +46775 -0
- package/dist/{types-C74wgGL1.d.ts → types-BbWJoC1c.d.ts} +1 -1
- package/dist/workgraph/index.d.ts +5 -0
- package/dist/workgraph/index.js +23 -0
- package/dist/workgraph/ledger.d.ts +2 -0
- package/dist/workgraph/ledger.js +25 -0
- package/dist/workgraph/registry.d.ts +2 -0
- package/dist/workgraph/registry.js +19 -0
- package/dist/workgraph/store.d.ts +2 -0
- package/dist/workgraph/store.js +25 -0
- package/dist/workgraph/thread.d.ts +2 -0
- package/dist/workgraph/thread.js +25 -0
- package/dist/workgraph/types.d.ts +54 -0
- package/dist/workgraph/types.js +7 -0
- package/hooks/clawvault/handler.js +714 -2
- package/hooks/clawvault/handler.test.js +153 -0
- package/hooks/clawvault/openclaw.plugin.json +72 -0
- package/openclaw.plugin.json +14 -2
- package/package.json +5 -4
- package/dist/chunk-4QYGFWRM.js +0 -88
- 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
|
-
|
|
467
|
-
|
|
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
|