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.
- package/README.md +352 -20
- 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-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-4BQTQMJP.js +93 -0
- package/dist/{chunk-MAKNAHAW.js → chunk-5PJ4STIC.js} +98 -8
- package/dist/{chunk-IEVLHNLU.js → chunk-627Q3QWK.js} +3 -3
- package/dist/{chunk-R6SXNSFD.js → chunk-6NYYDNNG.js} +3 -3
- package/dist/chunk-ECRZL5XR.js +50 -0
- package/dist/chunk-GNJL4YGR.js +79 -0
- package/dist/{chunk-OZ7RIXTO.js → chunk-IIOU45CK.js} +1 -1
- package/dist/chunk-L4HSSQ6T.js +152 -0
- package/dist/{chunk-XAVB4GB4.js → chunk-LIGHWOH6.js} +1 -1
- package/dist/{chunk-PBEE567J.js → chunk-LUBZXECN.js} +2 -2
- package/dist/{chunk-UEOUADMO.js → chunk-MFL6EEPF.js} +204 -35
- package/dist/chunk-MM6QGW3P.js +207 -0
- package/dist/{chunk-T76H47ZS.js → chunk-MNPUYCHQ.js} +1 -1
- package/dist/{chunk-TLGBDTYT.js → chunk-MPOSMDMU.js} +6 -6
- package/dist/{chunk-RVYA52PY.js → chunk-NJYJL5AA.js} +1 -1
- package/dist/{chunk-Q2J5YTUF.js → chunk-OQGYFZ4A.js} +669 -33
- package/dist/{chunk-ME37YNW3.js → chunk-P7SY3D4E.js} +3 -3
- 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-U4O6C46S.js +154 -0
- package/dist/{chunk-ITPEXLHA.js → chunk-URXDAUVH.js} +24 -5
- package/dist/chunk-WIOLLGAD.js +190 -0
- 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-KL4NAOMO.js → chunk-YDWHS4LJ.js} +49 -9
- package/dist/{chunk-4VRIMU4O.js → chunk-YNIPYN4F.js} +4 -4
- package/dist/{chunk-HIHOUSXS.js → chunk-YXQCA6B7.js} +105 -1
- package/dist/cli/index.js +18 -16
- 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 +1 -0
- package/dist/commands/checkpoint.js +1 -0
- package/dist/commands/compat.js +2 -1
- package/dist/commands/context.js +5 -3
- package/dist/commands/doctor.d.ts +10 -1
- package/dist/commands/doctor.js +11 -8
- package/dist/commands/embed.js +5 -3
- package/dist/commands/entities.js +2 -1
- package/dist/commands/graph.js +3 -2
- package/dist/commands/inject.d.ts +1 -1
- package/dist/commands/inject.js +4 -3
- package/dist/commands/kanban.js +1 -0
- package/dist/commands/link.js +2 -1
- package/dist/commands/migrate-observations.js +3 -2
- package/dist/commands/observe.js +8 -6
- 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 +8 -6
- package/dist/commands/status.d.ts +2 -0
- package/dist/commands/status.js +35 -24
- 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 +4 -2
- package/dist/index.d.ts +333 -10
- package/dist/index.js +320 -33
- 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 +1 -0
- 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/HOOK.md +34 -4
- package/hooks/clawvault/handler.js +751 -8
- package/hooks/clawvault/handler.test.js +247 -0
- package/hooks/clawvault/openclaw.plugin.json +72 -0
- package/openclaw.plugin.json +84 -0
- package/package.json +8 -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) {
|
|
@@ -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
|
-
|
|
449
|
-
|
|
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
|