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.
Files changed (291) hide show
  1. package/README.md +352 -20
  2. package/bin/clawvault.js +8 -2
  3. package/bin/command-registration.test.js +3 -1
  4. package/bin/command-runtime.js +9 -1
  5. package/bin/register-core-commands.js +23 -10
  6. package/bin/register-maintenance-commands.js +39 -3
  7. package/bin/register-query-commands.js +58 -29
  8. package/bin/register-task-commands.js +18 -1
  9. package/bin/register-task-commands.test.js +16 -0
  10. package/bin/register-vault-operations-commands.js +29 -1
  11. package/bin/register-workgraph-commands.js +1368 -0
  12. package/dashboard/lib/graph-diff.js +104 -0
  13. package/dashboard/lib/graph-diff.test.js +75 -0
  14. package/dashboard/lib/vault-parser.js +556 -0
  15. package/dashboard/lib/vault-parser.test.js +254 -0
  16. package/dashboard/public/app.js +796 -0
  17. package/dashboard/public/index.html +52 -0
  18. package/dashboard/public/styles.css +221 -0
  19. package/dashboard/server.js +374 -0
  20. package/dist/{chunk-F2JEUD4J.js → chunk-23YDQ3QU.js} +6 -8
  21. package/dist/{chunk-C7OK5WKP.js → chunk-2JQ3O2YL.js} +4 -4
  22. package/dist/{chunk-VR5NE7PZ.js → chunk-2RAZ4ZFE.js} +1 -1
  23. package/dist/chunk-2ZDO52B4.js +52 -0
  24. package/dist/{chunk-ZZA73MFY.js → chunk-33DOSHTA.js} +176 -36
  25. package/dist/chunk-33VSQP4J.js +37 -0
  26. package/dist/chunk-4BQTQMJP.js +93 -0
  27. package/dist/{chunk-GUKMRGM7.js → chunk-4OXMU5S2.js} +1 -1
  28. package/dist/{chunk-62YTUT6J.js → chunk-4PY655YM.js} +15 -3
  29. package/dist/chunk-6FH3IULF.js +352 -0
  30. package/dist/{chunk-3NSBOUT3.js → chunk-77Q5CSPJ.js} +404 -80
  31. package/dist/{chunk-4VQTUVH7.js → chunk-7YZWHM36.js} +52 -26
  32. package/dist/chunk-BSJ6RIT7.js +447 -0
  33. package/dist/chunk-BUEW6IIK.js +364 -0
  34. package/dist/{chunk-WGRQ6HDV.js → chunk-CLJTREDS.js} +74 -14
  35. package/dist/chunk-EK6S23ZB.js +469 -0
  36. package/dist/{chunk-LNJA2UGL.js → chunk-ESFLMDRB.js} +9 -86
  37. package/dist/{chunk-H34S76MB.js → chunk-ESVS6K2B.js} +6 -6
  38. package/dist/{chunk-WAZ3NLWL.js → chunk-F55HGNU4.js} +0 -47
  39. package/dist/{chunk-QK3UCXWL.js → chunk-FHFUXL6G.js} +2 -2
  40. package/dist/{chunk-YKTA5JOJ.js → chunk-GAOWA7GR.js} +212 -46
  41. package/dist/chunk-GGA32J2R.js +784 -0
  42. package/dist/chunk-GNJL4YGR.js +79 -0
  43. package/dist/chunk-MDIH26GC.js +183 -0
  44. package/dist/{chunk-LYHGEHXG.js → chunk-MFAWT5O5.js} +0 -1
  45. package/dist/chunk-MM6QGW3P.js +207 -0
  46. package/dist/{chunk-P5EPF6MB.js → chunk-MW5C6ZQA.js} +110 -13
  47. package/dist/chunk-NCKFNBHJ.js +257 -0
  48. package/dist/{chunk-QBLMXKF2.js → chunk-OIWVQYQF.js} +1 -1
  49. package/dist/{chunk-42MXU7A6.js → chunk-P62WHA27.js} +58 -47
  50. package/dist/chunk-PBACDKKP.js +66 -0
  51. package/dist/{chunk-VGLOTGAS.js → chunk-QSHD36LH.js} +2 -2
  52. package/dist/{chunk-OZ7RIXTO.js → chunk-QSRRMEYM.js} +2 -2
  53. package/dist/chunk-QVEERJSP.js +152 -0
  54. package/dist/{chunk-N2AXRYLC.js → chunk-QWQ3TIKS.js} +1 -1
  55. package/dist/{chunk-3DHXQHYG.js → chunk-R2MIW5G7.js} +1 -1
  56. package/dist/{chunk-SJSFRIYS.js → chunk-SLXOR3CC.js} +2 -2
  57. package/dist/chunk-SS4B7P7V.js +99 -0
  58. package/dist/{chunk-JY6FYXIT.js → chunk-STCQGCEQ.js} +6 -11
  59. package/dist/chunk-U4O6C46S.js +154 -0
  60. package/dist/{chunk-ITPEXLHA.js → chunk-URXDAUVH.js} +24 -5
  61. package/dist/chunk-VSL7KY3M.js +189 -0
  62. package/dist/{chunk-U55BGUAU.js → chunk-W4SPAEE7.js} +6 -6
  63. package/dist/chunk-WMGIIABP.js +15 -0
  64. package/dist/{chunk-3D6BCTP6.js → chunk-X3SPPUFG.js} +51 -39
  65. package/dist/{chunk-THRJVD4L.js → chunk-Y6VJKXGL.js} +1 -1
  66. package/dist/{chunk-ZVVFWOLW.js → chunk-ZN54U2OZ.js} +123 -10
  67. package/dist/cli/index.js +32 -25
  68. package/dist/commands/archive.js +3 -3
  69. package/dist/commands/backlog.js +3 -3
  70. package/dist/commands/blocked.js +3 -3
  71. package/dist/commands/canvas.d.ts +15 -0
  72. package/dist/commands/canvas.js +200 -0
  73. package/dist/commands/checkpoint.js +2 -2
  74. package/dist/commands/compat.js +2 -2
  75. package/dist/commands/context.js +8 -6
  76. package/dist/commands/doctor.d.ts +11 -7
  77. package/dist/commands/doctor.js +18 -16
  78. package/dist/commands/embed.js +5 -6
  79. package/dist/commands/entities.js +2 -2
  80. package/dist/commands/graph.js +4 -4
  81. package/dist/commands/inject.d.ts +1 -1
  82. package/dist/commands/inject.js +5 -6
  83. package/dist/commands/kanban.js +4 -4
  84. package/dist/commands/link.js +5 -5
  85. package/dist/commands/migrate-observations.js +4 -4
  86. package/dist/commands/observe.d.ts +0 -1
  87. package/dist/commands/observe.js +14 -13
  88. package/dist/commands/project.js +5 -5
  89. package/dist/commands/rebuild-embeddings.d.ts +21 -0
  90. package/dist/commands/rebuild-embeddings.js +91 -0
  91. package/dist/commands/rebuild.js +12 -11
  92. package/dist/commands/recover.js +3 -3
  93. package/dist/commands/reflect.js +6 -7
  94. package/dist/commands/repair-session.js +1 -1
  95. package/dist/commands/replay.js +14 -14
  96. package/dist/commands/session-recap.js +1 -1
  97. package/dist/commands/setup.d.ts +2 -90
  98. package/dist/commands/setup.js +3 -21
  99. package/dist/commands/shell-init.js +1 -1
  100. package/dist/commands/sleep.d.ts +1 -1
  101. package/dist/commands/sleep.js +20 -19
  102. package/dist/commands/status.d.ts +2 -0
  103. package/dist/commands/status.js +57 -35
  104. package/dist/commands/sync-bd.d.ts +10 -0
  105. package/dist/commands/sync-bd.js +10 -0
  106. package/dist/commands/tailscale.js +3 -3
  107. package/dist/commands/task.js +4 -4
  108. package/dist/commands/template.js +2 -2
  109. package/dist/commands/wake.d.ts +1 -1
  110. package/dist/commands/wake.js +11 -10
  111. package/dist/commands/workgraph.d.ts +124 -0
  112. package/dist/commands/workgraph.js +38 -0
  113. package/dist/index.d.ts +337 -191
  114. package/dist/index.js +387 -118
  115. package/dist/{inject-Bzi5E-By.d.cts → inject-DYUrDqQO.d.ts} +3 -3
  116. package/dist/ledger-B7g7jhqG.d.ts +44 -0
  117. package/dist/lib/auto-linker.js +2 -2
  118. package/dist/lib/canvas-layout.d.ts +100 -16
  119. package/dist/lib/canvas-layout.js +21 -78
  120. package/dist/lib/config.d.ts +27 -3
  121. package/dist/lib/config.js +4 -2
  122. package/dist/lib/entity-index.js +1 -1
  123. package/dist/lib/project-utils.js +4 -4
  124. package/dist/lib/session-repair.js +1 -1
  125. package/dist/lib/session-utils.js +1 -1
  126. package/dist/lib/tailscale.js +1 -1
  127. package/dist/lib/task-utils.js +3 -3
  128. package/dist/lib/template-engine.js +1 -1
  129. package/dist/lib/webdav.js +1 -1
  130. package/dist/onnxruntime_binding-5QEF3SUC.node +0 -0
  131. package/dist/onnxruntime_binding-BKPKNEGC.node +0 -0
  132. package/dist/onnxruntime_binding-FMOXGIUT.node +0 -0
  133. package/dist/onnxruntime_binding-OI2KMXC5.node +0 -0
  134. package/dist/onnxruntime_binding-UX44MLAZ.node +0 -0
  135. package/dist/onnxruntime_binding-Y2W7N7WY.node +0 -0
  136. package/dist/openclaw-plugin.d.ts +8 -0
  137. package/dist/openclaw-plugin.js +14 -0
  138. package/dist/registry-BR4326o0.d.ts +30 -0
  139. package/dist/store-CA-6sKCJ.d.ts +34 -0
  140. package/dist/thread-B9LhXNU0.d.ts +41 -0
  141. package/dist/transformers.node-A2ZRORSQ.js +46775 -0
  142. package/dist/{types-Y2_Um2Ls.d.cts → types-BbWJoC1c.d.ts} +1 -44
  143. package/dist/workgraph/index.d.ts +5 -0
  144. package/dist/workgraph/index.js +23 -0
  145. package/dist/workgraph/ledger.d.ts +2 -0
  146. package/dist/workgraph/ledger.js +25 -0
  147. package/dist/workgraph/registry.d.ts +2 -0
  148. package/dist/workgraph/registry.js +19 -0
  149. package/dist/workgraph/store.d.ts +2 -0
  150. package/dist/workgraph/store.js +25 -0
  151. package/dist/workgraph/thread.d.ts +2 -0
  152. package/dist/workgraph/thread.js +25 -0
  153. package/dist/workgraph/types.d.ts +54 -0
  154. package/dist/workgraph/types.js +7 -0
  155. package/hooks/clawvault/HOOK.md +34 -4
  156. package/hooks/clawvault/handler.js +760 -78
  157. package/hooks/clawvault/handler.test.js +235 -79
  158. package/hooks/clawvault/openclaw.plugin.json +72 -0
  159. package/openclaw.plugin.json +65 -38
  160. package/package.json +15 -18
  161. package/dist/chunk-3RG5ZIWI.js +0 -10
  162. package/dist/chunk-6U6MK36V.js +0 -205
  163. package/dist/chunk-7R7O6STJ.js +0 -88
  164. package/dist/chunk-CMB7UL7C.js +0 -327
  165. package/dist/chunk-DEFFDRVP.js +0 -938
  166. package/dist/chunk-E7MFQB6D.js +0 -163
  167. package/dist/chunk-GAJV4IGR.js +0 -82
  168. package/dist/chunk-GQSLDZTS.js +0 -560
  169. package/dist/chunk-K234IDRJ.js +0 -1073
  170. package/dist/chunk-MFM6K7PU.js +0 -374
  171. package/dist/chunk-MXSSG3QU.js +0 -42
  172. package/dist/chunk-PAH27GSN.js +0 -108
  173. package/dist/cli/index.cjs +0 -10033
  174. package/dist/cli/index.d.cts +0 -5
  175. package/dist/commands/archive.cjs +0 -287
  176. package/dist/commands/archive.d.cts +0 -11
  177. package/dist/commands/backlog.cjs +0 -721
  178. package/dist/commands/backlog.d.cts +0 -53
  179. package/dist/commands/blocked.cjs +0 -204
  180. package/dist/commands/blocked.d.cts +0 -26
  181. package/dist/commands/checkpoint.cjs +0 -244
  182. package/dist/commands/checkpoint.d.cts +0 -41
  183. package/dist/commands/compat.cjs +0 -369
  184. package/dist/commands/compat.d.cts +0 -28
  185. package/dist/commands/context.cjs +0 -2989
  186. package/dist/commands/context.d.cts +0 -2
  187. package/dist/commands/doctor.cjs +0 -3062
  188. package/dist/commands/doctor.d.cts +0 -21
  189. package/dist/commands/embed.cjs +0 -232
  190. package/dist/commands/embed.d.cts +0 -17
  191. package/dist/commands/entities.cjs +0 -141
  192. package/dist/commands/entities.d.cts +0 -7
  193. package/dist/commands/graph.cjs +0 -501
  194. package/dist/commands/graph.d.cts +0 -21
  195. package/dist/commands/inject.cjs +0 -1636
  196. package/dist/commands/inject.d.cts +0 -2
  197. package/dist/commands/kanban.cjs +0 -884
  198. package/dist/commands/kanban.d.cts +0 -63
  199. package/dist/commands/link.cjs +0 -965
  200. package/dist/commands/link.d.cts +0 -11
  201. package/dist/commands/migrate-observations.cjs +0 -362
  202. package/dist/commands/migrate-observations.d.cts +0 -19
  203. package/dist/commands/observe.cjs +0 -4099
  204. package/dist/commands/observe.d.cts +0 -23
  205. package/dist/commands/project.cjs +0 -1341
  206. package/dist/commands/project.d.cts +0 -85
  207. package/dist/commands/rebuild.cjs +0 -3136
  208. package/dist/commands/rebuild.d.cts +0 -11
  209. package/dist/commands/recover.cjs +0 -361
  210. package/dist/commands/recover.d.cts +0 -38
  211. package/dist/commands/reflect.cjs +0 -1008
  212. package/dist/commands/reflect.d.cts +0 -11
  213. package/dist/commands/repair-session.cjs +0 -457
  214. package/dist/commands/repair-session.d.cts +0 -38
  215. package/dist/commands/replay.cjs +0 -4103
  216. package/dist/commands/replay.d.cts +0 -16
  217. package/dist/commands/session-recap.cjs +0 -353
  218. package/dist/commands/session-recap.d.cts +0 -27
  219. package/dist/commands/setup.cjs +0 -1345
  220. package/dist/commands/setup.d.cts +0 -100
  221. package/dist/commands/shell-init.cjs +0 -75
  222. package/dist/commands/shell-init.d.cts +0 -7
  223. package/dist/commands/sleep.cjs +0 -6028
  224. package/dist/commands/sleep.d.cts +0 -36
  225. package/dist/commands/status.cjs +0 -2736
  226. package/dist/commands/status.d.cts +0 -52
  227. package/dist/commands/tailscale.cjs +0 -1532
  228. package/dist/commands/tailscale.d.cts +0 -52
  229. package/dist/commands/task.cjs +0 -1236
  230. package/dist/commands/task.d.cts +0 -97
  231. package/dist/commands/template.cjs +0 -457
  232. package/dist/commands/template.d.cts +0 -36
  233. package/dist/commands/wake.cjs +0 -2626
  234. package/dist/commands/wake.d.cts +0 -22
  235. package/dist/context-BUGaWpyL.d.cts +0 -46
  236. package/dist/index.cjs +0 -14526
  237. package/dist/index.d.cts +0 -858
  238. package/dist/inject-Bzi5E-By.d.ts +0 -137
  239. package/dist/lib/auto-linker.cjs +0 -176
  240. package/dist/lib/auto-linker.d.cts +0 -26
  241. package/dist/lib/canvas-layout.cjs +0 -136
  242. package/dist/lib/canvas-layout.d.cts +0 -31
  243. package/dist/lib/config.cjs +0 -78
  244. package/dist/lib/config.d.cts +0 -11
  245. package/dist/lib/entity-index.cjs +0 -84
  246. package/dist/lib/entity-index.d.cts +0 -26
  247. package/dist/lib/project-utils.cjs +0 -864
  248. package/dist/lib/project-utils.d.cts +0 -97
  249. package/dist/lib/session-repair.cjs +0 -239
  250. package/dist/lib/session-repair.d.cts +0 -110
  251. package/dist/lib/session-utils.cjs +0 -209
  252. package/dist/lib/session-utils.d.cts +0 -63
  253. package/dist/lib/tailscale.cjs +0 -1183
  254. package/dist/lib/tailscale.d.cts +0 -225
  255. package/dist/lib/task-utils.cjs +0 -1137
  256. package/dist/lib/task-utils.d.cts +0 -208
  257. package/dist/lib/template-engine.cjs +0 -47
  258. package/dist/lib/template-engine.d.cts +0 -11
  259. package/dist/lib/webdav.cjs +0 -568
  260. package/dist/lib/webdav.d.cts +0 -109
  261. package/dist/plugin/index.cjs +0 -1907
  262. package/dist/plugin/index.d.cts +0 -36
  263. package/dist/plugin/index.d.ts +0 -36
  264. package/dist/plugin/index.js +0 -572
  265. package/dist/plugin/inject.cjs +0 -356
  266. package/dist/plugin/inject.d.cts +0 -54
  267. package/dist/plugin/inject.d.ts +0 -54
  268. package/dist/plugin/inject.js +0 -17
  269. package/dist/plugin/observe.cjs +0 -631
  270. package/dist/plugin/observe.d.cts +0 -39
  271. package/dist/plugin/observe.d.ts +0 -39
  272. package/dist/plugin/observe.js +0 -18
  273. package/dist/plugin/templates.cjs +0 -593
  274. package/dist/plugin/templates.d.cts +0 -52
  275. package/dist/plugin/templates.d.ts +0 -52
  276. package/dist/plugin/templates.js +0 -25
  277. package/dist/plugin/types.cjs +0 -18
  278. package/dist/plugin/types.d.cts +0 -209
  279. package/dist/plugin/types.d.ts +0 -209
  280. package/dist/plugin/types.js +0 -0
  281. package/dist/plugin/vault.cjs +0 -927
  282. package/dist/plugin/vault.d.cts +0 -68
  283. package/dist/plugin/vault.d.ts +0 -68
  284. package/dist/plugin/vault.js +0 -22
  285. package/dist/types-Y2_Um2Ls.d.ts +0 -205
  286. package/templates/memory-event.md +0 -67
  287. package/templates/party.md +0 -63
  288. package/templates/primitive-registry.yaml +0 -551
  289. package/templates/run.md +0 -68
  290. package/templates/trigger.md +0 -68
  291. 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 = /[._:\/-]+/g;
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
- let filePath = path.join(sessionsDir, `${sessionId}.jsonl`);
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 = [normalizeEventToken(type), normalizeEventToken(action)]
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
- const normalizedEventPair = `${normalizedType}:${normalizedAction}`;
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
- function findVaultPath() {
491
- // Check env first
492
- if (process.env.CLAWVAULT_PATH) {
493
- return validateVaultPath(process.env.CLAWVAULT_PATH);
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
- const configuredCandidates = [
496
- process.env.OPENCLAW_MEMORY_PATH,
497
- process.env.OPENCLAW_VAULT_PATH
498
- ];
499
- for (const candidate of configuredCandidates) {
500
- const validated = validateVaultPath(candidate);
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