clawvault 3.1.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 (289) hide show
  1. package/README.md +422 -141
  2. package/bin/clawvault.js +10 -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 -28
  6. package/bin/register-maintenance-commands.js +39 -3
  7. package/bin/register-query-commands.js +58 -29
  8. package/bin/register-tailscale-commands.js +106 -0
  9. package/bin/register-task-commands.js +18 -1
  10. package/bin/register-task-commands.test.js +16 -0
  11. package/bin/register-vault-operations-commands.js +29 -1
  12. package/bin/register-workgraph-commands.js +1368 -0
  13. package/dashboard/lib/graph-diff.js +104 -0
  14. package/dashboard/lib/graph-diff.test.js +75 -0
  15. package/dashboard/lib/vault-parser.js +556 -0
  16. package/dashboard/lib/vault-parser.test.js +254 -0
  17. package/dashboard/public/app.js +796 -0
  18. package/dashboard/public/index.html +52 -0
  19. package/dashboard/public/styles.css +221 -0
  20. package/dashboard/server.js +374 -0
  21. package/dist/{chunk-F2JEUD4J.js → chunk-23YDQ3QU.js} +6 -8
  22. package/dist/{chunk-C7OK5WKP.js → chunk-2JQ3O2YL.js} +4 -4
  23. package/dist/{chunk-VR5NE7PZ.js → chunk-2RAZ4ZFE.js} +1 -1
  24. package/dist/chunk-2ZDO52B4.js +52 -0
  25. package/dist/{chunk-ZZA73MFY.js → chunk-33DOSHTA.js} +176 -36
  26. package/dist/chunk-33VSQP4J.js +37 -0
  27. package/dist/chunk-4BQTQMJP.js +93 -0
  28. package/dist/{chunk-GUKMRGM7.js → chunk-4OXMU5S2.js} +1 -1
  29. package/dist/{chunk-62YTUT6J.js → chunk-4PY655YM.js} +15 -3
  30. package/dist/chunk-6FH3IULF.js +352 -0
  31. package/dist/{chunk-3NSBOUT3.js → chunk-77Q5CSPJ.js} +404 -80
  32. package/dist/{chunk-4VQTUVH7.js → chunk-7YZWHM36.js} +52 -26
  33. package/dist/chunk-BSJ6RIT7.js +447 -0
  34. package/dist/chunk-BUEW6IIK.js +364 -0
  35. package/dist/{chunk-LI4O6NVK.js → chunk-CLJTREDS.js} +74 -14
  36. package/dist/chunk-EK6S23ZB.js +469 -0
  37. package/dist/{chunk-LNJA2UGL.js → chunk-ESFLMDRB.js} +9 -86
  38. package/dist/{chunk-H34S76MB.js → chunk-ESVS6K2B.js} +6 -6
  39. package/dist/{chunk-WAZ3NLWL.js → chunk-F55HGNU4.js} +0 -47
  40. package/dist/{chunk-QK3UCXWL.js → chunk-FHFUXL6G.js} +2 -2
  41. package/dist/{chunk-H62BP7RI.js → chunk-GAOWA7GR.js} +212 -46
  42. package/dist/chunk-GGA32J2R.js +784 -0
  43. package/dist/chunk-GNJL4YGR.js +79 -0
  44. package/dist/chunk-IVRIKYFE.js +520 -0
  45. package/dist/chunk-MDIH26GC.js +183 -0
  46. package/dist/{chunk-LYHGEHXG.js → chunk-MFAWT5O5.js} +0 -1
  47. package/dist/chunk-MM6QGW3P.js +207 -0
  48. package/dist/{chunk-P5EPF6MB.js → chunk-MW5C6ZQA.js} +110 -13
  49. package/dist/chunk-NCKFNBHJ.js +257 -0
  50. package/dist/{chunk-QBLMXKF2.js → chunk-OIWVQYQF.js} +1 -1
  51. package/dist/{chunk-42MXU7A6.js → chunk-P62WHA27.js} +58 -47
  52. package/dist/chunk-PBACDKKP.js +66 -0
  53. package/dist/{chunk-VGLOTGAS.js → chunk-QSHD36LH.js} +2 -2
  54. package/dist/{chunk-OZ7RIXTO.js → chunk-QSRRMEYM.js} +2 -2
  55. package/dist/chunk-QVEERJSP.js +152 -0
  56. package/dist/{chunk-N2AXRYLC.js → chunk-QWQ3TIKS.js} +1 -1
  57. package/dist/{chunk-3DHXQHYG.js → chunk-R2MIW5G7.js} +1 -1
  58. package/dist/{chunk-SJSFRIYS.js → chunk-SLXOR3CC.js} +2 -2
  59. package/dist/chunk-SS4B7P7V.js +99 -0
  60. package/dist/{chunk-JY6FYXIT.js → chunk-STCQGCEQ.js} +6 -11
  61. package/dist/chunk-TIGW564L.js +628 -0
  62. package/dist/chunk-U4O6C46S.js +154 -0
  63. package/dist/{chunk-ITPEXLHA.js → chunk-URXDAUVH.js} +24 -5
  64. package/dist/chunk-VSL7KY3M.js +189 -0
  65. package/dist/{chunk-U55BGUAU.js → chunk-W4SPAEE7.js} +6 -6
  66. package/dist/chunk-WMGIIABP.js +15 -0
  67. package/dist/{chunk-33UGEQRT.js → chunk-X3SPPUFG.js} +151 -64
  68. package/dist/chunk-Y6VJKXGL.js +373 -0
  69. package/dist/{chunk-3WRJEKN4.js → chunk-ZN54U2OZ.js} +123 -10
  70. package/dist/cli/index.js +34 -24
  71. package/dist/commands/archive.js +3 -3
  72. package/dist/commands/backlog.js +3 -3
  73. package/dist/commands/blocked.js +3 -3
  74. package/dist/commands/canvas.d.ts +15 -0
  75. package/dist/commands/canvas.js +200 -0
  76. package/dist/commands/checkpoint.js +2 -2
  77. package/dist/commands/compat.js +2 -2
  78. package/dist/commands/context.js +8 -6
  79. package/dist/commands/doctor.d.ts +11 -7
  80. package/dist/commands/doctor.js +18 -16
  81. package/dist/commands/embed.js +5 -6
  82. package/dist/commands/entities.js +2 -2
  83. package/dist/commands/graph.js +4 -4
  84. package/dist/commands/inject.d.ts +1 -1
  85. package/dist/commands/inject.js +5 -6
  86. package/dist/commands/kanban.js +4 -4
  87. package/dist/commands/link.js +5 -5
  88. package/dist/commands/migrate-observations.js +4 -4
  89. package/dist/commands/observe.d.ts +0 -1
  90. package/dist/commands/observe.js +14 -13
  91. package/dist/commands/project.js +5 -5
  92. package/dist/commands/rebuild-embeddings.d.ts +21 -0
  93. package/dist/commands/rebuild-embeddings.js +91 -0
  94. package/dist/commands/rebuild.js +12 -11
  95. package/dist/commands/recover.js +3 -3
  96. package/dist/commands/reflect.js +6 -7
  97. package/dist/commands/repair-session.js +1 -1
  98. package/dist/commands/replay.js +14 -14
  99. package/dist/commands/session-recap.js +1 -1
  100. package/dist/commands/setup.d.ts +2 -89
  101. package/dist/commands/setup.js +3 -21
  102. package/dist/commands/shell-init.js +1 -1
  103. package/dist/commands/sleep.d.ts +1 -1
  104. package/dist/commands/sleep.js +20 -19
  105. package/dist/commands/status.d.ts +2 -0
  106. package/dist/commands/status.js +57 -35
  107. package/dist/commands/sync-bd.d.ts +10 -0
  108. package/dist/commands/sync-bd.js +10 -0
  109. package/dist/commands/tailscale.d.ts +52 -0
  110. package/dist/commands/tailscale.js +26 -0
  111. package/dist/commands/task.js +4 -4
  112. package/dist/commands/template.js +2 -2
  113. package/dist/commands/wake.d.ts +1 -1
  114. package/dist/commands/wake.js +11 -10
  115. package/dist/commands/workgraph.d.ts +124 -0
  116. package/dist/commands/workgraph.js +38 -0
  117. package/dist/index.d.ts +341 -191
  118. package/dist/index.js +446 -116
  119. package/dist/{inject-Bzi5E-By.d.ts → inject-DYUrDqQO.d.ts} +3 -3
  120. package/dist/ledger-B7g7jhqG.d.ts +44 -0
  121. package/dist/lib/auto-linker.js +2 -2
  122. package/dist/lib/canvas-layout.d.ts +115 -0
  123. package/dist/lib/canvas-layout.js +35 -0
  124. package/dist/lib/config.d.ts +27 -3
  125. package/dist/lib/config.js +4 -2
  126. package/dist/lib/entity-index.js +1 -1
  127. package/dist/lib/project-utils.js +4 -4
  128. package/dist/lib/session-repair.js +1 -1
  129. package/dist/lib/session-utils.js +1 -1
  130. package/dist/lib/tailscale.d.ts +225 -0
  131. package/dist/lib/tailscale.js +50 -0
  132. package/dist/lib/task-utils.js +3 -3
  133. package/dist/lib/template-engine.js +1 -1
  134. package/dist/lib/webdav.d.ts +109 -0
  135. package/dist/lib/webdav.js +35 -0
  136. package/dist/onnxruntime_binding-5QEF3SUC.node +0 -0
  137. package/dist/onnxruntime_binding-BKPKNEGC.node +0 -0
  138. package/dist/onnxruntime_binding-FMOXGIUT.node +0 -0
  139. package/dist/onnxruntime_binding-OI2KMXC5.node +0 -0
  140. package/dist/onnxruntime_binding-UX44MLAZ.node +0 -0
  141. package/dist/onnxruntime_binding-Y2W7N7WY.node +0 -0
  142. package/dist/openclaw-plugin.d.ts +8 -0
  143. package/dist/openclaw-plugin.js +14 -0
  144. package/dist/registry-BR4326o0.d.ts +30 -0
  145. package/dist/store-CA-6sKCJ.d.ts +34 -0
  146. package/dist/thread-B9LhXNU0.d.ts +41 -0
  147. package/dist/transformers.node-A2ZRORSQ.js +46775 -0
  148. package/dist/{types-Y2_Um2Ls.d.ts → types-BbWJoC1c.d.ts} +1 -44
  149. package/dist/workgraph/index.d.ts +5 -0
  150. package/dist/workgraph/index.js +23 -0
  151. package/dist/workgraph/ledger.d.ts +2 -0
  152. package/dist/workgraph/ledger.js +25 -0
  153. package/dist/workgraph/registry.d.ts +2 -0
  154. package/dist/workgraph/registry.js +19 -0
  155. package/dist/workgraph/store.d.ts +2 -0
  156. package/dist/workgraph/store.js +25 -0
  157. package/dist/workgraph/thread.d.ts +2 -0
  158. package/dist/workgraph/thread.js +25 -0
  159. package/dist/workgraph/types.d.ts +54 -0
  160. package/dist/workgraph/types.js +7 -0
  161. package/hooks/clawvault/HOOK.md +113 -0
  162. package/hooks/clawvault/handler.js +1561 -0
  163. package/hooks/clawvault/handler.test.js +510 -0
  164. package/hooks/clawvault/openclaw.plugin.json +72 -0
  165. package/openclaw.plugin.json +65 -38
  166. package/package.json +25 -22
  167. package/dist/chunk-3RG5ZIWI.js +0 -10
  168. package/dist/chunk-3ZIH425O.js +0 -871
  169. package/dist/chunk-6U6MK36V.js +0 -205
  170. package/dist/chunk-CMB7UL7C.js +0 -327
  171. package/dist/chunk-D2H45LON.js +0 -1074
  172. package/dist/chunk-E7MFQB6D.js +0 -163
  173. package/dist/chunk-GQSLDZTS.js +0 -560
  174. package/dist/chunk-MFM6K7PU.js +0 -374
  175. package/dist/chunk-MXSSG3QU.js +0 -42
  176. package/dist/chunk-OCGVIN3L.js +0 -88
  177. package/dist/chunk-PAH27GSN.js +0 -108
  178. package/dist/chunk-YCUNCH2I.js +0 -78
  179. package/dist/cli/index.cjs +0 -8584
  180. package/dist/cli/index.d.cts +0 -5
  181. package/dist/commands/archive.cjs +0 -287
  182. package/dist/commands/archive.d.cts +0 -11
  183. package/dist/commands/backlog.cjs +0 -721
  184. package/dist/commands/backlog.d.cts +0 -53
  185. package/dist/commands/blocked.cjs +0 -204
  186. package/dist/commands/blocked.d.cts +0 -26
  187. package/dist/commands/checkpoint.cjs +0 -244
  188. package/dist/commands/checkpoint.d.cts +0 -41
  189. package/dist/commands/compat.cjs +0 -294
  190. package/dist/commands/compat.d.cts +0 -28
  191. package/dist/commands/context.cjs +0 -2990
  192. package/dist/commands/context.d.cts +0 -2
  193. package/dist/commands/doctor.cjs +0 -2986
  194. package/dist/commands/doctor.d.cts +0 -21
  195. package/dist/commands/embed.cjs +0 -232
  196. package/dist/commands/embed.d.cts +0 -17
  197. package/dist/commands/entities.cjs +0 -141
  198. package/dist/commands/entities.d.cts +0 -7
  199. package/dist/commands/graph.cjs +0 -501
  200. package/dist/commands/graph.d.cts +0 -21
  201. package/dist/commands/inject.cjs +0 -1636
  202. package/dist/commands/inject.d.cts +0 -2
  203. package/dist/commands/kanban.cjs +0 -884
  204. package/dist/commands/kanban.d.cts +0 -63
  205. package/dist/commands/link.cjs +0 -965
  206. package/dist/commands/link.d.cts +0 -11
  207. package/dist/commands/migrate-observations.cjs +0 -362
  208. package/dist/commands/migrate-observations.d.cts +0 -19
  209. package/dist/commands/observe.cjs +0 -4099
  210. package/dist/commands/observe.d.cts +0 -23
  211. package/dist/commands/project.cjs +0 -1341
  212. package/dist/commands/project.d.cts +0 -85
  213. package/dist/commands/rebuild.cjs +0 -3136
  214. package/dist/commands/rebuild.d.cts +0 -11
  215. package/dist/commands/recover.cjs +0 -361
  216. package/dist/commands/recover.d.cts +0 -38
  217. package/dist/commands/reflect.cjs +0 -1008
  218. package/dist/commands/reflect.d.cts +0 -11
  219. package/dist/commands/repair-session.cjs +0 -457
  220. package/dist/commands/repair-session.d.cts +0 -38
  221. package/dist/commands/replay.cjs +0 -4103
  222. package/dist/commands/replay.d.cts +0 -16
  223. package/dist/commands/session-recap.cjs +0 -353
  224. package/dist/commands/session-recap.d.cts +0 -27
  225. package/dist/commands/setup.cjs +0 -1278
  226. package/dist/commands/setup.d.cts +0 -99
  227. package/dist/commands/shell-init.cjs +0 -75
  228. package/dist/commands/shell-init.d.cts +0 -7
  229. package/dist/commands/sleep.cjs +0 -6029
  230. package/dist/commands/sleep.d.cts +0 -36
  231. package/dist/commands/status.cjs +0 -2737
  232. package/dist/commands/status.d.cts +0 -52
  233. package/dist/commands/task.cjs +0 -1236
  234. package/dist/commands/task.d.cts +0 -97
  235. package/dist/commands/template.cjs +0 -457
  236. package/dist/commands/template.d.cts +0 -36
  237. package/dist/commands/wake.cjs +0 -2627
  238. package/dist/commands/wake.d.cts +0 -22
  239. package/dist/context-BUGaWpyL.d.cts +0 -46
  240. package/dist/index.cjs +0 -12373
  241. package/dist/index.d.cts +0 -854
  242. package/dist/inject-Bzi5E-By.d.cts +0 -137
  243. package/dist/lib/auto-linker.cjs +0 -176
  244. package/dist/lib/auto-linker.d.cts +0 -26
  245. package/dist/lib/config.cjs +0 -78
  246. package/dist/lib/config.d.cts +0 -11
  247. package/dist/lib/entity-index.cjs +0 -84
  248. package/dist/lib/entity-index.d.cts +0 -26
  249. package/dist/lib/project-utils.cjs +0 -864
  250. package/dist/lib/project-utils.d.cts +0 -97
  251. package/dist/lib/session-repair.cjs +0 -239
  252. package/dist/lib/session-repair.d.cts +0 -110
  253. package/dist/lib/session-utils.cjs +0 -209
  254. package/dist/lib/session-utils.d.cts +0 -63
  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/plugin/index.cjs +0 -1907
  260. package/dist/plugin/index.d.cts +0 -36
  261. package/dist/plugin/index.d.ts +0 -36
  262. package/dist/plugin/index.js +0 -572
  263. package/dist/plugin/inject.cjs +0 -356
  264. package/dist/plugin/inject.d.cts +0 -54
  265. package/dist/plugin/inject.d.ts +0 -54
  266. package/dist/plugin/inject.js +0 -17
  267. package/dist/plugin/observe.cjs +0 -631
  268. package/dist/plugin/observe.d.cts +0 -39
  269. package/dist/plugin/observe.d.ts +0 -39
  270. package/dist/plugin/observe.js +0 -18
  271. package/dist/plugin/templates.cjs +0 -593
  272. package/dist/plugin/templates.d.cts +0 -52
  273. package/dist/plugin/templates.d.ts +0 -52
  274. package/dist/plugin/templates.js +0 -25
  275. package/dist/plugin/types.cjs +0 -18
  276. package/dist/plugin/types.d.cts +0 -209
  277. package/dist/plugin/types.d.ts +0 -209
  278. package/dist/plugin/types.js +0 -0
  279. package/dist/plugin/vault.cjs +0 -927
  280. package/dist/plugin/vault.d.cts +0 -68
  281. package/dist/plugin/vault.d.ts +0 -68
  282. package/dist/plugin/vault.js +0 -22
  283. package/dist/types-Y2_Um2Ls.d.cts +0 -205
  284. package/templates/memory-event.md +0 -67
  285. package/templates/party.md +0 -63
  286. package/templates/primitive-registry.yaml +0 -551
  287. package/templates/run.md +0 -68
  288. package/templates/trigger.md +0 -68
  289. package/templates/workspace.md +0 -50
@@ -0,0 +1,1561 @@
1
+ /**
2
+ * ClawVault OpenClaw Hook
3
+ *
4
+ * Provides automatic context death resilience:
5
+ * - gateway:startup → detect context death, inject recovery info
6
+ * - gateway:heartbeat → cheap active-session threshold checks
7
+ * - command:new → auto-checkpoint before session reset
8
+ * - compaction:memoryFlush → force active-session flush before compaction
9
+ * - session:start → inject relevant context for first user prompt
10
+ *
11
+ * SECURITY: Uses execFileSync (no shell) to prevent command injection
12
+ */
13
+
14
+ import { execFileSync } from 'child_process';
15
+ import { createHash, randomUUID } from 'crypto';
16
+ import * as fs from 'fs';
17
+ import * as os from 'os';
18
+ import * as path from 'path';
19
+
20
+ const MAX_CONTEXT_RESULTS = 4;
21
+ const MAX_CONTEXT_PROMPT_LENGTH = 500;
22
+ const MAX_CONTEXT_SNIPPET_LENGTH = 220;
23
+ const MAX_RECAP_RESULTS = 6;
24
+ const MAX_RECAP_SNIPPET_LENGTH = 220;
25
+ const EVENT_NAME_SEPARATOR_RE = /[.:/]/g;
26
+ const OBSERVE_CURSOR_FILE = 'observe-cursors.json';
27
+ const ONE_KIB = 1024;
28
+ const ONE_MIB = ONE_KIB * ONE_KIB;
29
+ const SMALL_SESSION_THRESHOLD_BYTES = 50 * ONE_KIB;
30
+ const MEDIUM_SESSION_THRESHOLD_BYTES = 150 * ONE_KIB;
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']);
39
+
40
+ // Sanitize string for safe display (prevent prompt injection via control chars)
41
+ function sanitizeForDisplay(str) {
42
+ if (typeof str !== 'string') return '';
43
+ // Remove control characters, limit length, escape markdown
44
+ return str
45
+ .replace(/[\x00-\x1f\x7f]/g, '') // Remove control chars
46
+ .replace(/[`*_~\[\]]/g, '\\$&') // Escape markdown
47
+ .slice(0, 200); // Limit length
48
+ }
49
+
50
+ // Sanitize prompt before passing to CLI command
51
+ function sanitizePromptForContext(str) {
52
+ if (typeof str !== 'string') return '';
53
+ return str
54
+ .replace(/[\x00-\x1f\x7f]/g, ' ')
55
+ .replace(/\s+/g, ' ')
56
+ .trim()
57
+ .slice(0, MAX_CONTEXT_PROMPT_LENGTH);
58
+ }
59
+
60
+ function sanitizeSessionKey(str) {
61
+ if (typeof str !== 'string') return '';
62
+ const trimmed = str.trim();
63
+ if (!/^agent:[a-zA-Z0-9_-]+:[a-zA-Z0-9:_-]+$/.test(trimmed)) {
64
+ return '';
65
+ }
66
+ return trimmed.slice(0, 200);
67
+ }
68
+
69
+ function extractSessionKey(event) {
70
+ const candidates = [
71
+ event?.sessionKey,
72
+ event?.context?.sessionKey,
73
+ event?.session?.key,
74
+ event?.context?.session?.key,
75
+ event?.metadata?.sessionKey
76
+ ];
77
+
78
+ for (const candidate of candidates) {
79
+ const key = sanitizeSessionKey(candidate);
80
+ if (key) return key;
81
+ }
82
+
83
+ return '';
84
+ }
85
+
86
+ function extractAgentIdFromSessionKey(sessionKey) {
87
+ const match = /^agent:([^:]+):/.exec(sessionKey);
88
+ if (!match?.[1]) return '';
89
+ const agentId = match[1].trim();
90
+ if (!/^[a-zA-Z0-9_-]{1,100}$/.test(agentId)) return '';
91
+ return agentId;
92
+ }
93
+
94
+ function sanitizeAgentId(agentId) {
95
+ if (typeof agentId !== 'string') return '';
96
+ const normalized = agentId.trim();
97
+ if (!/^[a-zA-Z0-9_-]{1,100}$/.test(normalized)) return '';
98
+ return normalized;
99
+ }
100
+
101
+ function normalizeAbsoluteEnvPath(value) {
102
+ if (typeof value !== 'string') return null;
103
+ const trimmed = value.trim();
104
+ if (!trimmed) return null;
105
+ const resolved = path.resolve(trimmed);
106
+ if (!path.isAbsolute(resolved)) return null;
107
+ return resolved;
108
+ }
109
+
110
+ function getOpenClawAgentsDir() {
111
+ const stateDir = normalizeAbsoluteEnvPath(process.env.OPENCLAW_STATE_DIR);
112
+ if (stateDir) {
113
+ return path.join(stateDir, 'agents');
114
+ }
115
+
116
+ const openClawHome = normalizeAbsoluteEnvPath(process.env.OPENCLAW_HOME);
117
+ if (openClawHome) {
118
+ return path.join(openClawHome, 'agents');
119
+ }
120
+
121
+ return path.join(os.homedir(), '.openclaw', 'agents');
122
+ }
123
+
124
+ function getObserveCursorPath(vaultPath) {
125
+ return path.join(vaultPath, '.clawvault', OBSERVE_CURSOR_FILE);
126
+ }
127
+
128
+ function loadObserveCursors(vaultPath) {
129
+ const cursorPath = getObserveCursorPath(vaultPath);
130
+ if (!fs.existsSync(cursorPath)) {
131
+ return {};
132
+ }
133
+
134
+ try {
135
+ const parsed = JSON.parse(fs.readFileSync(cursorPath, 'utf-8'));
136
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
137
+ return {};
138
+ }
139
+ return parsed;
140
+ } catch {
141
+ return {};
142
+ }
143
+ }
144
+
145
+ function getScaledObservationThresholdBytes(fileSizeBytes) {
146
+ if (!Number.isFinite(fileSizeBytes) || fileSizeBytes <= 0) {
147
+ return SMALL_SESSION_THRESHOLD_BYTES;
148
+ }
149
+ if (fileSizeBytes < ONE_MIB) {
150
+ return SMALL_SESSION_THRESHOLD_BYTES;
151
+ }
152
+ if (fileSizeBytes <= 5 * ONE_MIB) {
153
+ return MEDIUM_SESSION_THRESHOLD_BYTES;
154
+ }
155
+ return LARGE_SESSION_THRESHOLD_BYTES;
156
+ }
157
+
158
+ function parseSessionIndex(agentId) {
159
+ const sessionsDir = path.join(getOpenClawAgentsDir(), agentId, 'sessions');
160
+ const sessionsJsonPath = path.join(sessionsDir, 'sessions.json');
161
+ if (!fs.existsSync(sessionsJsonPath)) {
162
+ return { sessionsDir, index: {} };
163
+ }
164
+
165
+ try {
166
+ const parsed = JSON.parse(fs.readFileSync(sessionsJsonPath, 'utf-8'));
167
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
168
+ return { sessionsDir, index: {} };
169
+ }
170
+ return { sessionsDir, index: parsed };
171
+ } catch {
172
+ return { sessionsDir, index: {} };
173
+ }
174
+ }
175
+
176
+ function shouldObserveActiveSessions(vaultPath, agentId) {
177
+ const cursors = loadObserveCursors(vaultPath);
178
+ const { sessionsDir, index } = parseSessionIndex(agentId);
179
+ const entries = Object.entries(index);
180
+ if (entries.length === 0) {
181
+ return false;
182
+ }
183
+
184
+ for (const [sessionKey, value] of entries) {
185
+ if (!value || typeof value !== 'object') continue;
186
+ const sessionId = typeof value.sessionId === 'string' ? value.sessionId.trim() : '';
187
+ if (!/^[a-zA-Z0-9._-]{1,200}$/.test(sessionId)) continue;
188
+
189
+ const filePath = path.join(sessionsDir, `${sessionId}.jsonl`);
190
+ let stat;
191
+ try {
192
+ stat = fs.statSync(filePath);
193
+ } catch {
194
+ continue;
195
+ }
196
+ if (!stat.isFile()) continue;
197
+
198
+ const fileSize = stat.size;
199
+ const cursorEntry = cursors[sessionId];
200
+ const previousOffset = Number.isFinite(cursorEntry?.lastObservedOffset)
201
+ ? Math.max(0, Number(cursorEntry.lastObservedOffset))
202
+ : 0;
203
+ const startOffset = previousOffset <= fileSize ? previousOffset : 0;
204
+ const newBytes = Math.max(0, fileSize - startOffset);
205
+ const thresholdBytes = getScaledObservationThresholdBytes(fileSize);
206
+
207
+ if (newBytes >= thresholdBytes) {
208
+ console.log(`[clawvault] Active observe trigger: ${sessionKey} (+${newBytes}B >= ${thresholdBytes}B)`);
209
+ return true;
210
+ }
211
+ }
212
+
213
+ return false;
214
+ }
215
+
216
+ function extractTextFromMessage(message) {
217
+ if (typeof message === 'string') return message;
218
+ if (!message || typeof message !== 'object') return '';
219
+
220
+ const content = message.content ?? message.text ?? message.message;
221
+ if (typeof content === 'string') return content;
222
+
223
+ if (Array.isArray(content)) {
224
+ return content
225
+ .map((part) => {
226
+ if (typeof part === 'string') return part;
227
+ if (!part || typeof part !== 'object') return '';
228
+ if (typeof part.text === 'string') return part.text;
229
+ if (typeof part.content === 'string') return part.content;
230
+ return '';
231
+ })
232
+ .filter(Boolean)
233
+ .join(' ');
234
+ }
235
+
236
+ return '';
237
+ }
238
+
239
+ function isUserMessage(message) {
240
+ if (typeof message === 'string') return true;
241
+ if (!message || typeof message !== 'object') return false;
242
+ const role = typeof message.role === 'string' ? message.role.toLowerCase() : '';
243
+ const type = typeof message.type === 'string' ? message.type.toLowerCase() : '';
244
+ return role === 'user' || role === 'human' || type === 'user';
245
+ }
246
+
247
+ function extractInitialPrompt(event) {
248
+ const fromContext = sanitizePromptForContext(event?.context?.initialPrompt);
249
+ if (fromContext) return fromContext;
250
+
251
+ const candidates = [
252
+ event?.context?.messages,
253
+ event?.context?.initialMessages,
254
+ event?.context?.history,
255
+ event?.messages
256
+ ];
257
+
258
+ for (const list of candidates) {
259
+ if (!Array.isArray(list)) continue;
260
+ for (const message of list) {
261
+ if (!isUserMessage(message)) continue;
262
+ const text = sanitizePromptForContext(extractTextFromMessage(message));
263
+ if (text) return text;
264
+ }
265
+ }
266
+
267
+ return '';
268
+ }
269
+
270
+ function truncateSnippet(snippet) {
271
+ const safe = sanitizeForDisplay(snippet).replace(/\s+/g, ' ').trim();
272
+ if (safe.length <= MAX_CONTEXT_SNIPPET_LENGTH) return safe;
273
+ return `${safe.slice(0, MAX_CONTEXT_SNIPPET_LENGTH - 3).trimEnd()}...`;
274
+ }
275
+
276
+ function truncateRecapSnippet(snippet) {
277
+ const safe = sanitizeForDisplay(snippet).replace(/\s+/g, ' ').trim();
278
+ if (safe.length <= MAX_RECAP_SNIPPET_LENGTH) return safe;
279
+ return `${safe.slice(0, MAX_RECAP_SNIPPET_LENGTH - 3).trimEnd()}...`;
280
+ }
281
+
282
+ function parseContextJson(output) {
283
+ try {
284
+ const parsed = JSON.parse(output);
285
+ if (!parsed || !Array.isArray(parsed.context)) return [];
286
+
287
+ return parsed.context
288
+ .slice(0, MAX_CONTEXT_RESULTS)
289
+ .map((entry) => ({
290
+ title: sanitizeForDisplay(entry?.title || 'Untitled'),
291
+ age: sanitizeForDisplay(entry?.age || 'unknown age'),
292
+ snippet: truncateSnippet(entry?.snippet || '')
293
+ }))
294
+ .filter((entry) => entry.snippet);
295
+ } catch {
296
+ return [];
297
+ }
298
+ }
299
+
300
+ function parseSessionRecapJson(output) {
301
+ try {
302
+ const parsed = JSON.parse(output);
303
+ if (!parsed || !Array.isArray(parsed.messages)) return [];
304
+
305
+ return parsed.messages
306
+ .map((entry) => {
307
+ if (!entry || typeof entry !== 'object') return null;
308
+ const role = typeof entry.role === 'string' ? entry.role.toLowerCase() : '';
309
+ if (role !== 'user' && role !== 'assistant') return null;
310
+ const text = truncateRecapSnippet(typeof entry.text === 'string' ? entry.text : '');
311
+ if (!text) return null;
312
+ return {
313
+ role: role === 'user' ? 'User' : 'Assistant',
314
+ text
315
+ };
316
+ })
317
+ .filter(Boolean)
318
+ .slice(-MAX_RECAP_RESULTS);
319
+ } catch {
320
+ return [];
321
+ }
322
+ }
323
+
324
+ function formatSessionContextInjection(recapEntries, memoryEntries) {
325
+ const lines = ['[ClawVault] Session context restored:', '', 'Recent conversation:'];
326
+
327
+ if (recapEntries.length === 0) {
328
+ lines.push('- No recent user/assistant turns found for this session.');
329
+ } else {
330
+ for (const entry of recapEntries) {
331
+ lines.push(`- ${entry.role}: ${entry.text}`);
332
+ }
333
+ }
334
+
335
+ lines.push('', 'Relevant memories:');
336
+ if (memoryEntries.length === 0) {
337
+ lines.push('- No relevant vault memories found for the current prompt.');
338
+ } else {
339
+ for (const entry of memoryEntries) {
340
+ lines.push(`- ${entry.title} (${entry.age}): ${entry.snippet}`);
341
+ }
342
+ }
343
+
344
+ return lines.join('\n');
345
+ }
346
+
347
+ function injectSystemMessage(event, message) {
348
+ if (!event.messages || !Array.isArray(event.messages)) return false;
349
+
350
+ if (event.messages.length === 0) {
351
+ event.messages.push(message);
352
+ return true;
353
+ }
354
+
355
+ const first = event.messages[0];
356
+ if (first && typeof first === 'object' && !Array.isArray(first)) {
357
+ if ('role' in first || 'content' in first) {
358
+ event.messages.push({ role: 'system', content: message });
359
+ return true;
360
+ }
361
+ if ('type' in first || 'text' in first) {
362
+ event.messages.push({ type: 'system', text: message });
363
+ return true;
364
+ }
365
+ }
366
+
367
+ event.messages.push(message);
368
+ return true;
369
+ }
370
+
371
+ function normalizeEventToken(value) {
372
+ if (typeof value !== 'string') return '';
373
+ return value
374
+ .trim()
375
+ .toLowerCase()
376
+ .replace(/\s+/g, '')
377
+ .replace(EVENT_NAME_SEPARATOR_RE, ':');
378
+ }
379
+
380
+ function eventMatches(event, type, action) {
381
+ const normalizedExpected = `${normalizeEventToken(type)}:${normalizeEventToken(action)}`;
382
+ const normalizedType = normalizeEventToken(event?.type);
383
+ const normalizedAction = normalizeEventToken(event?.action);
384
+
385
+ if (normalizedType && normalizedAction) {
386
+ if (`${normalizedType}:${normalizedAction}` === normalizedExpected) {
387
+ return true;
388
+ }
389
+ }
390
+
391
+ const aliases = [
392
+ event?.event,
393
+ event?.name,
394
+ event?.hook,
395
+ event?.trigger,
396
+ event?.eventName
397
+ ];
398
+
399
+ for (const alias of aliases) {
400
+ const normalizedAlias = normalizeEventToken(alias);
401
+ if (!normalizedAlias) continue;
402
+ if (normalizedAlias === normalizedExpected) {
403
+ return true;
404
+ }
405
+ }
406
+
407
+ return false;
408
+ }
409
+
410
+ function eventIncludesToken(event, token) {
411
+ const normalizedToken = normalizeEventToken(token);
412
+ if (!normalizedToken) return false;
413
+
414
+ const values = [
415
+ event?.type,
416
+ event?.action,
417
+ event?.event,
418
+ event?.name,
419
+ event?.hook,
420
+ event?.trigger,
421
+ event?.eventName
422
+ ];
423
+
424
+ return values
425
+ .map((value) => normalizeEventToken(value))
426
+ .filter(Boolean)
427
+ .some((value) => value.includes(normalizedToken));
428
+ }
429
+
430
+ // Validate vault path - must be absolute and exist
431
+ function validateVaultPath(vaultPath) {
432
+ if (!vaultPath || typeof vaultPath !== 'string') return null;
433
+
434
+ // Resolve to absolute path
435
+ const resolved = path.resolve(vaultPath);
436
+
437
+ // Must be absolute
438
+ if (!path.isAbsolute(resolved)) return null;
439
+
440
+ // Must exist and be a directory
441
+ try {
442
+ const stat = fs.statSync(resolved);
443
+ if (!stat.isDirectory()) return null;
444
+ } catch {
445
+ return null;
446
+ }
447
+
448
+ // Must contain .clawvault.json
449
+ const configPath = path.join(resolved, '.clawvault.json');
450
+ if (!fs.existsSync(configPath)) return null;
451
+
452
+ return resolved;
453
+ }
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
+
490
+ // Find vault by walking up directories
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
+ }
505
+ }
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);
516
+ if (validated) return validated;
517
+ }
518
+
519
+ // Check CLAWVAULT_PATH env
520
+ if (process.env.CLAWVAULT_PATH) {
521
+ return validateVaultPath(process.env.CLAWVAULT_PATH);
522
+ }
523
+
524
+ // Walk up from cwd
525
+ let dir = process.cwd();
526
+ const root = path.parse(dir).root;
527
+
528
+ while (dir !== root) {
529
+ const validated = validateVaultPath(dir);
530
+ if (validated) return validated;
531
+
532
+ // Also check memory/ subdirectory (OpenClaw convention)
533
+ const memoryDir = path.join(dir, 'memory');
534
+ const memoryValidated = validateVaultPath(memoryDir);
535
+ if (memoryValidated) return memoryValidated;
536
+
537
+ dir = path.dirname(dir);
538
+ }
539
+
540
+ return null;
541
+ }
542
+
543
+ // Run clawvault command safely (no shell)
544
+ function runClawvault(args, options = {}) {
545
+ const timeoutMs = Number.isFinite(options.timeoutMs) ? Math.max(1000, Number(options.timeoutMs)) : 15000;
546
+ try {
547
+ // Use execFileSync to avoid shell injection
548
+ // Arguments are passed as array, not interpolated into shell
549
+ const output = execFileSync('clawvault', args, {
550
+ encoding: 'utf-8',
551
+ timeout: timeoutMs,
552
+ stdio: ['pipe', 'pipe', 'pipe'],
553
+ // Explicitly no shell
554
+ shell: false
555
+ });
556
+ return { success: true, output: output.trim(), code: 0 };
557
+ } catch (err) {
558
+ return {
559
+ success: false,
560
+ output: err.stderr?.toString() || err.message || String(err),
561
+ code: err.status || 1
562
+ };
563
+ }
564
+ }
565
+
566
+ // Parse recovery output safely
567
+ function parseRecoveryOutput(output) {
568
+ if (!output || typeof output !== 'string') {
569
+ return { hadDeath: false, workingOn: null };
570
+ }
571
+
572
+ const hadDeath = output.includes('Context death detected') ||
573
+ output.includes('died') ||
574
+ output.includes('⚠️');
575
+
576
+ let workingOn = null;
577
+ if (hadDeath) {
578
+ const lines = output.split('\n');
579
+ const workingOnLine = lines.find(l => l.toLowerCase().includes('working on'));
580
+ if (workingOnLine) {
581
+ const parts = workingOnLine.split(':');
582
+ if (parts.length > 1) {
583
+ workingOn = sanitizeForDisplay(parts.slice(1).join(':').trim());
584
+ }
585
+ }
586
+ }
587
+
588
+ return { hadDeath, workingOn };
589
+ }
590
+
591
+ function resolveAgentIdForEvent(event) {
592
+ const fromSessionKey = extractAgentIdFromSessionKey(extractSessionKey(event));
593
+ if (fromSessionKey) return fromSessionKey;
594
+
595
+ const fromEnv = sanitizeAgentId(process.env.OPENCLAW_AGENT_ID);
596
+ if (fromEnv) return fromEnv;
597
+
598
+ return 'main';
599
+ }
600
+
601
+ function runObserverCron(vaultPath, agentId, options = {}) {
602
+ const args = ['observe', '--cron', '--agent', agentId, '-v', vaultPath];
603
+ if (Number.isFinite(options.minNewBytes) && Number(options.minNewBytes) > 0) {
604
+ args.push('--min-new', String(Math.floor(Number(options.minNewBytes))));
605
+ }
606
+
607
+ const result = runClawvault(args, { timeoutMs: 120000 });
608
+ if (!result.success) {
609
+ console.warn(`[clawvault] Observer cron failed (${options.reason || 'unknown reason'})`);
610
+ return false;
611
+ }
612
+
613
+ if (result.output) {
614
+ console.log(`[clawvault] Observer cron: ${result.output}`);
615
+ } else {
616
+ console.log('[clawvault] Observer cron: complete');
617
+ }
618
+ return true;
619
+ }
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
+
1294
+ function extractEventTimestamp(event) {
1295
+ const candidates = [
1296
+ event?.timestamp,
1297
+ event?.scheduledAt,
1298
+ event?.time,
1299
+ event?.context?.timestamp,
1300
+ event?.context?.scheduledAt
1301
+ ];
1302
+ for (const candidate of candidates) {
1303
+ if (!candidate) continue;
1304
+ const parsed = new Date(candidate);
1305
+ if (!Number.isNaN(parsed.getTime())) {
1306
+ return parsed;
1307
+ }
1308
+ }
1309
+ return null;
1310
+ }
1311
+
1312
+ function isSundayMidnightUtc(date) {
1313
+ return date.getUTCDay() === 0 && date.getUTCHours() === 0 && date.getUTCMinutes() === 0;
1314
+ }
1315
+
1316
+ async function handleWeeklyReflect(event) {
1317
+ const vaultPath = findVaultPath(event);
1318
+ if (!vaultPath) {
1319
+ console.log('[clawvault] No vault found, skipping weekly reflection');
1320
+ return;
1321
+ }
1322
+
1323
+ const timestamp = extractEventTimestamp(event) || new Date();
1324
+ if (!isSundayMidnightUtc(timestamp)) {
1325
+ console.log('[clawvault] Weekly reflect skipped (not Sunday midnight UTC)');
1326
+ return;
1327
+ }
1328
+
1329
+ const result = runClawvault(['reflect', '-v', vaultPath], { timeoutMs: 120000 });
1330
+ if (!result.success) {
1331
+ console.warn('[clawvault] Weekly reflection failed');
1332
+ return;
1333
+ }
1334
+ console.log('[clawvault] Weekly reflection complete');
1335
+ }
1336
+
1337
+ // Handle gateway startup - check for context death
1338
+ async function handleStartup(event) {
1339
+ const vaultPath = findVaultPath(event);
1340
+ if (!vaultPath) {
1341
+ console.log('[clawvault] No vault found, skipping recovery check');
1342
+ return;
1343
+ }
1344
+
1345
+ console.log(`[clawvault] Checking for context death`);
1346
+
1347
+ // Pass vault path as separate argument (not interpolated)
1348
+ const result = runClawvault(['recover', '--clear', '-v', vaultPath]);
1349
+
1350
+ if (!result.success) {
1351
+ console.warn('[clawvault] Recovery check failed');
1352
+ return;
1353
+ }
1354
+
1355
+ const { hadDeath, workingOn } = parseRecoveryOutput(result.output);
1356
+
1357
+ if (hadDeath) {
1358
+ // Build safe alert message with sanitized content
1359
+ const alertParts = ['[ClawVault] Context death detected.'];
1360
+ if (workingOn) {
1361
+ alertParts.push(`Last working on: ${workingOn}`);
1362
+ }
1363
+ alertParts.push('Run `clawvault wake` for full recovery context.');
1364
+
1365
+ const alertMsg = alertParts.join(' ');
1366
+
1367
+ // Inject into event messages if available
1368
+ if (injectSystemMessage(event, alertMsg)) {
1369
+ console.warn('[clawvault] Context death detected, alert injected');
1370
+ }
1371
+ } else {
1372
+ console.log('[clawvault] Clean startup - no context death');
1373
+ }
1374
+ }
1375
+
1376
+ // Handle /new command - auto-checkpoint before reset
1377
+ async function handleNew(event) {
1378
+ const vaultPath = findVaultPath(event);
1379
+ if (!vaultPath) {
1380
+ console.log('[clawvault] No vault found, skipping auto-checkpoint');
1381
+ return;
1382
+ }
1383
+
1384
+ // Sanitize session info for checkpoint
1385
+ const sessionKey = typeof event.sessionKey === 'string'
1386
+ ? event.sessionKey.replace(/[^a-zA-Z0-9:_-]/g, '').slice(0, 100)
1387
+ : 'unknown';
1388
+ const source = typeof event.context?.commandSource === 'string'
1389
+ ? event.context.commandSource.replace(/[^a-zA-Z0-9_-]/g, '').slice(0, 50)
1390
+ : 'cli';
1391
+
1392
+ console.log('[clawvault] Auto-checkpoint before /new');
1393
+
1394
+ // Pass each argument separately (no shell interpolation)
1395
+ const result = runClawvault([
1396
+ 'checkpoint',
1397
+ '--working-on', `Session reset via /new from ${source}`,
1398
+ '--focus', `Pre-reset checkpoint, session: ${sessionKey}`,
1399
+ '-v', vaultPath
1400
+ ]);
1401
+
1402
+ if (result.success) {
1403
+ console.log('[clawvault] Auto-checkpoint created');
1404
+ } else {
1405
+ console.warn('[clawvault] Auto-checkpoint failed');
1406
+ }
1407
+
1408
+ const agentId = resolveAgentIdForEvent(event);
1409
+ runObserverCron(vaultPath, agentId, {
1410
+ minNewBytes: 1,
1411
+ reason: 'command:new flush'
1412
+ });
1413
+ runFactExtractionForEvent(vaultPath, event, 'command:new');
1414
+ }
1415
+
1416
+ // Handle session start - inject dynamic context for first prompt
1417
+ async function handleSessionStart(event) {
1418
+ const vaultPath = findVaultPath(event);
1419
+ if (!vaultPath) {
1420
+ console.log('[clawvault] No vault found, skipping context injection');
1421
+ return;
1422
+ }
1423
+
1424
+ const sessionKey = extractSessionKey(event);
1425
+ const prompt = extractInitialPrompt(event);
1426
+ let recapEntries = [];
1427
+ let memoryEntries = [];
1428
+
1429
+ if (sessionKey) {
1430
+ console.log('[clawvault] Fetching session recap for context restoration');
1431
+ const recapArgs = ['session-recap', sessionKey, '--format', 'json'];
1432
+ const agentId = extractAgentIdFromSessionKey(sessionKey);
1433
+ if (agentId) {
1434
+ recapArgs.push('--agent', agentId);
1435
+ }
1436
+
1437
+ const recapResult = runClawvault(recapArgs);
1438
+ if (!recapResult.success) {
1439
+ console.warn('[clawvault] Session recap lookup failed');
1440
+ } else {
1441
+ recapEntries = parseSessionRecapJson(recapResult.output);
1442
+ }
1443
+ } else {
1444
+ console.log('[clawvault] No session key found, skipping session recap');
1445
+ }
1446
+
1447
+ if (prompt) {
1448
+ console.log('[clawvault] Fetching vault memories for session start prompt');
1449
+ const contextResult = runClawvault([
1450
+ 'context',
1451
+ prompt,
1452
+ '--format', 'json',
1453
+ '--profile', 'auto',
1454
+ '-v', vaultPath
1455
+ ]);
1456
+
1457
+ if (!contextResult.success) {
1458
+ console.warn('[clawvault] Context lookup failed');
1459
+ } else {
1460
+ memoryEntries = parseContextJson(contextResult.output);
1461
+ }
1462
+ } else {
1463
+ console.log('[clawvault] No initial prompt, skipping vault memory lookup');
1464
+ }
1465
+
1466
+ if (recapEntries.length === 0 && memoryEntries.length === 0) {
1467
+ console.log('[clawvault] No session context available to inject');
1468
+ return;
1469
+ }
1470
+
1471
+ if (injectSystemMessage(event, formatSessionContextInjection(recapEntries, memoryEntries))) {
1472
+ console.log(`[clawvault] Injected session context (${recapEntries.length} recap, ${memoryEntries.length} memories)`);
1473
+ } else {
1474
+ console.log('[clawvault] No message array available, skipping injection');
1475
+ }
1476
+ }
1477
+
1478
+ // Handle heartbeat events - cheap stat-based trigger for active observation
1479
+ async function handleHeartbeat(event) {
1480
+ const vaultPath = findVaultPath(event);
1481
+ if (!vaultPath) {
1482
+ console.log('[clawvault] No vault found, skipping heartbeat observation check');
1483
+ return;
1484
+ }
1485
+
1486
+ const agentId = resolveAgentIdForEvent(event);
1487
+ if (!shouldObserveActiveSessions(vaultPath, agentId)) {
1488
+ console.log('[clawvault] Heartbeat: no sessions crossed active-observe threshold');
1489
+ return;
1490
+ }
1491
+
1492
+ runObserverCron(vaultPath, agentId, { reason: 'heartbeat threshold crossed' });
1493
+ }
1494
+
1495
+ // Handle context compaction - force flush any pending session deltas
1496
+ async function handleContextCompaction(event) {
1497
+ const vaultPath = findVaultPath(event);
1498
+ if (!vaultPath) {
1499
+ console.log('[clawvault] No vault found, skipping compaction observation');
1500
+ return;
1501
+ }
1502
+
1503
+ const agentId = resolveAgentIdForEvent(event);
1504
+ runObserverCron(vaultPath, agentId, {
1505
+ minNewBytes: 1,
1506
+ reason: 'context compaction'
1507
+ });
1508
+ runFactExtractionForEvent(vaultPath, event, 'compaction:memoryFlush');
1509
+ }
1510
+
1511
+ // Main handler - route events
1512
+ const handler = async (event) => {
1513
+ try {
1514
+ if (eventMatches(event, 'gateway', 'startup')) {
1515
+ await handleStartup(event);
1516
+ return;
1517
+ }
1518
+
1519
+ if (
1520
+ eventMatches(event, 'cron', 'weekly')
1521
+ || eventIncludesToken(event, 'cron:weekly')
1522
+ ) {
1523
+ await handleWeeklyReflect(event);
1524
+ return;
1525
+ }
1526
+
1527
+ if (
1528
+ eventMatches(event, 'gateway', 'heartbeat')
1529
+ || eventMatches(event, 'session', 'heartbeat')
1530
+ || eventIncludesToken(event, 'heartbeat')
1531
+ ) {
1532
+ await handleHeartbeat(event);
1533
+ return;
1534
+ }
1535
+
1536
+ if (
1537
+ eventMatches(event, 'compaction', 'memoryflush')
1538
+ || eventMatches(event, 'context', 'compaction')
1539
+ || eventMatches(event, 'context', 'compact')
1540
+ || eventIncludesToken(event, 'compaction')
1541
+ || eventIncludesToken(event, 'memoryflush')
1542
+ ) {
1543
+ await handleContextCompaction(event);
1544
+ return;
1545
+ }
1546
+
1547
+ if (eventMatches(event, 'command', 'new')) {
1548
+ await handleNew(event);
1549
+ return;
1550
+ }
1551
+
1552
+ if (eventMatches(event, 'session', 'start')) {
1553
+ await handleSessionStart(event);
1554
+ return;
1555
+ }
1556
+ } catch (err) {
1557
+ console.error('[clawvault] Hook error:', err.message || 'unknown error');
1558
+ }
1559
+ };
1560
+
1561
+ export default handler;