clawvault 3.0.0 → 3.2.0
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-registration.test.js +3 -1
- package/bin/command-runtime.js +9 -1
- package/bin/register-core-commands.js +23 -10
- package/bin/register-maintenance-commands.js +39 -3
- package/bin/register-query-commands.js +58 -29
- package/bin/register-task-commands.js +18 -1
- package/bin/register-task-commands.test.js +16 -0
- package/bin/register-vault-operations-commands.js +29 -1
- package/bin/register-workgraph-commands.js +1368 -0
- package/dashboard/lib/graph-diff.js +104 -0
- package/dashboard/lib/graph-diff.test.js +75 -0
- package/dashboard/lib/vault-parser.js +556 -0
- package/dashboard/lib/vault-parser.test.js +254 -0
- package/dashboard/public/app.js +796 -0
- package/dashboard/public/index.html +52 -0
- package/dashboard/public/styles.css +221 -0
- package/dashboard/server.js +374 -0
- package/dist/{chunk-F2JEUD4J.js → chunk-23YDQ3QU.js} +6 -8
- package/dist/{chunk-C7OK5WKP.js → chunk-2JQ3O2YL.js} +4 -4
- 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-33VSQP4J.js +37 -0
- package/dist/chunk-4BQTQMJP.js +93 -0
- package/dist/{chunk-GUKMRGM7.js → chunk-4OXMU5S2.js} +1 -1
- package/dist/{chunk-62YTUT6J.js → chunk-4PY655YM.js} +15 -3
- package/dist/chunk-6FH3IULF.js +352 -0
- package/dist/{chunk-3NSBOUT3.js → chunk-77Q5CSPJ.js} +404 -80
- package/dist/{chunk-4VQTUVH7.js → chunk-7YZWHM36.js} +52 -26
- package/dist/chunk-BSJ6RIT7.js +447 -0
- package/dist/chunk-BUEW6IIK.js +364 -0
- package/dist/{chunk-WGRQ6HDV.js → chunk-CLJTREDS.js} +74 -14
- package/dist/chunk-EK6S23ZB.js +469 -0
- package/dist/{chunk-LNJA2UGL.js → chunk-ESFLMDRB.js} +9 -86
- package/dist/{chunk-H34S76MB.js → chunk-ESVS6K2B.js} +6 -6
- package/dist/{chunk-WAZ3NLWL.js → chunk-F55HGNU4.js} +0 -47
- package/dist/{chunk-QK3UCXWL.js → chunk-FHFUXL6G.js} +2 -2
- package/dist/{chunk-YKTA5JOJ.js → chunk-GAOWA7GR.js} +212 -46
- package/dist/chunk-GGA32J2R.js +784 -0
- package/dist/chunk-GNJL4YGR.js +79 -0
- package/dist/chunk-MDIH26GC.js +183 -0
- package/dist/{chunk-LYHGEHXG.js → chunk-MFAWT5O5.js} +0 -1
- package/dist/chunk-MM6QGW3P.js +207 -0
- package/dist/{chunk-P5EPF6MB.js → chunk-MW5C6ZQA.js} +110 -13
- package/dist/chunk-NCKFNBHJ.js +257 -0
- package/dist/{chunk-QBLMXKF2.js → chunk-OIWVQYQF.js} +1 -1
- package/dist/{chunk-42MXU7A6.js → chunk-P62WHA27.js} +58 -47
- package/dist/chunk-PBACDKKP.js +66 -0
- package/dist/{chunk-VGLOTGAS.js → chunk-QSHD36LH.js} +2 -2
- package/dist/{chunk-OZ7RIXTO.js → chunk-QSRRMEYM.js} +2 -2
- package/dist/chunk-QVEERJSP.js +152 -0
- package/dist/{chunk-N2AXRYLC.js → chunk-QWQ3TIKS.js} +1 -1
- package/dist/{chunk-3DHXQHYG.js → chunk-R2MIW5G7.js} +1 -1
- package/dist/{chunk-SJSFRIYS.js → chunk-SLXOR3CC.js} +2 -2
- package/dist/chunk-SS4B7P7V.js +99 -0
- package/dist/{chunk-JY6FYXIT.js → chunk-STCQGCEQ.js} +6 -11
- package/dist/chunk-U4O6C46S.js +154 -0
- package/dist/{chunk-ITPEXLHA.js → chunk-URXDAUVH.js} +24 -5
- package/dist/chunk-VSL7KY3M.js +189 -0
- package/dist/{chunk-U55BGUAU.js → chunk-W4SPAEE7.js} +6 -6
- package/dist/chunk-WMGIIABP.js +15 -0
- package/dist/{chunk-3D6BCTP6.js → chunk-X3SPPUFG.js} +51 -39
- package/dist/{chunk-THRJVD4L.js → chunk-Y6VJKXGL.js} +1 -1
- package/dist/{chunk-ZVVFWOLW.js → chunk-ZN54U2OZ.js} +123 -10
- package/dist/cli/index.js +32 -25
- package/dist/commands/archive.js +3 -3
- package/dist/commands/backlog.js +3 -3
- package/dist/commands/blocked.js +3 -3
- package/dist/commands/canvas.d.ts +15 -0
- package/dist/commands/canvas.js +200 -0
- package/dist/commands/checkpoint.js +2 -2
- package/dist/commands/compat.js +2 -2
- package/dist/commands/context.js +8 -6
- package/dist/commands/doctor.d.ts +11 -7
- package/dist/commands/doctor.js +18 -16
- package/dist/commands/embed.js +5 -6
- package/dist/commands/entities.js +2 -2
- package/dist/commands/graph.js +4 -4
- package/dist/commands/inject.d.ts +1 -1
- package/dist/commands/inject.js +5 -6
- package/dist/commands/kanban.js +4 -4
- package/dist/commands/link.js +5 -5
- package/dist/commands/migrate-observations.js +4 -4
- package/dist/commands/observe.d.ts +0 -1
- package/dist/commands/observe.js +14 -13
- package/dist/commands/project.js +5 -5
- package/dist/commands/rebuild-embeddings.d.ts +21 -0
- package/dist/commands/rebuild-embeddings.js +91 -0
- package/dist/commands/rebuild.js +12 -11
- package/dist/commands/recover.js +3 -3
- package/dist/commands/reflect.js +6 -7
- package/dist/commands/repair-session.js +1 -1
- package/dist/commands/replay.js +14 -14
- package/dist/commands/session-recap.js +1 -1
- package/dist/commands/setup.d.ts +2 -90
- package/dist/commands/setup.js +3 -21
- package/dist/commands/shell-init.js +1 -1
- package/dist/commands/sleep.d.ts +1 -1
- package/dist/commands/sleep.js +20 -19
- package/dist/commands/status.d.ts +2 -0
- package/dist/commands/status.js +57 -35
- package/dist/commands/sync-bd.d.ts +10 -0
- package/dist/commands/sync-bd.js +10 -0
- package/dist/commands/tailscale.js +3 -3
- package/dist/commands/task.js +4 -4
- package/dist/commands/template.js +2 -2
- package/dist/commands/wake.d.ts +1 -1
- package/dist/commands/wake.js +11 -10
- package/dist/commands/workgraph.d.ts +124 -0
- package/dist/commands/workgraph.js +38 -0
- package/dist/index.d.ts +337 -191
- package/dist/index.js +387 -118
- package/dist/{inject-Bzi5E-By.d.cts → inject-DYUrDqQO.d.ts} +3 -3
- package/dist/ledger-B7g7jhqG.d.ts +44 -0
- package/dist/lib/auto-linker.js +2 -2
- package/dist/lib/canvas-layout.d.ts +100 -16
- package/dist/lib/canvas-layout.js +21 -78
- package/dist/lib/config.d.ts +27 -3
- package/dist/lib/config.js +4 -2
- package/dist/lib/entity-index.js +1 -1
- package/dist/lib/project-utils.js +4 -4
- package/dist/lib/session-repair.js +1 -1
- package/dist/lib/session-utils.js +1 -1
- package/dist/lib/tailscale.js +1 -1
- package/dist/lib/task-utils.js +3 -3
- package/dist/lib/template-engine.js +1 -1
- package/dist/lib/webdav.js +1 -1
- 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/openclaw-plugin.d.ts +8 -0
- package/dist/openclaw-plugin.js +14 -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-Y2_Um2Ls.d.cts → types-BbWJoC1c.d.ts} +1 -44
- 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 +760 -78
- package/hooks/clawvault/handler.test.js +235 -79
- package/hooks/clawvault/openclaw.plugin.json +72 -0
- package/openclaw.plugin.json +65 -38
- package/package.json +15 -18
- package/dist/chunk-3RG5ZIWI.js +0 -10
- package/dist/chunk-6U6MK36V.js +0 -205
- package/dist/chunk-7R7O6STJ.js +0 -88
- package/dist/chunk-CMB7UL7C.js +0 -327
- package/dist/chunk-DEFFDRVP.js +0 -938
- package/dist/chunk-E7MFQB6D.js +0 -163
- package/dist/chunk-GAJV4IGR.js +0 -82
- package/dist/chunk-GQSLDZTS.js +0 -560
- package/dist/chunk-K234IDRJ.js +0 -1073
- package/dist/chunk-MFM6K7PU.js +0 -374
- package/dist/chunk-MXSSG3QU.js +0 -42
- package/dist/chunk-PAH27GSN.js +0 -108
- package/dist/cli/index.cjs +0 -10033
- package/dist/cli/index.d.cts +0 -5
- package/dist/commands/archive.cjs +0 -287
- package/dist/commands/archive.d.cts +0 -11
- package/dist/commands/backlog.cjs +0 -721
- package/dist/commands/backlog.d.cts +0 -53
- package/dist/commands/blocked.cjs +0 -204
- package/dist/commands/blocked.d.cts +0 -26
- package/dist/commands/checkpoint.cjs +0 -244
- package/dist/commands/checkpoint.d.cts +0 -41
- package/dist/commands/compat.cjs +0 -369
- package/dist/commands/compat.d.cts +0 -28
- package/dist/commands/context.cjs +0 -2989
- package/dist/commands/context.d.cts +0 -2
- package/dist/commands/doctor.cjs +0 -3062
- package/dist/commands/doctor.d.cts +0 -21
- package/dist/commands/embed.cjs +0 -232
- package/dist/commands/embed.d.cts +0 -17
- package/dist/commands/entities.cjs +0 -141
- package/dist/commands/entities.d.cts +0 -7
- package/dist/commands/graph.cjs +0 -501
- package/dist/commands/graph.d.cts +0 -21
- package/dist/commands/inject.cjs +0 -1636
- package/dist/commands/inject.d.cts +0 -2
- package/dist/commands/kanban.cjs +0 -884
- package/dist/commands/kanban.d.cts +0 -63
- package/dist/commands/link.cjs +0 -965
- package/dist/commands/link.d.cts +0 -11
- package/dist/commands/migrate-observations.cjs +0 -362
- package/dist/commands/migrate-observations.d.cts +0 -19
- package/dist/commands/observe.cjs +0 -4099
- package/dist/commands/observe.d.cts +0 -23
- package/dist/commands/project.cjs +0 -1341
- package/dist/commands/project.d.cts +0 -85
- package/dist/commands/rebuild.cjs +0 -3136
- package/dist/commands/rebuild.d.cts +0 -11
- package/dist/commands/recover.cjs +0 -361
- package/dist/commands/recover.d.cts +0 -38
- package/dist/commands/reflect.cjs +0 -1008
- package/dist/commands/reflect.d.cts +0 -11
- package/dist/commands/repair-session.cjs +0 -457
- package/dist/commands/repair-session.d.cts +0 -38
- package/dist/commands/replay.cjs +0 -4103
- package/dist/commands/replay.d.cts +0 -16
- package/dist/commands/session-recap.cjs +0 -353
- package/dist/commands/session-recap.d.cts +0 -27
- package/dist/commands/setup.cjs +0 -1345
- package/dist/commands/setup.d.cts +0 -100
- package/dist/commands/shell-init.cjs +0 -75
- package/dist/commands/shell-init.d.cts +0 -7
- package/dist/commands/sleep.cjs +0 -6028
- package/dist/commands/sleep.d.cts +0 -36
- package/dist/commands/status.cjs +0 -2736
- package/dist/commands/status.d.cts +0 -52
- package/dist/commands/tailscale.cjs +0 -1532
- package/dist/commands/tailscale.d.cts +0 -52
- package/dist/commands/task.cjs +0 -1236
- package/dist/commands/task.d.cts +0 -97
- package/dist/commands/template.cjs +0 -457
- package/dist/commands/template.d.cts +0 -36
- package/dist/commands/wake.cjs +0 -2626
- package/dist/commands/wake.d.cts +0 -22
- package/dist/context-BUGaWpyL.d.cts +0 -46
- package/dist/index.cjs +0 -14526
- package/dist/index.d.cts +0 -858
- package/dist/inject-Bzi5E-By.d.ts +0 -137
- package/dist/lib/auto-linker.cjs +0 -176
- package/dist/lib/auto-linker.d.cts +0 -26
- package/dist/lib/canvas-layout.cjs +0 -136
- package/dist/lib/canvas-layout.d.cts +0 -31
- package/dist/lib/config.cjs +0 -78
- package/dist/lib/config.d.cts +0 -11
- package/dist/lib/entity-index.cjs +0 -84
- package/dist/lib/entity-index.d.cts +0 -26
- package/dist/lib/project-utils.cjs +0 -864
- package/dist/lib/project-utils.d.cts +0 -97
- package/dist/lib/session-repair.cjs +0 -239
- package/dist/lib/session-repair.d.cts +0 -110
- package/dist/lib/session-utils.cjs +0 -209
- package/dist/lib/session-utils.d.cts +0 -63
- package/dist/lib/tailscale.cjs +0 -1183
- package/dist/lib/tailscale.d.cts +0 -225
- package/dist/lib/task-utils.cjs +0 -1137
- package/dist/lib/task-utils.d.cts +0 -208
- package/dist/lib/template-engine.cjs +0 -47
- package/dist/lib/template-engine.d.cts +0 -11
- package/dist/lib/webdav.cjs +0 -568
- package/dist/lib/webdav.d.cts +0 -109
- package/dist/plugin/index.cjs +0 -1907
- package/dist/plugin/index.d.cts +0 -36
- package/dist/plugin/index.d.ts +0 -36
- package/dist/plugin/index.js +0 -572
- package/dist/plugin/inject.cjs +0 -356
- package/dist/plugin/inject.d.cts +0 -54
- package/dist/plugin/inject.d.ts +0 -54
- package/dist/plugin/inject.js +0 -17
- package/dist/plugin/observe.cjs +0 -631
- package/dist/plugin/observe.d.cts +0 -39
- package/dist/plugin/observe.d.ts +0 -39
- package/dist/plugin/observe.js +0 -18
- package/dist/plugin/templates.cjs +0 -593
- package/dist/plugin/templates.d.cts +0 -52
- package/dist/plugin/templates.d.ts +0 -52
- package/dist/plugin/templates.js +0 -25
- package/dist/plugin/types.cjs +0 -18
- package/dist/plugin/types.d.cts +0 -209
- package/dist/plugin/types.d.ts +0 -209
- package/dist/plugin/types.js +0 -0
- package/dist/plugin/vault.cjs +0 -927
- package/dist/plugin/vault.d.cts +0 -68
- package/dist/plugin/vault.d.ts +0 -68
- package/dist/plugin/vault.js +0 -22
- package/dist/types-Y2_Um2Ls.d.ts +0 -205
- package/templates/memory-event.md +0 -67
- package/templates/party.md +0 -63
- package/templates/primitive-registry.yaml +0 -551
- package/templates/run.md +0 -68
- package/templates/trigger.md +0 -68
- package/templates/workspace.md +0 -50
|
@@ -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';
|
|
@@ -21,13 +22,20 @@ const MAX_CONTEXT_PROMPT_LENGTH = 500;
|
|
|
21
22
|
const MAX_CONTEXT_SNIPPET_LENGTH = 220;
|
|
22
23
|
const MAX_RECAP_RESULTS = 6;
|
|
23
24
|
const MAX_RECAP_SNIPPET_LENGTH = 220;
|
|
24
|
-
const EVENT_NAME_SEPARATOR_RE = /[
|
|
25
|
+
const EVENT_NAME_SEPARATOR_RE = /[.:/]/g;
|
|
25
26
|
const OBSERVE_CURSOR_FILE = 'observe-cursors.json';
|
|
26
27
|
const ONE_KIB = 1024;
|
|
27
28
|
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) {
|
|
@@ -178,7 +186,7 @@ function shouldObserveActiveSessions(vaultPath, agentId) {
|
|
|
178
186
|
const sessionId = typeof value.sessionId === 'string' ? value.sessionId.trim() : '';
|
|
179
187
|
if (!/^[a-zA-Z0-9._-]{1,200}$/.test(sessionId)) continue;
|
|
180
188
|
|
|
181
|
-
|
|
189
|
+
const filePath = path.join(sessionsDir, `${sessionId}.jsonl`);
|
|
182
190
|
let stat;
|
|
183
191
|
try {
|
|
184
192
|
stat = fs.statSync(filePath);
|
|
@@ -187,27 +195,6 @@ function shouldObserveActiveSessions(vaultPath, agentId) {
|
|
|
187
195
|
}
|
|
188
196
|
if (!stat.isFile()) continue;
|
|
189
197
|
|
|
190
|
-
// After /new or /reset, the main .jsonl is emptied — fall back to reset file
|
|
191
|
-
if (stat.size < 100) {
|
|
192
|
-
const resetPrefix = `${sessionId}.jsonl.reset.`;
|
|
193
|
-
try {
|
|
194
|
-
const resetFiles = fs.readdirSync(sessionsDir)
|
|
195
|
-
.filter(f => f.startsWith(resetPrefix))
|
|
196
|
-
.sort()
|
|
197
|
-
.reverse();
|
|
198
|
-
if (resetFiles.length > 0) {
|
|
199
|
-
const resetPath = path.join(sessionsDir, resetFiles[0]);
|
|
200
|
-
const resetStat = fs.statSync(resetPath);
|
|
201
|
-
if (resetStat.isFile() && resetStat.size > stat.size) {
|
|
202
|
-
filePath = resetPath;
|
|
203
|
-
stat = resetStat;
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
} catch {
|
|
207
|
-
// Ignore — use original file
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
|
|
211
198
|
const fileSize = stat.size;
|
|
212
199
|
const cursorEntry = cursors[sessionId];
|
|
213
200
|
const previousOffset = Number.isFinite(cursorEntry?.lastObservedOffset)
|
|
@@ -387,28 +374,16 @@ function normalizeEventToken(value) {
|
|
|
387
374
|
.trim()
|
|
388
375
|
.toLowerCase()
|
|
389
376
|
.replace(/\s+/g, '')
|
|
390
|
-
.replace(EVENT_NAME_SEPARATOR_RE, ':')
|
|
391
|
-
.replace(/^:+|:+$/g, '');
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
function collapseEventToken(value) {
|
|
395
|
-
return normalizeEventToken(value).replace(/:/g, '');
|
|
377
|
+
.replace(EVENT_NAME_SEPARATOR_RE, ':');
|
|
396
378
|
}
|
|
397
379
|
|
|
398
380
|
function eventMatches(event, type, action) {
|
|
399
|
-
const normalizedExpected =
|
|
400
|
-
.filter(Boolean)
|
|
401
|
-
.join(':');
|
|
402
|
-
const collapsedExpected = collapseEventToken(normalizedExpected);
|
|
381
|
+
const normalizedExpected = `${normalizeEventToken(type)}:${normalizeEventToken(action)}`;
|
|
403
382
|
const normalizedType = normalizeEventToken(event?.type);
|
|
404
383
|
const normalizedAction = normalizeEventToken(event?.action);
|
|
405
384
|
|
|
406
385
|
if (normalizedType && normalizedAction) {
|
|
407
|
-
|
|
408
|
-
if (
|
|
409
|
-
normalizedEventPair === normalizedExpected
|
|
410
|
-
|| collapseEventToken(normalizedEventPair) === collapsedExpected
|
|
411
|
-
) {
|
|
386
|
+
if (`${normalizedType}:${normalizedAction}` === normalizedExpected) {
|
|
412
387
|
return true;
|
|
413
388
|
}
|
|
414
389
|
}
|
|
@@ -424,10 +399,7 @@ function eventMatches(event, type, action) {
|
|
|
424
399
|
for (const alias of aliases) {
|
|
425
400
|
const normalizedAlias = normalizeEventToken(alias);
|
|
426
401
|
if (!normalizedAlias) continue;
|
|
427
|
-
if (
|
|
428
|
-
normalizedAlias === normalizedExpected
|
|
429
|
-
|| collapseEventToken(normalizedAlias) === collapsedExpected
|
|
430
|
-
) {
|
|
402
|
+
if (normalizedAlias === normalizedExpected) {
|
|
431
403
|
return true;
|
|
432
404
|
}
|
|
433
405
|
}
|
|
@@ -438,7 +410,6 @@ function eventMatches(event, type, action) {
|
|
|
438
410
|
function eventIncludesToken(event, token) {
|
|
439
411
|
const normalizedToken = normalizeEventToken(token);
|
|
440
412
|
if (!normalizedToken) return false;
|
|
441
|
-
const collapsedToken = collapseEventToken(normalizedToken);
|
|
442
413
|
|
|
443
414
|
const values = [
|
|
444
415
|
event?.type,
|
|
@@ -453,12 +424,7 @@ function eventIncludesToken(event, token) {
|
|
|
453
424
|
return values
|
|
454
425
|
.map((value) => normalizeEventToken(value))
|
|
455
426
|
.filter(Boolean)
|
|
456
|
-
.some((value) =>
|
|
457
|
-
if (value.includes(normalizedToken)) {
|
|
458
|
-
return true;
|
|
459
|
-
}
|
|
460
|
-
return collapseEventToken(value).includes(collapsedToken);
|
|
461
|
-
});
|
|
427
|
+
.some((value) => value.includes(normalizedToken));
|
|
462
428
|
}
|
|
463
429
|
|
|
464
430
|
// Validate vault path - must be absolute and exist
|
|
@@ -486,21 +452,75 @@ function validateVaultPath(vaultPath) {
|
|
|
486
452
|
return resolved;
|
|
487
453
|
}
|
|
488
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?.entries?.clawvault?.config,
|
|
461
|
+
event?.context?.config?.plugins?.entries?.clawvault?.config,
|
|
462
|
+
event?.config?.plugins?.clawvault?.config,
|
|
463
|
+
event?.context?.config?.plugins?.clawvault?.config
|
|
464
|
+
];
|
|
465
|
+
|
|
466
|
+
for (const candidate of candidates) {
|
|
467
|
+
if (candidate && typeof candidate === 'object' && !Array.isArray(candidate)) {
|
|
468
|
+
return candidate;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
return {};
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// Resolve vault path for a specific agent from agentVaults config
|
|
476
|
+
function resolveAgentVaultPath(pluginConfig, agentId) {
|
|
477
|
+
if (!agentId || typeof agentId !== 'string') return null;
|
|
478
|
+
|
|
479
|
+
const agentVaults = pluginConfig?.agentVaults;
|
|
480
|
+
if (!agentVaults || typeof agentVaults !== 'object' || Array.isArray(agentVaults)) {
|
|
481
|
+
return null;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
const agentPath = agentVaults[agentId];
|
|
485
|
+
if (!agentPath || typeof agentPath !== 'string') return null;
|
|
486
|
+
|
|
487
|
+
return validateVaultPath(agentPath);
|
|
488
|
+
}
|
|
489
|
+
|
|
489
490
|
// Find vault by walking up directories
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
491
|
+
// Supports per-agent vault paths via agentVaults config
|
|
492
|
+
function findVaultPath(event, options = {}) {
|
|
493
|
+
const pluginConfig = extractPluginConfig(event);
|
|
494
|
+
|
|
495
|
+
// Determine agent ID for per-agent vault resolution
|
|
496
|
+
const agentId = options.agentId || resolveAgentIdForEvent(event);
|
|
497
|
+
|
|
498
|
+
// Check agentVaults first (per-agent vault paths)
|
|
499
|
+
if (agentId) {
|
|
500
|
+
const agentVaultPath = resolveAgentVaultPath(pluginConfig, agentId);
|
|
501
|
+
if (agentVaultPath) {
|
|
502
|
+
console.log(`[clawvault] Using per-agent vault for ${agentId}: ${agentVaultPath}`);
|
|
503
|
+
return agentVaultPath;
|
|
504
|
+
}
|
|
494
505
|
}
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
506
|
+
|
|
507
|
+
// Check plugin config vaultPath (fallback for all agents)
|
|
508
|
+
if (pluginConfig.vaultPath) {
|
|
509
|
+
const validated = validateVaultPath(pluginConfig.vaultPath);
|
|
510
|
+
if (validated) return validated;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// Check OPENCLAW_PLUGIN_CLAWVAULT_VAULTPATH env (injected by OpenClaw from plugin config)
|
|
514
|
+
if (process.env.OPENCLAW_PLUGIN_CLAWVAULT_VAULTPATH) {
|
|
515
|
+
const validated = validateVaultPath(process.env.OPENCLAW_PLUGIN_CLAWVAULT_VAULTPATH);
|
|
501
516
|
if (validated) return validated;
|
|
502
517
|
}
|
|
503
518
|
|
|
519
|
+
// Check CLAWVAULT_PATH env
|
|
520
|
+
if (process.env.CLAWVAULT_PATH) {
|
|
521
|
+
return validateVaultPath(process.env.CLAWVAULT_PATH);
|
|
522
|
+
}
|
|
523
|
+
|
|
504
524
|
// Walk up from cwd
|
|
505
525
|
let dir = process.cwd();
|
|
506
526
|
const root = path.parse(dir).root;
|
|
@@ -516,19 +536,6 @@ function findVaultPath() {
|
|
|
516
536
|
|
|
517
537
|
dir = path.dirname(dir);
|
|
518
538
|
}
|
|
519
|
-
|
|
520
|
-
// Canonical local-first defaults for OpenClaw users.
|
|
521
|
-
const homeDir = normalizeAbsoluteEnvPath(process.env.HOME) || os.homedir();
|
|
522
|
-
const homeCandidates = [
|
|
523
|
-
path.join(homeDir, 'memory'),
|
|
524
|
-
path.join(homeDir, 'Memory'),
|
|
525
|
-
path.join(homeDir, 'vault'),
|
|
526
|
-
path.join(homeDir, 'Vault')
|
|
527
|
-
];
|
|
528
|
-
for (const candidate of homeCandidates) {
|
|
529
|
-
const validated = validateVaultPath(candidate);
|
|
530
|
-
if (validated) return validated;
|
|
531
|
-
}
|
|
532
539
|
|
|
533
540
|
return null;
|
|
534
541
|
}
|
|
@@ -611,6 +618,679 @@ function runObserverCron(vaultPath, agentId, options = {}) {
|
|
|
611
618
|
return true;
|
|
612
619
|
}
|
|
613
620
|
|
|
621
|
+
function ensureClawvaultDir(vaultPath) {
|
|
622
|
+
const dir = path.join(vaultPath, '.clawvault');
|
|
623
|
+
if (!fs.existsSync(dir)) {
|
|
624
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
625
|
+
}
|
|
626
|
+
return dir;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
function getFactsFilePath(vaultPath) {
|
|
630
|
+
return path.join(ensureClawvaultDir(vaultPath), FACTS_FILE);
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
function getEntityGraphFilePath(vaultPath) {
|
|
634
|
+
return path.join(ensureClawvaultDir(vaultPath), ENTITY_GRAPH_FILE);
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
function sanitizeFactText(value, maxLength = MAX_FACT_TEXT_LENGTH) {
|
|
638
|
+
if (typeof value !== 'string') return '';
|
|
639
|
+
return value
|
|
640
|
+
.replace(/[\x00-\x1f\x7f]/g, ' ')
|
|
641
|
+
.replace(/\s+/g, ' ')
|
|
642
|
+
.trim()
|
|
643
|
+
.slice(0, maxLength);
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
function normalizeEntityLabel(value) {
|
|
647
|
+
const cleaned = sanitizeFactText(value, 120).replace(/^[^a-zA-Z0-9]+|[^a-zA-Z0-9]+$/g, '');
|
|
648
|
+
if (!cleaned) return 'User';
|
|
649
|
+
if (/^(i|me|my|mine|we|us|our|ours)$/i.test(cleaned)) {
|
|
650
|
+
return 'User';
|
|
651
|
+
}
|
|
652
|
+
return cleaned;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
function normalizeEntityToken(value) {
|
|
656
|
+
const normalized = sanitizeFactText(value, 120)
|
|
657
|
+
.toLowerCase()
|
|
658
|
+
.replace(/[^a-z0-9]+/g, '_')
|
|
659
|
+
.replace(/^_+|_+$/g, '');
|
|
660
|
+
return normalized || 'user';
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
function normalizeFactValue(value) {
|
|
664
|
+
return sanitizeFactText(String(value ?? ''), 260)
|
|
665
|
+
.replace(/^[,:;\s-]+|[,:;\s-]+$/g, '')
|
|
666
|
+
.trim();
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
function normalizeFactRelation(value) {
|
|
670
|
+
if (typeof value !== 'string') return '';
|
|
671
|
+
return value
|
|
672
|
+
.trim()
|
|
673
|
+
.toLowerCase()
|
|
674
|
+
.replace(/[^a-z0-9_]+/g, '_')
|
|
675
|
+
.replace(/^_+|_+$/g, '');
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
function clampConfidence(value, fallback = 0.7) {
|
|
679
|
+
const numeric = Number(value);
|
|
680
|
+
if (!Number.isFinite(numeric)) return fallback;
|
|
681
|
+
if (numeric < 0) return 0;
|
|
682
|
+
if (numeric > 1) return 1;
|
|
683
|
+
return numeric;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
function toIsoTimestamp(value) {
|
|
687
|
+
const date = value instanceof Date ? value : new Date(value);
|
|
688
|
+
if (Number.isNaN(date.getTime())) {
|
|
689
|
+
return new Date().toISOString();
|
|
690
|
+
}
|
|
691
|
+
return date.toISOString();
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
function slugifyForId(value) {
|
|
695
|
+
const base = sanitizeFactText(String(value ?? ''), 180)
|
|
696
|
+
.toLowerCase()
|
|
697
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
698
|
+
.replace(/^-+|-+$/g, '');
|
|
699
|
+
if (!base) return 'unknown';
|
|
700
|
+
if (base.length <= 80) return base;
|
|
701
|
+
const hash = createHash('sha1').update(base).digest('hex').slice(0, 10);
|
|
702
|
+
return `${base.slice(0, 64)}-${hash}`;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
function isExclusiveFactRelation(relation) {
|
|
706
|
+
return EXCLUSIVE_FACT_RELATIONS.has(relation) || relation.startsWith('favorite_');
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
function createFactRecord({
|
|
710
|
+
entity,
|
|
711
|
+
relation,
|
|
712
|
+
value,
|
|
713
|
+
validFrom,
|
|
714
|
+
confidence,
|
|
715
|
+
category,
|
|
716
|
+
source,
|
|
717
|
+
rawText
|
|
718
|
+
}) {
|
|
719
|
+
const relationToken = normalizeFactRelation(relation);
|
|
720
|
+
const valueToken = normalizeFactValue(value);
|
|
721
|
+
if (!relationToken || !valueToken) return null;
|
|
722
|
+
|
|
723
|
+
const entityLabel = normalizeEntityLabel(entity || 'User');
|
|
724
|
+
const entityNorm = normalizeEntityToken(entityLabel);
|
|
725
|
+
const factSource = sanitizeFactText(source || 'hook');
|
|
726
|
+
const factRawText = sanitizeFactText(rawText || valueToken);
|
|
727
|
+
const categoryToken = sanitizeFactText(category || 'facts', 40).toLowerCase() || 'facts';
|
|
728
|
+
|
|
729
|
+
return {
|
|
730
|
+
id: randomUUID(),
|
|
731
|
+
entity: entityLabel,
|
|
732
|
+
entityNorm,
|
|
733
|
+
relation: relationToken,
|
|
734
|
+
value: valueToken,
|
|
735
|
+
validFrom: toIsoTimestamp(validFrom),
|
|
736
|
+
validUntil: null,
|
|
737
|
+
confidence: clampConfidence(confidence, 0.7),
|
|
738
|
+
category: categoryToken,
|
|
739
|
+
source: factSource,
|
|
740
|
+
rawText: factRawText
|
|
741
|
+
};
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
function appendPatternFacts(target, sentence, pattern, options = {}) {
|
|
745
|
+
pattern.lastIndex = 0;
|
|
746
|
+
let match;
|
|
747
|
+
|
|
748
|
+
while ((match = pattern.exec(sentence)) !== null) {
|
|
749
|
+
const relation = options.relation;
|
|
750
|
+
const category = options.category || 'facts';
|
|
751
|
+
const confidence = options.confidence ?? 0.7;
|
|
752
|
+
const value = typeof options.value === 'function' ? options.value(match) : match[2];
|
|
753
|
+
const entity = typeof options.entity === 'function'
|
|
754
|
+
? options.entity(match)
|
|
755
|
+
: options.entity || match[1] || 'User';
|
|
756
|
+
|
|
757
|
+
const record = createFactRecord({
|
|
758
|
+
entity,
|
|
759
|
+
relation,
|
|
760
|
+
value,
|
|
761
|
+
validFrom: options.validFrom,
|
|
762
|
+
confidence,
|
|
763
|
+
category,
|
|
764
|
+
source: options.source,
|
|
765
|
+
rawText: sentence
|
|
766
|
+
});
|
|
767
|
+
|
|
768
|
+
if (record) {
|
|
769
|
+
target.push(record);
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
function extractFactsFromSentence(sentence, options) {
|
|
775
|
+
const source = options.source || 'hook:event';
|
|
776
|
+
const validFrom = options.validFrom || new Date().toISOString();
|
|
777
|
+
const facts = [];
|
|
778
|
+
const subjectPattern = '([A-Za-z][a-z]+(?:\\s+[A-Za-z][a-z]+)?|i|we)';
|
|
779
|
+
|
|
780
|
+
appendPatternFacts(
|
|
781
|
+
facts,
|
|
782
|
+
sentence,
|
|
783
|
+
new RegExp(`\\b${subjectPattern}\\s+(?:really\\s+)?prefer(?:s|red|ring)?\\s+([^.;!?]+)`, 'gi'),
|
|
784
|
+
{ relation: 'favorite_preference', category: 'preferences', confidence: 0.86, source, validFrom }
|
|
785
|
+
);
|
|
786
|
+
|
|
787
|
+
appendPatternFacts(
|
|
788
|
+
facts,
|
|
789
|
+
sentence,
|
|
790
|
+
new RegExp(`\\b${subjectPattern}\\s+(?:really\\s+)?like(?:s|d)?\\s+([^.;!?]+)`, 'gi'),
|
|
791
|
+
{ relation: 'favorite_preference', category: 'preferences', confidence: 0.8, source, validFrom }
|
|
792
|
+
);
|
|
793
|
+
|
|
794
|
+
appendPatternFacts(
|
|
795
|
+
facts,
|
|
796
|
+
sentence,
|
|
797
|
+
new RegExp(`\\b${subjectPattern}\\s+(?:really\\s+)?(?:hate|dislike(?:s|d)?)\\s+([^.;!?]+)`, 'gi'),
|
|
798
|
+
{ relation: 'dislikes', category: 'preferences', confidence: 0.84, source, validFrom }
|
|
799
|
+
);
|
|
800
|
+
|
|
801
|
+
appendPatternFacts(
|
|
802
|
+
facts,
|
|
803
|
+
sentence,
|
|
804
|
+
new RegExp(`\\b${subjectPattern}\\s+(?:am|is|are)?\\s*allergic\\s+to\\s+([^.;!?]+)`, 'gi'),
|
|
805
|
+
{ relation: 'allergic_to', category: 'preferences', confidence: 0.92, source, validFrom }
|
|
806
|
+
);
|
|
807
|
+
|
|
808
|
+
appendPatternFacts(
|
|
809
|
+
facts,
|
|
810
|
+
sentence,
|
|
811
|
+
new RegExp(`\\b${subjectPattern}\\s+(?:work|works|working)\\s+at\\s+([^.;!?]+)`, 'gi'),
|
|
812
|
+
{ relation: 'works_at', category: 'facts', confidence: 0.92, source, validFrom }
|
|
813
|
+
);
|
|
814
|
+
|
|
815
|
+
appendPatternFacts(
|
|
816
|
+
facts,
|
|
817
|
+
sentence,
|
|
818
|
+
new RegExp(`\\b${subjectPattern}\\s+(?:live|lives|living)\\s+in\\s+([^.;!?]+)`, 'gi'),
|
|
819
|
+
{ relation: 'lives_in', category: 'facts', confidence: 0.9, source, validFrom }
|
|
820
|
+
);
|
|
821
|
+
|
|
822
|
+
appendPatternFacts(
|
|
823
|
+
facts,
|
|
824
|
+
sentence,
|
|
825
|
+
new RegExp(`\\b${subjectPattern}\\s+(?:am|is|are)\\s+(\\d{1,3})\\s*(?:years?\\s*old)?\\b`, 'gi'),
|
|
826
|
+
{
|
|
827
|
+
relation: 'age',
|
|
828
|
+
category: 'facts',
|
|
829
|
+
confidence: 0.92,
|
|
830
|
+
source,
|
|
831
|
+
validFrom,
|
|
832
|
+
value: (match) => match[2]
|
|
833
|
+
}
|
|
834
|
+
);
|
|
835
|
+
|
|
836
|
+
appendPatternFacts(
|
|
837
|
+
facts,
|
|
838
|
+
sentence,
|
|
839
|
+
new RegExp(`\\b${subjectPattern}\\s+bought\\s+([^.;!?]+)`, 'gi'),
|
|
840
|
+
{ relation: 'bought', category: 'facts', confidence: 0.86, source, validFrom }
|
|
841
|
+
);
|
|
842
|
+
|
|
843
|
+
appendPatternFacts(
|
|
844
|
+
facts,
|
|
845
|
+
sentence,
|
|
846
|
+
new RegExp(`\\b${subjectPattern}\\s+spent\\s+\\$?(\\d+(?:\\.\\d{1,2})?)(?:\\s*(?:usd|dollars?))?(?:\\s+on\\s+([^.;!?]+))?`, 'gi'),
|
|
847
|
+
{
|
|
848
|
+
relation: 'spent',
|
|
849
|
+
category: 'facts',
|
|
850
|
+
confidence: 0.9,
|
|
851
|
+
source,
|
|
852
|
+
validFrom,
|
|
853
|
+
value: (match) => {
|
|
854
|
+
const amount = match[2] ? `$${match[2]}` : '';
|
|
855
|
+
const onWhat = normalizeFactValue(match[3] || '');
|
|
856
|
+
return onWhat ? `${amount} on ${onWhat}` : amount;
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
);
|
|
860
|
+
|
|
861
|
+
appendPatternFacts(
|
|
862
|
+
facts,
|
|
863
|
+
sentence,
|
|
864
|
+
new RegExp(`\\b${subjectPattern}\\s+(?:decided|chose)\\s+(?:to\\s+|on\\s+)?([^.;!?]+)`, 'gi'),
|
|
865
|
+
{ relation: 'decided', category: 'decisions', confidence: 0.88, source, validFrom }
|
|
866
|
+
);
|
|
867
|
+
|
|
868
|
+
appendPatternFacts(
|
|
869
|
+
facts,
|
|
870
|
+
sentence,
|
|
871
|
+
/\bmy\s+partner\s+is\s+([A-Za-z][a-z]+(?:\s+[A-Za-z][a-z]+)*)\b/gi,
|
|
872
|
+
{ relation: 'partner_name', category: 'entities', confidence: 0.9, source, validFrom, entity: 'User', value: (match) => match[1] }
|
|
873
|
+
);
|
|
874
|
+
|
|
875
|
+
appendPatternFacts(
|
|
876
|
+
facts,
|
|
877
|
+
sentence,
|
|
878
|
+
/\b([A-Za-z][a-z]+(?:\s+[A-Za-z][a-z]+)*)\s+is\s+my\s+partner\b/gi,
|
|
879
|
+
{ relation: 'partner_name', category: 'entities', confidence: 0.9, source, validFrom, entity: 'User', value: (match) => match[1] }
|
|
880
|
+
);
|
|
881
|
+
|
|
882
|
+
appendPatternFacts(
|
|
883
|
+
facts,
|
|
884
|
+
sentence,
|
|
885
|
+
/\bmy\s+dog\s+is\s+([A-Za-z][a-z]+(?:\s+[A-Za-z][a-z]+)*)\b/gi,
|
|
886
|
+
{ relation: 'dog_name', category: 'entities', confidence: 0.9, source, validFrom, entity: 'User', value: (match) => match[1] }
|
|
887
|
+
);
|
|
888
|
+
|
|
889
|
+
appendPatternFacts(
|
|
890
|
+
facts,
|
|
891
|
+
sentence,
|
|
892
|
+
/\bmy\s+(?:mom|mother|dad|father|parent)\s+is\s+([A-Za-z][a-z]+(?:\s+[A-Za-z][a-z]+)*)\b/gi,
|
|
893
|
+
{ relation: 'parent_name', category: 'entities', confidence: 0.9, source, validFrom, entity: 'User', value: (match) => match[1] }
|
|
894
|
+
);
|
|
895
|
+
|
|
896
|
+
const deduped = [];
|
|
897
|
+
const seen = new Set();
|
|
898
|
+
for (const fact of facts) {
|
|
899
|
+
const dedupeKey = `${fact.entityNorm}|${fact.relation}|${normalizeFactValue(fact.value).toLowerCase()}`;
|
|
900
|
+
if (seen.has(dedupeKey)) continue;
|
|
901
|
+
seen.add(dedupeKey);
|
|
902
|
+
deduped.push(fact);
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
return deduped;
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
function splitObservedTextIntoSentences(text) {
|
|
909
|
+
return sanitizeFactText(text, 6000)
|
|
910
|
+
.split(FACT_SENTENCE_SPLIT_RE)
|
|
911
|
+
.map((part) => sanitizeFactText(part))
|
|
912
|
+
.filter((part) => part.length >= 8);
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
function collectTextsFromMessageLike(target, value, depth = 0) {
|
|
916
|
+
if (depth > 3 || value === null || value === undefined) return;
|
|
917
|
+
|
|
918
|
+
if (typeof value === 'string') {
|
|
919
|
+
const text = sanitizeFactText(value, 4000);
|
|
920
|
+
if (text) target.push(text);
|
|
921
|
+
return;
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
if (Array.isArray(value)) {
|
|
925
|
+
for (const entry of value) {
|
|
926
|
+
collectTextsFromMessageLike(target, entry, depth + 1);
|
|
927
|
+
}
|
|
928
|
+
return;
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
if (typeof value !== 'object') return;
|
|
932
|
+
|
|
933
|
+
const direct = extractTextFromMessage(value);
|
|
934
|
+
if (direct) {
|
|
935
|
+
target.push(sanitizeFactText(direct, 4000));
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
const directKeys = ['text', 'message', 'content', 'rawText', 'observedText', 'observation', 'prompt'];
|
|
939
|
+
for (const key of directKeys) {
|
|
940
|
+
if (typeof value[key] === 'string') {
|
|
941
|
+
target.push(sanitizeFactText(value[key], 4000));
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
const nestedKeys = ['messages', 'history', 'entries', 'items', 'observations', 'events', 'payload', 'context'];
|
|
946
|
+
for (const key of nestedKeys) {
|
|
947
|
+
if (value[key] !== undefined) {
|
|
948
|
+
collectTextsFromMessageLike(target, value[key], depth + 1);
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
function collectObservedTextsForFactExtraction(event) {
|
|
954
|
+
const collected = [];
|
|
955
|
+
|
|
956
|
+
const directStringCandidates = [
|
|
957
|
+
event?.text,
|
|
958
|
+
event?.message,
|
|
959
|
+
event?.content,
|
|
960
|
+
event?.rawText,
|
|
961
|
+
event?.context?.text,
|
|
962
|
+
event?.context?.message,
|
|
963
|
+
event?.context?.content,
|
|
964
|
+
event?.context?.rawText,
|
|
965
|
+
event?.context?.initialPrompt
|
|
966
|
+
];
|
|
967
|
+
|
|
968
|
+
for (const candidate of directStringCandidates) {
|
|
969
|
+
if (typeof candidate === 'string') {
|
|
970
|
+
const text = sanitizeFactText(candidate, 4000);
|
|
971
|
+
if (text) collected.push(text);
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
const structuredCandidates = [
|
|
976
|
+
event?.messages,
|
|
977
|
+
event?.context?.messages,
|
|
978
|
+
event?.context?.history,
|
|
979
|
+
event?.context?.initialMessages,
|
|
980
|
+
event?.context?.memoryFlush,
|
|
981
|
+
event?.context?.flush,
|
|
982
|
+
event?.observations,
|
|
983
|
+
event?.context?.observations,
|
|
984
|
+
event?.payload?.messages,
|
|
985
|
+
event?.payload?.events
|
|
986
|
+
];
|
|
987
|
+
|
|
988
|
+
for (const candidate of structuredCandidates) {
|
|
989
|
+
collectTextsFromMessageLike(collected, candidate);
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
const deduped = [];
|
|
993
|
+
const seen = new Set();
|
|
994
|
+
for (const item of collected) {
|
|
995
|
+
const normalized = sanitizeFactText(item, 4000);
|
|
996
|
+
if (!normalized) continue;
|
|
997
|
+
if (seen.has(normalized)) continue;
|
|
998
|
+
seen.add(normalized);
|
|
999
|
+
deduped.push(normalized);
|
|
1000
|
+
}
|
|
1001
|
+
return deduped;
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
function extractFactsFromObservedText(observedTexts, options) {
|
|
1005
|
+
const facts = [];
|
|
1006
|
+
const globalSeen = new Set();
|
|
1007
|
+
for (const text of observedTexts) {
|
|
1008
|
+
for (const sentence of splitObservedTextIntoSentences(text)) {
|
|
1009
|
+
const extracted = extractFactsFromSentence(sentence, options);
|
|
1010
|
+
for (const fact of extracted) {
|
|
1011
|
+
const dedupeKey = `${fact.entityNorm}|${fact.relation}|${normalizeFactValue(fact.value).toLowerCase()}`;
|
|
1012
|
+
if (globalSeen.has(dedupeKey)) continue;
|
|
1013
|
+
globalSeen.add(dedupeKey);
|
|
1014
|
+
facts.push(fact);
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
return facts;
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
function normalizeStoredFact(raw) {
|
|
1022
|
+
if (!raw || typeof raw !== 'object') return null;
|
|
1023
|
+
const relation = normalizeFactRelation(raw.relation);
|
|
1024
|
+
const value = normalizeFactValue(raw.value);
|
|
1025
|
+
if (!relation || !value) return null;
|
|
1026
|
+
|
|
1027
|
+
const entity = normalizeEntityLabel(raw.entity || raw.entityNorm || 'User');
|
|
1028
|
+
const entityNorm = normalizeEntityToken(raw.entityNorm || entity);
|
|
1029
|
+
const validFrom = toIsoTimestamp(raw.validFrom || new Date().toISOString());
|
|
1030
|
+
let validUntil = null;
|
|
1031
|
+
if (typeof raw.validUntil === 'string' && raw.validUntil.trim()) {
|
|
1032
|
+
validUntil = toIsoTimestamp(raw.validUntil);
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
const idBase = `${entityNorm}|${relation}|${value}|${validFrom}`;
|
|
1036
|
+
const fallbackId = createHash('sha1').update(idBase).digest('hex').slice(0, 16);
|
|
1037
|
+
|
|
1038
|
+
return {
|
|
1039
|
+
id: typeof raw.id === 'string' && raw.id.trim() ? raw.id.trim() : fallbackId,
|
|
1040
|
+
entity,
|
|
1041
|
+
entityNorm,
|
|
1042
|
+
relation,
|
|
1043
|
+
value,
|
|
1044
|
+
validFrom,
|
|
1045
|
+
validUntil,
|
|
1046
|
+
confidence: clampConfidence(raw.confidence, 0.7),
|
|
1047
|
+
category: sanitizeFactText(raw.category || 'facts', 40).toLowerCase() || 'facts',
|
|
1048
|
+
source: sanitizeFactText(raw.source || 'hook', 120) || 'hook',
|
|
1049
|
+
rawText: sanitizeFactText(raw.rawText || value, MAX_FACT_TEXT_LENGTH)
|
|
1050
|
+
};
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
function readFactsFromVault(vaultPath) {
|
|
1054
|
+
const factsPath = getFactsFilePath(vaultPath);
|
|
1055
|
+
if (!fs.existsSync(factsPath)) {
|
|
1056
|
+
return [];
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
try {
|
|
1060
|
+
const lines = fs.readFileSync(factsPath, 'utf-8')
|
|
1061
|
+
.split(/\r?\n/)
|
|
1062
|
+
.map((line) => line.trim())
|
|
1063
|
+
.filter(Boolean);
|
|
1064
|
+
const facts = [];
|
|
1065
|
+
for (const line of lines) {
|
|
1066
|
+
try {
|
|
1067
|
+
const parsed = JSON.parse(line);
|
|
1068
|
+
const normalized = normalizeStoredFact(parsed);
|
|
1069
|
+
if (normalized) facts.push(normalized);
|
|
1070
|
+
} catch {
|
|
1071
|
+
// Skip malformed lines and keep processing.
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
return facts;
|
|
1075
|
+
} catch {
|
|
1076
|
+
return [];
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
function writeFactsToVault(vaultPath, facts) {
|
|
1081
|
+
const factsPath = getFactsFilePath(vaultPath);
|
|
1082
|
+
const lines = facts.map((fact) => JSON.stringify(fact));
|
|
1083
|
+
const payload = lines.length > 0 ? `${lines.join('\n')}\n` : '';
|
|
1084
|
+
fs.writeFileSync(factsPath, payload, 'utf-8');
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
function mergeFactsWithConflictResolution(existingFacts, incomingFacts) {
|
|
1088
|
+
const merged = [...existingFacts];
|
|
1089
|
+
let added = 0;
|
|
1090
|
+
let superseded = 0;
|
|
1091
|
+
let changed = false;
|
|
1092
|
+
|
|
1093
|
+
for (const incoming of incomingFacts) {
|
|
1094
|
+
const activeSameRelation = merged.filter((fact) =>
|
|
1095
|
+
fact.entityNorm === incoming.entityNorm
|
|
1096
|
+
&& fact.relation === incoming.relation
|
|
1097
|
+
&& !fact.validUntil
|
|
1098
|
+
);
|
|
1099
|
+
|
|
1100
|
+
const incomingValue = normalizeFactValue(incoming.value).toLowerCase();
|
|
1101
|
+
const hasExactActiveMatch = activeSameRelation.some((fact) =>
|
|
1102
|
+
normalizeFactValue(fact.value).toLowerCase() === incomingValue
|
|
1103
|
+
);
|
|
1104
|
+
if (hasExactActiveMatch) {
|
|
1105
|
+
continue;
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
const shouldSupersede = activeSameRelation.some((fact) =>
|
|
1109
|
+
normalizeFactValue(fact.value).toLowerCase() !== incomingValue
|
|
1110
|
+
);
|
|
1111
|
+
if (shouldSupersede || isExclusiveFactRelation(incoming.relation)) {
|
|
1112
|
+
for (const fact of activeSameRelation) {
|
|
1113
|
+
if (normalizeFactValue(fact.value).toLowerCase() === incomingValue) continue;
|
|
1114
|
+
if (!fact.validUntil) {
|
|
1115
|
+
fact.validUntil = incoming.validFrom;
|
|
1116
|
+
superseded += 1;
|
|
1117
|
+
changed = true;
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
merged.push(incoming);
|
|
1123
|
+
added += 1;
|
|
1124
|
+
changed = true;
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
return { facts: merged, added, superseded, changed };
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
function isTimestampAfter(candidate, reference) {
|
|
1131
|
+
const candidateTime = new Date(candidate).getTime();
|
|
1132
|
+
const referenceTime = new Date(reference).getTime();
|
|
1133
|
+
if (Number.isNaN(candidateTime)) return false;
|
|
1134
|
+
if (Number.isNaN(referenceTime)) return true;
|
|
1135
|
+
return candidateTime > referenceTime;
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
function ensureGraphNode(nodesById, descriptor, seenAt) {
|
|
1139
|
+
const existing = nodesById.get(descriptor.id);
|
|
1140
|
+
if (!existing) {
|
|
1141
|
+
nodesById.set(descriptor.id, {
|
|
1142
|
+
id: descriptor.id,
|
|
1143
|
+
name: descriptor.name,
|
|
1144
|
+
displayName: descriptor.displayName,
|
|
1145
|
+
type: descriptor.type,
|
|
1146
|
+
attributes: descriptor.attributes || {},
|
|
1147
|
+
lastSeen: seenAt
|
|
1148
|
+
});
|
|
1149
|
+
return;
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
existing.attributes = { ...existing.attributes, ...(descriptor.attributes || {}) };
|
|
1153
|
+
if (isTimestampAfter(seenAt, existing.lastSeen)) {
|
|
1154
|
+
existing.lastSeen = seenAt;
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
function inferTargetNodeType(relation) {
|
|
1159
|
+
if (relation === 'works_at') return 'organization';
|
|
1160
|
+
if (relation === 'lives_in') return 'location';
|
|
1161
|
+
if (relation === 'partner_name' || relation === 'parent_name') return 'person';
|
|
1162
|
+
if (relation === 'dog_name') return 'pet';
|
|
1163
|
+
if (relation === 'age' || relation === 'spent') return 'number';
|
|
1164
|
+
if (relation === 'bought') return 'item';
|
|
1165
|
+
if (relation === 'decided') return 'decision';
|
|
1166
|
+
if (relation === 'allergic_to') return 'substance';
|
|
1167
|
+
if (relation === 'favorite_preference' || relation === 'dislikes') return 'preference';
|
|
1168
|
+
return 'attribute';
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
function buildTargetNodeDescriptor(fact) {
|
|
1172
|
+
const relation = normalizeFactRelation(fact.relation);
|
|
1173
|
+
const value = normalizeFactValue(fact.value);
|
|
1174
|
+
if (!relation || !value) return null;
|
|
1175
|
+
|
|
1176
|
+
if (ENTITY_TARGET_RELATIONS.has(relation)) {
|
|
1177
|
+
const normalizedEntityValue = normalizeEntityToken(value);
|
|
1178
|
+
return {
|
|
1179
|
+
id: `entity:${slugifyForId(normalizedEntityValue)}`,
|
|
1180
|
+
name: normalizedEntityValue,
|
|
1181
|
+
displayName: value,
|
|
1182
|
+
type: inferTargetNodeType(relation),
|
|
1183
|
+
attributes: { relation }
|
|
1184
|
+
};
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
return {
|
|
1188
|
+
id: `value:${relation}:${slugifyForId(value)}`,
|
|
1189
|
+
name: value.toLowerCase(),
|
|
1190
|
+
displayName: value,
|
|
1191
|
+
type: inferTargetNodeType(relation),
|
|
1192
|
+
attributes: { relation }
|
|
1193
|
+
};
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
function buildEntityGraphFromFacts(facts) {
|
|
1197
|
+
const nodesById = new Map();
|
|
1198
|
+
const edges = [];
|
|
1199
|
+
|
|
1200
|
+
for (const fact of facts) {
|
|
1201
|
+
const normalized = normalizeStoredFact(fact);
|
|
1202
|
+
if (!normalized) continue;
|
|
1203
|
+
|
|
1204
|
+
const sourceNodeId = `entity:${slugifyForId(normalized.entityNorm)}`;
|
|
1205
|
+
const seenAt = normalized.validFrom || new Date().toISOString();
|
|
1206
|
+
ensureGraphNode(nodesById, {
|
|
1207
|
+
id: sourceNodeId,
|
|
1208
|
+
name: normalized.entityNorm,
|
|
1209
|
+
displayName: normalized.entity,
|
|
1210
|
+
type: 'person',
|
|
1211
|
+
attributes: { entityNorm: normalized.entityNorm }
|
|
1212
|
+
}, seenAt);
|
|
1213
|
+
|
|
1214
|
+
const targetNode = buildTargetNodeDescriptor(normalized);
|
|
1215
|
+
if (!targetNode) continue;
|
|
1216
|
+
ensureGraphNode(nodesById, targetNode, seenAt);
|
|
1217
|
+
|
|
1218
|
+
const edgeHashSource = `${normalized.id}|${sourceNodeId}|${targetNode.id}|${normalized.relation}|${normalized.validFrom}`;
|
|
1219
|
+
const edgeId = `edge:${createHash('sha1').update(edgeHashSource).digest('hex').slice(0, 18)}`;
|
|
1220
|
+
|
|
1221
|
+
edges.push({
|
|
1222
|
+
id: edgeId,
|
|
1223
|
+
source: sourceNodeId,
|
|
1224
|
+
target: targetNode.id,
|
|
1225
|
+
relation: normalized.relation,
|
|
1226
|
+
validFrom: normalized.validFrom,
|
|
1227
|
+
validUntil: normalized.validUntil,
|
|
1228
|
+
confidence: clampConfidence(normalized.confidence, 0.7)
|
|
1229
|
+
});
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
const nodes = [...nodesById.values()].sort((a, b) => a.id.localeCompare(b.id));
|
|
1233
|
+
const sortedEdges = edges.sort((a, b) => a.id.localeCompare(b.id));
|
|
1234
|
+
return {
|
|
1235
|
+
version: ENTITY_GRAPH_VERSION,
|
|
1236
|
+
nodes,
|
|
1237
|
+
edges: sortedEdges
|
|
1238
|
+
};
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
function writeEntityGraphToVault(vaultPath, facts) {
|
|
1242
|
+
const graphPath = getEntityGraphFilePath(vaultPath);
|
|
1243
|
+
const graph = buildEntityGraphFromFacts(facts);
|
|
1244
|
+
fs.writeFileSync(graphPath, JSON.stringify(graph, null, 2), 'utf-8');
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
function persistExtractedFacts(vaultPath, incomingFacts) {
|
|
1248
|
+
const existingFacts = readFactsFromVault(vaultPath);
|
|
1249
|
+
const normalizedIncomingFacts = incomingFacts
|
|
1250
|
+
.map((fact) => normalizeStoredFact(fact))
|
|
1251
|
+
.filter(Boolean);
|
|
1252
|
+
|
|
1253
|
+
if (normalizedIncomingFacts.length === 0) {
|
|
1254
|
+
writeEntityGraphToVault(vaultPath, existingFacts);
|
|
1255
|
+
return { facts: existingFacts, added: 0, superseded: 0 };
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
const { facts, added, superseded, changed } = mergeFactsWithConflictResolution(
|
|
1259
|
+
existingFacts,
|
|
1260
|
+
normalizedIncomingFacts
|
|
1261
|
+
);
|
|
1262
|
+
|
|
1263
|
+
if (changed || !fs.existsSync(getFactsFilePath(vaultPath))) {
|
|
1264
|
+
writeFactsToVault(vaultPath, facts);
|
|
1265
|
+
}
|
|
1266
|
+
writeEntityGraphToVault(vaultPath, facts);
|
|
1267
|
+
return { facts, added, superseded };
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
function runFactExtractionForEvent(vaultPath, event, eventLabel) {
|
|
1271
|
+
try {
|
|
1272
|
+
const observedTexts = collectObservedTextsForFactExtraction(event);
|
|
1273
|
+
if (observedTexts.length === 0) {
|
|
1274
|
+
console.log(`[clawvault] Fact extraction skipped (${eventLabel}: no observed text)`);
|
|
1275
|
+
return;
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
const validFrom = toIsoTimestamp(extractEventTimestamp(event) || new Date());
|
|
1279
|
+
const source = `hook:${eventLabel}`;
|
|
1280
|
+
const extracted = extractFactsFromObservedText(observedTexts, { source, validFrom });
|
|
1281
|
+
|
|
1282
|
+
if (extracted.length === 0) {
|
|
1283
|
+
console.log(`[clawvault] Fact extraction found no matches (${eventLabel})`);
|
|
1284
|
+
return;
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
const { facts, added, superseded } = persistExtractedFacts(vaultPath, extracted);
|
|
1288
|
+
console.log(`[clawvault] Fact extraction complete (${eventLabel}): +${added}, superseded ${superseded}, total ${facts.length}`);
|
|
1289
|
+
} catch (err) {
|
|
1290
|
+
console.warn(`[clawvault] Fact extraction failed (${eventLabel}): ${err?.message || 'unknown error'}`);
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
|
|
614
1294
|
function extractEventTimestamp(event) {
|
|
615
1295
|
const candidates = [
|
|
616
1296
|
event?.timestamp,
|
|
@@ -634,7 +1314,7 @@ function isSundayMidnightUtc(date) {
|
|
|
634
1314
|
}
|
|
635
1315
|
|
|
636
1316
|
async function handleWeeklyReflect(event) {
|
|
637
|
-
const vaultPath = findVaultPath();
|
|
1317
|
+
const vaultPath = findVaultPath(event);
|
|
638
1318
|
if (!vaultPath) {
|
|
639
1319
|
console.log('[clawvault] No vault found, skipping weekly reflection');
|
|
640
1320
|
return;
|
|
@@ -656,7 +1336,7 @@ async function handleWeeklyReflect(event) {
|
|
|
656
1336
|
|
|
657
1337
|
// Handle gateway startup - check for context death
|
|
658
1338
|
async function handleStartup(event) {
|
|
659
|
-
const vaultPath = findVaultPath();
|
|
1339
|
+
const vaultPath = findVaultPath(event);
|
|
660
1340
|
if (!vaultPath) {
|
|
661
1341
|
console.log('[clawvault] No vault found, skipping recovery check');
|
|
662
1342
|
return;
|
|
@@ -695,7 +1375,7 @@ async function handleStartup(event) {
|
|
|
695
1375
|
|
|
696
1376
|
// Handle /new command - auto-checkpoint before reset
|
|
697
1377
|
async function handleNew(event) {
|
|
698
|
-
const vaultPath = findVaultPath();
|
|
1378
|
+
const vaultPath = findVaultPath(event);
|
|
699
1379
|
if (!vaultPath) {
|
|
700
1380
|
console.log('[clawvault] No vault found, skipping auto-checkpoint');
|
|
701
1381
|
return;
|
|
@@ -730,11 +1410,12 @@ async function handleNew(event) {
|
|
|
730
1410
|
minNewBytes: 1,
|
|
731
1411
|
reason: 'command:new flush'
|
|
732
1412
|
});
|
|
1413
|
+
runFactExtractionForEvent(vaultPath, event, 'command:new');
|
|
733
1414
|
}
|
|
734
1415
|
|
|
735
1416
|
// Handle session start - inject dynamic context for first prompt
|
|
736
1417
|
async function handleSessionStart(event) {
|
|
737
|
-
const vaultPath = findVaultPath();
|
|
1418
|
+
const vaultPath = findVaultPath(event);
|
|
738
1419
|
if (!vaultPath) {
|
|
739
1420
|
console.log('[clawvault] No vault found, skipping context injection');
|
|
740
1421
|
return;
|
|
@@ -796,7 +1477,7 @@ async function handleSessionStart(event) {
|
|
|
796
1477
|
|
|
797
1478
|
// Handle heartbeat events - cheap stat-based trigger for active observation
|
|
798
1479
|
async function handleHeartbeat(event) {
|
|
799
|
-
const vaultPath = findVaultPath();
|
|
1480
|
+
const vaultPath = findVaultPath(event);
|
|
800
1481
|
if (!vaultPath) {
|
|
801
1482
|
console.log('[clawvault] No vault found, skipping heartbeat observation check');
|
|
802
1483
|
return;
|
|
@@ -813,7 +1494,7 @@ async function handleHeartbeat(event) {
|
|
|
813
1494
|
|
|
814
1495
|
// Handle context compaction - force flush any pending session deltas
|
|
815
1496
|
async function handleContextCompaction(event) {
|
|
816
|
-
const vaultPath = findVaultPath();
|
|
1497
|
+
const vaultPath = findVaultPath(event);
|
|
817
1498
|
if (!vaultPath) {
|
|
818
1499
|
console.log('[clawvault] No vault found, skipping compaction observation');
|
|
819
1500
|
return;
|
|
@@ -824,6 +1505,7 @@ async function handleContextCompaction(event) {
|
|
|
824
1505
|
minNewBytes: 1,
|
|
825
1506
|
reason: 'context compaction'
|
|
826
1507
|
});
|
|
1508
|
+
runFactExtractionForEvent(vaultPath, event, 'compaction:memoryFlush');
|
|
827
1509
|
}
|
|
828
1510
|
|
|
829
1511
|
// Main handler - route events
|