@hybridaione/hybridclaw 0.2.2 → 0.2.6

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 (277) hide show
  1. package/.github/workflows/ci.yml +70 -0
  2. package/.husky/pre-commit +1 -0
  3. package/CHANGELOG.md +85 -0
  4. package/CONTRIBUTING.md +33 -0
  5. package/README.md +41 -16
  6. package/SECURITY.md +17 -0
  7. package/biome.json +35 -0
  8. package/config.example.json +71 -8
  9. package/container/package-lock.json +2 -2
  10. package/container/package.json +1 -1
  11. package/container/src/approval-policy.ts +1303 -0
  12. package/container/src/browser-tools.ts +431 -136
  13. package/container/src/extensions.ts +36 -12
  14. package/container/src/hybridai-client.ts +34 -13
  15. package/container/src/index.ts +451 -109
  16. package/container/src/ipc.ts +5 -3
  17. package/container/src/token-usage.ts +20 -10
  18. package/container/src/tools.ts +599 -225
  19. package/container/src/types.ts +32 -2
  20. package/container/src/web-fetch.ts +89 -32
  21. package/dist/agent.d.ts.map +1 -1
  22. package/dist/agent.js +10 -2
  23. package/dist/agent.js.map +1 -1
  24. package/dist/audit-cli.d.ts.map +1 -1
  25. package/dist/audit-cli.js +4 -2
  26. package/dist/audit-cli.js.map +1 -1
  27. package/dist/audit-events.d.ts.map +1 -1
  28. package/dist/audit-events.js +53 -3
  29. package/dist/audit-events.js.map +1 -1
  30. package/dist/audit-trail.d.ts.map +1 -1
  31. package/dist/audit-trail.js +17 -8
  32. package/dist/audit-trail.js.map +1 -1
  33. package/dist/channels/discord/attachments.d.ts.map +1 -1
  34. package/dist/channels/discord/attachments.js +14 -7
  35. package/dist/channels/discord/attachments.js.map +1 -1
  36. package/dist/channels/discord/debounce.d.ts +9 -0
  37. package/dist/channels/discord/debounce.d.ts.map +1 -0
  38. package/dist/channels/discord/debounce.js +20 -0
  39. package/dist/channels/discord/debounce.js.map +1 -0
  40. package/dist/channels/discord/delivery.d.ts +4 -1
  41. package/dist/channels/discord/delivery.d.ts.map +1 -1
  42. package/dist/channels/discord/delivery.js +19 -3
  43. package/dist/channels/discord/delivery.js.map +1 -1
  44. package/dist/channels/discord/human-delay.d.ts +16 -0
  45. package/dist/channels/discord/human-delay.d.ts.map +1 -0
  46. package/dist/channels/discord/human-delay.js +29 -0
  47. package/dist/channels/discord/human-delay.js.map +1 -0
  48. package/dist/channels/discord/inbound.d.ts +4 -0
  49. package/dist/channels/discord/inbound.d.ts.map +1 -1
  50. package/dist/channels/discord/inbound.js +45 -4
  51. package/dist/channels/discord/inbound.js.map +1 -1
  52. package/dist/channels/discord/mentions.d.ts.map +1 -1
  53. package/dist/channels/discord/mentions.js +16 -4
  54. package/dist/channels/discord/mentions.js.map +1 -1
  55. package/dist/channels/discord/presence.d.ts +33 -0
  56. package/dist/channels/discord/presence.d.ts.map +1 -0
  57. package/dist/channels/discord/presence.js +111 -0
  58. package/dist/channels/discord/presence.js.map +1 -0
  59. package/dist/channels/discord/rate-limiter.d.ts +14 -0
  60. package/dist/channels/discord/rate-limiter.d.ts.map +1 -0
  61. package/dist/channels/discord/rate-limiter.js +49 -0
  62. package/dist/channels/discord/rate-limiter.js.map +1 -0
  63. package/dist/channels/discord/reactions.d.ts +38 -0
  64. package/dist/channels/discord/reactions.d.ts.map +1 -0
  65. package/dist/channels/discord/reactions.js +151 -0
  66. package/dist/channels/discord/reactions.js.map +1 -0
  67. package/dist/channels/discord/runtime.d.ts +6 -3
  68. package/dist/channels/discord/runtime.d.ts.map +1 -1
  69. package/dist/channels/discord/runtime.js +621 -125
  70. package/dist/channels/discord/runtime.js.map +1 -1
  71. package/dist/channels/discord/stream.d.ts +4 -1
  72. package/dist/channels/discord/stream.d.ts.map +1 -1
  73. package/dist/channels/discord/stream.js +16 -8
  74. package/dist/channels/discord/stream.js.map +1 -1
  75. package/dist/channels/discord/tool-actions.d.ts.map +1 -1
  76. package/dist/channels/discord/tool-actions.js +24 -12
  77. package/dist/channels/discord/tool-actions.js.map +1 -1
  78. package/dist/channels/discord/typing.d.ts +15 -0
  79. package/dist/channels/discord/typing.d.ts.map +1 -0
  80. package/dist/channels/discord/typing.js +106 -0
  81. package/dist/channels/discord/typing.js.map +1 -0
  82. package/dist/chunk.d.ts.map +1 -1
  83. package/dist/chunk.js +4 -2
  84. package/dist/chunk.js.map +1 -1
  85. package/dist/cli.js +47 -22
  86. package/dist/cli.js.map +1 -1
  87. package/dist/config.d.ts +19 -0
  88. package/dist/config.d.ts.map +1 -1
  89. package/dist/config.js +103 -18
  90. package/dist/config.js.map +1 -1
  91. package/dist/container-runner.d.ts.map +1 -1
  92. package/dist/container-runner.js +58 -26
  93. package/dist/container-runner.js.map +1 -1
  94. package/dist/container-setup.d.ts.map +1 -1
  95. package/dist/container-setup.js +10 -9
  96. package/dist/container-setup.js.map +1 -1
  97. package/dist/conversation.d.ts +2 -2
  98. package/dist/conversation.d.ts.map +1 -1
  99. package/dist/conversation.js +1 -1
  100. package/dist/conversation.js.map +1 -1
  101. package/dist/db.d.ts +118 -2
  102. package/dist/db.d.ts.map +1 -1
  103. package/dist/db.js +1568 -50
  104. package/dist/db.js.map +1 -1
  105. package/dist/delegation-manager.d.ts.map +1 -1
  106. package/dist/delegation-manager.js +3 -2
  107. package/dist/delegation-manager.js.map +1 -1
  108. package/dist/gateway-client.d.ts +2 -2
  109. package/dist/gateway-client.d.ts.map +1 -1
  110. package/dist/gateway-client.js +10 -4
  111. package/dist/gateway-client.js.map +1 -1
  112. package/dist/gateway-service.d.ts +3 -3
  113. package/dist/gateway-service.d.ts.map +1 -1
  114. package/dist/gateway-service.js +563 -73
  115. package/dist/gateway-service.js.map +1 -1
  116. package/dist/gateway-types.d.ts +24 -0
  117. package/dist/gateway-types.d.ts.map +1 -1
  118. package/dist/gateway-types.js.map +1 -1
  119. package/dist/gateway.js +179 -24
  120. package/dist/gateway.js.map +1 -1
  121. package/dist/health.d.ts.map +1 -1
  122. package/dist/health.js +20 -10
  123. package/dist/health.js.map +1 -1
  124. package/dist/heartbeat.d.ts +4 -0
  125. package/dist/heartbeat.d.ts.map +1 -1
  126. package/dist/heartbeat.js +48 -20
  127. package/dist/heartbeat.js.map +1 -1
  128. package/dist/hybridai-bots.d.ts.map +1 -1
  129. package/dist/hybridai-bots.js +4 -2
  130. package/dist/hybridai-bots.js.map +1 -1
  131. package/dist/instruction-approval-audit.d.ts.map +1 -1
  132. package/dist/instruction-approval-audit.js.map +1 -1
  133. package/dist/instruction-integrity.d.ts.map +1 -1
  134. package/dist/instruction-integrity.js +8 -2
  135. package/dist/instruction-integrity.js.map +1 -1
  136. package/dist/ipc.d.ts.map +1 -1
  137. package/dist/ipc.js +6 -1
  138. package/dist/ipc.js.map +1 -1
  139. package/dist/logger.js.map +1 -1
  140. package/dist/memory-consolidation.d.ts +17 -0
  141. package/dist/memory-consolidation.d.ts.map +1 -0
  142. package/dist/memory-consolidation.js +25 -0
  143. package/dist/memory-consolidation.js.map +1 -0
  144. package/dist/memory-service.d.ts +200 -0
  145. package/dist/memory-service.d.ts.map +1 -0
  146. package/dist/memory-service.js +294 -0
  147. package/dist/memory-service.js.map +1 -0
  148. package/dist/mount-security.d.ts.map +1 -1
  149. package/dist/mount-security.js +31 -7
  150. package/dist/mount-security.js.map +1 -1
  151. package/dist/observability-ingest.d.ts.map +1 -1
  152. package/dist/observability-ingest.js +32 -11
  153. package/dist/observability-ingest.js.map +1 -1
  154. package/dist/onboarding.d.ts.map +1 -1
  155. package/dist/onboarding.js +32 -9
  156. package/dist/onboarding.js.map +1 -1
  157. package/dist/proactive-policy.d.ts.map +1 -1
  158. package/dist/proactive-policy.js +2 -1
  159. package/dist/proactive-policy.js.map +1 -1
  160. package/dist/prompt-hooks.d.ts.map +1 -1
  161. package/dist/prompt-hooks.js +9 -7
  162. package/dist/prompt-hooks.js.map +1 -1
  163. package/dist/runtime-config.d.ts +98 -1
  164. package/dist/runtime-config.d.ts.map +1 -1
  165. package/dist/runtime-config.js +477 -23
  166. package/dist/runtime-config.js.map +1 -1
  167. package/dist/scheduled-task-runner.d.ts +1 -0
  168. package/dist/scheduled-task-runner.d.ts.map +1 -1
  169. package/dist/scheduled-task-runner.js +29 -10
  170. package/dist/scheduled-task-runner.js.map +1 -1
  171. package/dist/scheduler.d.ts +43 -4
  172. package/dist/scheduler.d.ts.map +1 -1
  173. package/dist/scheduler.js +530 -56
  174. package/dist/scheduler.js.map +1 -1
  175. package/dist/session-export.d.ts +26 -0
  176. package/dist/session-export.d.ts.map +1 -0
  177. package/dist/session-export.js +149 -0
  178. package/dist/session-export.js.map +1 -0
  179. package/dist/session-maintenance.d.ts.map +1 -1
  180. package/dist/session-maintenance.js +75 -13
  181. package/dist/session-maintenance.js.map +1 -1
  182. package/dist/session-transcripts.d.ts.map +1 -1
  183. package/dist/session-transcripts.js.map +1 -1
  184. package/dist/side-effects.d.ts.map +1 -1
  185. package/dist/side-effects.js +14 -2
  186. package/dist/side-effects.js.map +1 -1
  187. package/dist/skills-guard.d.ts.map +1 -1
  188. package/dist/skills-guard.js +893 -130
  189. package/dist/skills-guard.js.map +1 -1
  190. package/dist/skills.d.ts +5 -0
  191. package/dist/skills.d.ts.map +1 -1
  192. package/dist/skills.js +29 -15
  193. package/dist/skills.js.map +1 -1
  194. package/dist/token-efficiency.d.ts.map +1 -1
  195. package/dist/token-efficiency.js.map +1 -1
  196. package/dist/tui.js +92 -11
  197. package/dist/tui.js.map +1 -1
  198. package/dist/types.d.ts +146 -0
  199. package/dist/types.d.ts.map +1 -1
  200. package/dist/types.js +24 -1
  201. package/dist/types.js.map +1 -1
  202. package/dist/update.d.ts.map +1 -1
  203. package/dist/update.js +42 -14
  204. package/dist/update.js.map +1 -1
  205. package/dist/workspace.d.ts.map +1 -1
  206. package/dist/workspace.js +49 -9
  207. package/dist/workspace.js.map +1 -1
  208. package/docs/chat.html +9 -3
  209. package/docs/index.html +37 -13
  210. package/package.json +8 -2
  211. package/src/agent.ts +16 -3
  212. package/src/audit-cli.ts +44 -16
  213. package/src/audit-events.ts +69 -5
  214. package/src/audit-trail.ts +41 -15
  215. package/src/channels/discord/attachments.ts +81 -27
  216. package/src/channels/discord/debounce.ts +25 -0
  217. package/src/channels/discord/delivery.ts +57 -13
  218. package/src/channels/discord/human-delay.ts +48 -0
  219. package/src/channels/discord/inbound.ts +66 -7
  220. package/src/channels/discord/mentions.ts +42 -18
  221. package/src/channels/discord/presence.ts +148 -0
  222. package/src/channels/discord/rate-limiter.ts +58 -0
  223. package/src/channels/discord/reactions.ts +211 -0
  224. package/src/channels/discord/runtime.ts +1048 -182
  225. package/src/channels/discord/stream.ts +73 -27
  226. package/src/channels/discord/tool-actions.ts +78 -37
  227. package/src/channels/discord/typing.ts +140 -0
  228. package/src/chunk.ts +12 -4
  229. package/src/cli.ts +141 -56
  230. package/src/config.ts +192 -34
  231. package/src/container-runner.ts +132 -42
  232. package/src/container-setup.ts +57 -22
  233. package/src/conversation.ts +9 -7
  234. package/src/db.ts +2217 -84
  235. package/src/delegation-manager.ts +6 -2
  236. package/src/gateway-client.ts +41 -17
  237. package/src/gateway-service.ts +1019 -201
  238. package/src/gateway-types.ts +33 -0
  239. package/src/gateway.ts +321 -48
  240. package/src/health.ts +66 -26
  241. package/src/heartbeat.ts +84 -22
  242. package/src/hybridai-bots.ts +14 -5
  243. package/src/instruction-approval-audit.ts +4 -1
  244. package/src/instruction-integrity.ts +30 -9
  245. package/src/ipc.ts +23 -5
  246. package/src/logger.ts +4 -1
  247. package/src/memory-consolidation.ts +41 -0
  248. package/src/memory-service.ts +606 -0
  249. package/src/mount-security.ts +58 -13
  250. package/src/observability-ingest.ts +134 -35
  251. package/src/onboarding.ts +126 -35
  252. package/src/proactive-policy.ts +3 -1
  253. package/src/prompt-hooks.ts +40 -17
  254. package/src/runtime-config.ts +1114 -99
  255. package/src/scheduled-task-runner.ts +63 -11
  256. package/src/scheduler.ts +683 -60
  257. package/src/session-export.ts +196 -0
  258. package/src/session-maintenance.ts +125 -22
  259. package/src/session-transcripts.ts +12 -3
  260. package/src/side-effects.ts +28 -5
  261. package/src/skills-guard.ts +1067 -219
  262. package/src/skills.ts +163 -65
  263. package/src/token-efficiency.ts +31 -9
  264. package/src/tui.ts +166 -25
  265. package/src/types.ts +195 -2
  266. package/src/update.ts +79 -23
  267. package/src/workspace.ts +63 -11
  268. package/tests/approval-policy.test.ts +224 -0
  269. package/tests/discord.basic.test.ts +82 -2
  270. package/tests/discord.human-presence.test.ts +85 -0
  271. package/tests/gateway-service.media-routing.test.ts +8 -2
  272. package/tests/memory-service.test.ts +1114 -0
  273. package/tests/token-efficiency.basic.test.ts +8 -2
  274. package/vitest.e2e.config.ts +3 -1
  275. package/vitest.integration.config.ts +3 -1
  276. package/vitest.live.config.ts +3 -1
  277. package/vitest.unit.config.ts +9 -0
package/dist/db.js CHANGED
@@ -1,10 +1,60 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
1
4
  import Database from 'better-sqlite3';
2
- import fs from 'fs';
3
- import path from 'path';
4
5
  import { DB_PATH } from './config.js';
5
6
  import { logger } from './logger.js';
7
+ import { KnowledgeEntityType, KnowledgeRelationType } from './types.js';
6
8
  let db;
7
- function createSchema(database) {
9
+ const SCHEMA_VERSION = 4;
10
+ function getSchemaVersion(database) {
11
+ const raw = database.pragma('user_version', { simple: true });
12
+ const value = typeof raw === 'number' ? raw : Number(raw);
13
+ if (!Number.isFinite(value))
14
+ return 0;
15
+ return Math.max(0, Math.trunc(value));
16
+ }
17
+ function setSchemaVersion(database, version) {
18
+ const bounded = Math.max(0, Math.trunc(version));
19
+ database.pragma(`user_version = ${bounded}`);
20
+ }
21
+ function tableExists(database, table) {
22
+ const row = database
23
+ .prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?")
24
+ .get(table);
25
+ return Boolean(row?.name);
26
+ }
27
+ function columnExists(database, table, column) {
28
+ const cols = database.pragma(`table_info(${table})`);
29
+ return cols.some((entry) => entry.name === column);
30
+ }
31
+ function addColumnIfMissing(params) {
32
+ if (!tableExists(params.database, params.table))
33
+ return;
34
+ if (columnExists(params.database, params.table, params.column))
35
+ return;
36
+ params.database.exec(`ALTER TABLE ${params.table} ADD COLUMN ${params.ddl}`);
37
+ if (!params.quiet) {
38
+ logger.info({ table: params.table, column: params.column }, 'Migrated table: added column');
39
+ }
40
+ }
41
+ function ensureMigrationTable(database) {
42
+ database.exec(`
43
+ CREATE TABLE IF NOT EXISTS migrations (
44
+ version INTEGER PRIMARY KEY,
45
+ applied_at TEXT NOT NULL,
46
+ description TEXT
47
+ );
48
+ `);
49
+ }
50
+ function recordMigration(database, version, description) {
51
+ ensureMigrationTable(database);
52
+ database
53
+ .prepare(`INSERT OR IGNORE INTO migrations (version, applied_at, description)
54
+ VALUES (?, datetime('now'), ?)`)
55
+ .run(version, description);
56
+ }
57
+ function migrateV1(database) {
8
58
  database.exec(`
9
59
  CREATE TABLE IF NOT EXISTS sessions (
10
60
  id TEXT PRIMARY KEY,
@@ -33,14 +83,45 @@ function createSchema(database) {
33
83
  );
34
84
  CREATE INDEX IF NOT EXISTS idx_messages_session ON messages(session_id);
35
85
 
86
+ CREATE TABLE IF NOT EXISTS semantic_memories (
87
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
88
+ session_id TEXT NOT NULL,
89
+ role TEXT NOT NULL,
90
+ source TEXT NOT NULL DEFAULT 'conversation',
91
+ scope TEXT NOT NULL DEFAULT 'episodic',
92
+ metadata TEXT NOT NULL DEFAULT '{}',
93
+ content TEXT NOT NULL,
94
+ confidence REAL NOT NULL DEFAULT 1.0,
95
+ embedding BLOB,
96
+ source_message_id INTEGER,
97
+ created_at TEXT DEFAULT (datetime('now')),
98
+ accessed_at TEXT DEFAULT (datetime('now')),
99
+ access_count INTEGER NOT NULL DEFAULT 0,
100
+ deleted INTEGER NOT NULL DEFAULT 0
101
+ );
102
+
103
+ CREATE TABLE IF NOT EXISTS kv_store (
104
+ agent_id TEXT NOT NULL,
105
+ key TEXT NOT NULL,
106
+ value BLOB NOT NULL,
107
+ version INTEGER NOT NULL DEFAULT 1,
108
+ updated_at TEXT NOT NULL,
109
+ PRIMARY KEY (agent_id, key)
110
+ );
111
+ CREATE INDEX IF NOT EXISTS idx_kv_store_agent ON kv_store(agent_id);
112
+
36
113
  CREATE TABLE IF NOT EXISTS tasks (
37
114
  id INTEGER PRIMARY KEY AUTOINCREMENT,
38
115
  session_id TEXT NOT NULL,
39
116
  channel_id TEXT NOT NULL,
40
117
  cron_expr TEXT NOT NULL,
118
+ run_at TEXT,
119
+ every_ms INTEGER,
41
120
  prompt TEXT NOT NULL,
42
121
  enabled INTEGER DEFAULT 1,
43
122
  last_run TEXT,
123
+ last_status TEXT,
124
+ consecutive_errors INTEGER DEFAULT 0,
44
125
  created_at TEXT DEFAULT (datetime('now'))
45
126
  );
46
127
 
@@ -107,67 +188,1015 @@ function createSchema(database) {
107
188
  queued_at TEXT DEFAULT (datetime('now'))
108
189
  );
109
190
  CREATE INDEX IF NOT EXISTS idx_proactive_queue_id ON proactive_message_queue(id);
191
+
192
+ CREATE TABLE IF NOT EXISTS migrations (
193
+ version INTEGER PRIMARY KEY,
194
+ applied_at TEXT NOT NULL,
195
+ description TEXT
196
+ );
110
197
  `);
198
+ recordMigration(database, 1, 'Initial schema');
111
199
  }
112
- function migrateSchema(database, opts) {
200
+ function migrateV2(database, opts) {
113
201
  const quiet = opts?.quiet === true;
114
- const addColumnIfMissing = (table, column, ddl) => {
115
- const cols = database.pragma(`table_info(${table})`);
116
- if (!cols.some((c) => c.name === column)) {
117
- database.exec(`ALTER TABLE ${table} ADD COLUMN ${ddl}`);
118
- if (!quiet)
119
- logger.info({ table, column }, 'Migrated table: added column');
120
- }
121
- };
122
- // Add session columns if they don't exist
123
- const sessionCols = database.pragma('table_info(sessions)');
124
- if (!sessionCols.some((c) => c.name === 'model')) {
125
- database.exec('ALTER TABLE sessions ADD COLUMN model TEXT');
126
- if (!quiet)
127
- logger.info('Migrated sessions table: added model column');
128
- }
129
- addColumnIfMissing('sessions', 'session_summary', 'session_summary TEXT');
130
- addColumnIfMissing('sessions', 'summary_updated_at', 'summary_updated_at TEXT');
131
- addColumnIfMissing('sessions', 'compaction_count', 'compaction_count INTEGER DEFAULT 0');
132
- addColumnIfMissing('sessions', 'memory_flush_at', 'memory_flush_at TEXT');
133
- // Add run_at and every_ms columns to tasks if they don't exist
134
- const taskCols = database.pragma('table_info(tasks)');
135
- if (!taskCols.some((c) => c.name === 'run_at')) {
136
- database.exec('ALTER TABLE tasks ADD COLUMN run_at TEXT');
202
+ addColumnIfMissing({
203
+ database,
204
+ table: 'sessions',
205
+ column: 'model',
206
+ ddl: 'model TEXT',
207
+ quiet,
208
+ });
209
+ addColumnIfMissing({
210
+ database,
211
+ table: 'sessions',
212
+ column: 'session_summary',
213
+ ddl: 'session_summary TEXT',
214
+ quiet,
215
+ });
216
+ addColumnIfMissing({
217
+ database,
218
+ table: 'sessions',
219
+ column: 'summary_updated_at',
220
+ ddl: 'summary_updated_at TEXT',
221
+ quiet,
222
+ });
223
+ addColumnIfMissing({
224
+ database,
225
+ table: 'sessions',
226
+ column: 'compaction_count',
227
+ ddl: 'compaction_count INTEGER DEFAULT 0',
228
+ quiet,
229
+ });
230
+ addColumnIfMissing({
231
+ database,
232
+ table: 'sessions',
233
+ column: 'memory_flush_at',
234
+ ddl: 'memory_flush_at TEXT',
235
+ quiet,
236
+ });
237
+ addColumnIfMissing({
238
+ database,
239
+ table: 'tasks',
240
+ column: 'run_at',
241
+ ddl: 'run_at TEXT',
242
+ quiet,
243
+ });
244
+ addColumnIfMissing({
245
+ database,
246
+ table: 'tasks',
247
+ column: 'every_ms',
248
+ ddl: 'every_ms INTEGER',
249
+ quiet,
250
+ });
251
+ addColumnIfMissing({
252
+ database,
253
+ table: 'tasks',
254
+ column: 'last_status',
255
+ ddl: 'last_status TEXT',
256
+ quiet,
257
+ });
258
+ addColumnIfMissing({
259
+ database,
260
+ table: 'tasks',
261
+ column: 'consecutive_errors',
262
+ ddl: 'consecutive_errors INTEGER DEFAULT 0',
263
+ quiet,
264
+ });
265
+ addColumnIfMissing({
266
+ database,
267
+ table: 'semantic_memories',
268
+ column: 'embedding',
269
+ ddl: 'embedding BLOB',
270
+ quiet,
271
+ });
272
+ addColumnIfMissing({
273
+ database,
274
+ table: 'semantic_memories',
275
+ column: 'source',
276
+ ddl: "source TEXT NOT NULL DEFAULT 'conversation'",
277
+ quiet,
278
+ });
279
+ addColumnIfMissing({
280
+ database,
281
+ table: 'semantic_memories',
282
+ column: 'scope',
283
+ ddl: "scope TEXT NOT NULL DEFAULT 'episodic'",
284
+ quiet,
285
+ });
286
+ addColumnIfMissing({
287
+ database,
288
+ table: 'semantic_memories',
289
+ column: 'metadata',
290
+ ddl: "metadata TEXT NOT NULL DEFAULT '{}'",
291
+ quiet,
292
+ });
293
+ addColumnIfMissing({
294
+ database,
295
+ table: 'semantic_memories',
296
+ column: 'deleted',
297
+ ddl: 'deleted INTEGER NOT NULL DEFAULT 0',
298
+ quiet,
299
+ });
300
+ // Semantic indexes are created after column migrations so older DBs can boot.
301
+ database.exec(`
302
+ CREATE INDEX IF NOT EXISTS idx_semantic_memories_session ON semantic_memories(session_id);
303
+ CREATE INDEX IF NOT EXISTS idx_semantic_memories_scope ON semantic_memories(scope);
304
+ CREATE INDEX IF NOT EXISTS idx_semantic_memories_confidence ON semantic_memories(confidence);
305
+ CREATE INDEX IF NOT EXISTS idx_semantic_memories_accessed ON semantic_memories(accessed_at);
306
+ CREATE INDEX IF NOT EXISTS idx_semantic_memories_deleted ON semantic_memories(deleted);
307
+ `);
308
+ if (tableExists(database, 'memory_kv')) {
309
+ database.exec(`INSERT OR IGNORE INTO kv_store (agent_id, key, value, version, updated_at)
310
+ SELECT session_id,
311
+ mem_key,
312
+ CAST(value_json AS BLOB),
313
+ 1,
314
+ COALESCE(updated_at, datetime('now'))
315
+ FROM memory_kv`);
137
316
  if (!quiet)
138
- logger.info('Migrated tasks table: added run_at column');
317
+ logger.info('Migrated legacy memory_kv rows into kv_store');
139
318
  }
140
- if (!taskCols.some((c) => c.name === 'every_ms')) {
141
- database.exec('ALTER TABLE tasks ADD COLUMN every_ms INTEGER');
142
- if (!quiet)
143
- logger.info('Migrated tasks table: added every_ms column');
319
+ recordMigration(database, 2, 'Backfill legacy columns/indexes and migrate memory_kv to kv_store');
320
+ }
321
+ function migrateV3(database) {
322
+ database.exec(`
323
+ CREATE TABLE IF NOT EXISTS entities (
324
+ id TEXT PRIMARY KEY,
325
+ entity_type TEXT NOT NULL,
326
+ name TEXT NOT NULL,
327
+ properties TEXT NOT NULL DEFAULT '{}',
328
+ created_at TEXT NOT NULL,
329
+ updated_at TEXT NOT NULL
330
+ );
331
+
332
+ CREATE TABLE IF NOT EXISTS relations (
333
+ id TEXT PRIMARY KEY,
334
+ source_entity TEXT NOT NULL,
335
+ relation_type TEXT NOT NULL,
336
+ target_entity TEXT NOT NULL,
337
+ properties TEXT NOT NULL DEFAULT '{}',
338
+ confidence REAL NOT NULL DEFAULT 1.0,
339
+ created_at TEXT NOT NULL
340
+ );
341
+ CREATE INDEX IF NOT EXISTS idx_relations_source ON relations(source_entity);
342
+ CREATE INDEX IF NOT EXISTS idx_relations_target ON relations(target_entity);
343
+ CREATE INDEX IF NOT EXISTS idx_relations_type ON relations(relation_type);
344
+ `);
345
+ recordMigration(database, 3, 'Add knowledge graph entities/relations tables and indexes');
346
+ }
347
+ function migrateV4(database) {
348
+ database.exec(`
349
+ CREATE TABLE IF NOT EXISTS canonical_sessions (
350
+ canonical_id TEXT PRIMARY KEY,
351
+ agent_id TEXT NOT NULL,
352
+ user_id TEXT NOT NULL,
353
+ messages TEXT NOT NULL DEFAULT '[]',
354
+ compaction_cursor INTEGER NOT NULL DEFAULT 0,
355
+ compacted_summary TEXT,
356
+ message_count INTEGER NOT NULL DEFAULT 0,
357
+ created_at TEXT NOT NULL,
358
+ updated_at TEXT NOT NULL,
359
+ UNIQUE(agent_id, user_id)
360
+ );
361
+ CREATE INDEX IF NOT EXISTS idx_canonical_sessions_agent_user ON canonical_sessions(agent_id, user_id);
362
+ CREATE INDEX IF NOT EXISTS idx_canonical_sessions_updated ON canonical_sessions(updated_at);
363
+
364
+ CREATE TABLE IF NOT EXISTS usage_events (
365
+ id TEXT PRIMARY KEY,
366
+ session_id TEXT NOT NULL,
367
+ agent_id TEXT NOT NULL,
368
+ timestamp TEXT NOT NULL,
369
+ model TEXT NOT NULL,
370
+ input_tokens INTEGER NOT NULL DEFAULT 0,
371
+ output_tokens INTEGER NOT NULL DEFAULT 0,
372
+ total_tokens INTEGER NOT NULL DEFAULT 0,
373
+ cost_usd REAL NOT NULL DEFAULT 0.0,
374
+ tool_calls INTEGER NOT NULL DEFAULT 0
375
+ );
376
+ CREATE INDEX IF NOT EXISTS idx_usage_events_agent_time ON usage_events(agent_id, timestamp);
377
+ CREATE INDEX IF NOT EXISTS idx_usage_events_time ON usage_events(timestamp);
378
+ CREATE INDEX IF NOT EXISTS idx_usage_events_model_time ON usage_events(model, timestamp);
379
+ CREATE INDEX IF NOT EXISTS idx_usage_events_session_time ON usage_events(session_id, timestamp);
380
+ `);
381
+ recordMigration(database, 4, 'Add canonical_sessions and usage_events tables');
382
+ }
383
+ function runMigrations(database, opts) {
384
+ const currentVersion = getSchemaVersion(database);
385
+ const quiet = opts?.quiet === true;
386
+ if (currentVersion > SCHEMA_VERSION) {
387
+ if (!quiet) {
388
+ logger.warn({ currentVersion, supportedVersion: SCHEMA_VERSION }, 'Database schema version is newer than this binary supports; skipping migrations');
389
+ }
390
+ return;
391
+ }
392
+ if (currentVersion < 1)
393
+ migrateV1(database);
394
+ if (currentVersion < 2)
395
+ migrateV2(database, opts);
396
+ if (currentVersion < 3)
397
+ migrateV3(database);
398
+ if (currentVersion < 4)
399
+ migrateV4(database);
400
+ setSchemaVersion(database, SCHEMA_VERSION);
401
+ if (!quiet && currentVersion < SCHEMA_VERSION) {
402
+ logger.info({ fromVersion: currentVersion, toVersion: SCHEMA_VERSION }, 'Database schema migrated');
144
403
  }
145
404
  }
146
405
  export function initDatabase(opts) {
147
406
  const quiet = opts?.quiet === true;
148
- const dbPath = path.resolve(DB_PATH);
407
+ const dbPath = path.resolve(opts?.dbPath || DB_PATH);
149
408
  fs.mkdirSync(path.dirname(dbPath), { recursive: true });
150
409
  db = new Database(dbPath);
151
410
  db.pragma('journal_mode = WAL');
152
- createSchema(db);
153
- migrateSchema(db, opts);
411
+ db.pragma('busy_timeout = 5000');
412
+ runMigrations(db, opts);
154
413
  if (!quiet)
155
414
  logger.info({ path: dbPath }, 'Database initialized');
156
415
  }
416
+ function normalizeMemoryKvKey(key) {
417
+ return key.trim();
418
+ }
419
+ function serializeMemoryKvValue(value) {
420
+ if (typeof value === 'undefined')
421
+ return Buffer.from('null', 'utf8');
422
+ try {
423
+ const serialized = JSON.stringify(value);
424
+ return Buffer.from(typeof serialized === 'string' ? serialized : 'null', 'utf8');
425
+ }
426
+ catch {
427
+ return Buffer.from('null', 'utf8');
428
+ }
429
+ }
430
+ function parseMemoryKvValue(raw) {
431
+ const text = Buffer.isBuffer(raw)
432
+ ? raw.toString('utf8')
433
+ : raw instanceof Uint8Array
434
+ ? Buffer.from(raw).toString('utf8')
435
+ : typeof raw === 'string'
436
+ ? raw
437
+ : null;
438
+ if (text == null)
439
+ return null;
440
+ try {
441
+ return JSON.parse(text);
442
+ }
443
+ catch {
444
+ return text;
445
+ }
446
+ }
447
+ export function getMemoryValue(sessionId, key) {
448
+ const normalizedKey = normalizeMemoryKvKey(key);
449
+ if (!normalizedKey)
450
+ return null;
451
+ const row = db
452
+ .prepare(`SELECT value
453
+ FROM kv_store
454
+ WHERE agent_id = ?
455
+ AND key = ?`)
456
+ .get(sessionId, normalizedKey);
457
+ if (!row)
458
+ return null;
459
+ return parseMemoryKvValue(row.value);
460
+ }
461
+ export function setMemoryValue(sessionId, key, value) {
462
+ const normalizedKey = normalizeMemoryKvKey(key);
463
+ if (!normalizedKey)
464
+ return;
465
+ const valueBlob = serializeMemoryKvValue(value);
466
+ const now = new Date().toISOString();
467
+ db.prepare(`INSERT INTO kv_store (agent_id, key, value, version, updated_at)
468
+ VALUES (?, ?, ?, 1, ?)
469
+ ON CONFLICT(agent_id, key)
470
+ DO UPDATE SET value = excluded.value, version = version + 1, updated_at = excluded.updated_at`).run(sessionId, normalizedKey, valueBlob, now);
471
+ }
472
+ export function deleteMemoryValue(sessionId, key) {
473
+ const normalizedKey = normalizeMemoryKvKey(key);
474
+ if (!normalizedKey)
475
+ return false;
476
+ const result = db
477
+ .prepare(`DELETE FROM kv_store
478
+ WHERE agent_id = ?
479
+ AND key = ?`)
480
+ .run(sessionId, normalizedKey);
481
+ return result.changes > 0;
482
+ }
483
+ export function listMemoryValues(sessionId, prefix) {
484
+ const normalizedPrefix = (prefix || '').trim();
485
+ const rows = normalizedPrefix
486
+ ? db
487
+ .prepare(`SELECT agent_id, key, value, version, updated_at
488
+ FROM kv_store
489
+ WHERE agent_id = ?
490
+ AND key LIKE ?
491
+ ORDER BY key ASC`)
492
+ .all(sessionId, `${normalizedPrefix}%`)
493
+ : db
494
+ .prepare(`SELECT agent_id, key, value, version, updated_at
495
+ FROM kv_store
496
+ WHERE agent_id = ?
497
+ ORDER BY key ASC`)
498
+ .all(sessionId);
499
+ return rows.map((row) => ({
500
+ agent_id: row.agent_id,
501
+ key: row.key,
502
+ value: parseMemoryKvValue(row.value),
503
+ version: row.version,
504
+ updated_at: row.updated_at,
505
+ }));
506
+ }
507
+ // --- Canonical Sessions (Cross-Channel Memory) ---
508
+ const DEFAULT_CANONICAL_WINDOW = 50;
509
+ const DEFAULT_CANONICAL_COMPACTION_THRESHOLD = 100;
510
+ const CANONICAL_SUMMARY_MAX_CHARS = 4_000;
511
+ const CANONICAL_MESSAGE_MAX_CHARS = 220;
512
+ function canonicalSessionId(agentId, userId) {
513
+ return `${agentId}:${userId}`;
514
+ }
515
+ function normalizeCanonicalRole(role) {
516
+ const normalized = role.trim().toLowerCase();
517
+ if (normalized === 'user' ||
518
+ normalized === 'assistant' ||
519
+ normalized === 'system' ||
520
+ normalized === 'tool') {
521
+ return normalized;
522
+ }
523
+ return 'user';
524
+ }
525
+ function truncateCanonicalContent(content) {
526
+ const compact = content.replace(/\s+/g, ' ').trim();
527
+ if (compact.length <= CANONICAL_MESSAGE_MAX_CHARS)
528
+ return compact;
529
+ return `${compact.slice(0, CANONICAL_MESSAGE_MAX_CHARS)}...`;
530
+ }
531
+ function parseCanonicalMessages(raw) {
532
+ const text = typeof raw === 'string' ? raw.trim() : '';
533
+ if (!text)
534
+ return [];
535
+ try {
536
+ const parsed = JSON.parse(text);
537
+ if (!Array.isArray(parsed))
538
+ return [];
539
+ const messages = [];
540
+ for (const item of parsed) {
541
+ if (!item || typeof item !== 'object')
542
+ continue;
543
+ const row = item;
544
+ const content = typeof row.content === 'string' ? row.content.trim() : '';
545
+ if (!content)
546
+ continue;
547
+ const sessionId = typeof row.session_id === 'string' ? row.session_id.trim() : '';
548
+ if (!sessionId)
549
+ continue;
550
+ const createdAt = typeof row.created_at === 'string' && row.created_at.trim()
551
+ ? row.created_at.trim()
552
+ : new Date().toISOString();
553
+ messages.push({
554
+ role: normalizeCanonicalRole(typeof row.role === 'string' ? row.role : 'user'),
555
+ content,
556
+ session_id: sessionId,
557
+ channel_id: typeof row.channel_id === 'string' && row.channel_id.trim()
558
+ ? row.channel_id.trim()
559
+ : null,
560
+ created_at: createdAt,
561
+ });
562
+ }
563
+ return messages;
564
+ }
565
+ catch {
566
+ return [];
567
+ }
568
+ }
569
+ function serializeCanonicalMessages(messages) {
570
+ try {
571
+ return JSON.stringify(messages);
572
+ }
573
+ catch {
574
+ return '[]';
575
+ }
576
+ }
577
+ function buildCanonicalSummary(params) {
578
+ const lines = [];
579
+ const previous = (params.previousSummary || '').trim();
580
+ if (previous)
581
+ lines.push(previous);
582
+ for (const message of params.compactingMessages) {
583
+ const role = message.role === 'assistant'
584
+ ? 'Assistant'
585
+ : message.role === 'system'
586
+ ? 'System'
587
+ : message.role === 'tool'
588
+ ? 'Tool'
589
+ : 'User';
590
+ const compact = truncateCanonicalContent(message.content);
591
+ if (!compact)
592
+ continue;
593
+ lines.push(`${role}: ${compact}`);
594
+ }
595
+ if (lines.length === 0)
596
+ return previous || null;
597
+ const merged = lines.join('\n');
598
+ if (merged.length <= CANONICAL_SUMMARY_MAX_CHARS)
599
+ return merged;
600
+ return merged.slice(Math.max(0, merged.length - CANONICAL_SUMMARY_MAX_CHARS));
601
+ }
602
+ function saveCanonicalSession(session) {
603
+ db.prepare(`INSERT INTO canonical_sessions
604
+ (canonical_id, agent_id, user_id, messages, compaction_cursor, compacted_summary, message_count, created_at, updated_at)
605
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
606
+ ON CONFLICT(canonical_id) DO UPDATE SET
607
+ messages = excluded.messages,
608
+ compaction_cursor = excluded.compaction_cursor,
609
+ compacted_summary = excluded.compacted_summary,
610
+ message_count = excluded.message_count,
611
+ updated_at = excluded.updated_at`).run(session.canonical_id, session.agent_id, session.user_id, serializeCanonicalMessages(session.messages), Math.max(0, Math.floor(session.compaction_cursor)), session.compacted_summary, Math.max(0, Math.floor(session.message_count)), session.created_at, session.updated_at);
612
+ }
613
+ export function loadCanonicalSession(agentId, userId) {
614
+ const normalizedAgentId = agentId.trim();
615
+ const normalizedUserId = userId.trim();
616
+ if (!normalizedAgentId) {
617
+ throw new Error('Canonical session agentId is required');
618
+ }
619
+ if (!normalizedUserId) {
620
+ throw new Error('Canonical session userId is required');
621
+ }
622
+ const row = db
623
+ .prepare(`SELECT canonical_id, agent_id, user_id, messages, compaction_cursor, compacted_summary, message_count, created_at, updated_at
624
+ FROM canonical_sessions
625
+ WHERE agent_id = ?
626
+ AND user_id = ?
627
+ LIMIT 1`)
628
+ .get(normalizedAgentId, normalizedUserId);
629
+ const now = new Date().toISOString();
630
+ if (!row) {
631
+ return {
632
+ canonical_id: canonicalSessionId(normalizedAgentId, normalizedUserId),
633
+ agent_id: normalizedAgentId,
634
+ user_id: normalizedUserId,
635
+ messages: [],
636
+ compaction_cursor: 0,
637
+ compacted_summary: null,
638
+ message_count: 0,
639
+ created_at: now,
640
+ updated_at: now,
641
+ };
642
+ }
643
+ return {
644
+ canonical_id: row.canonical_id,
645
+ agent_id: row.agent_id,
646
+ user_id: row.user_id,
647
+ messages: parseCanonicalMessages(row.messages),
648
+ compaction_cursor: Math.max(0, Math.floor(row.compaction_cursor || 0)),
649
+ compacted_summary: row.compacted_summary,
650
+ message_count: Math.max(0, Math.floor(row.message_count || 0)),
651
+ created_at: row.created_at || now,
652
+ updated_at: row.updated_at || now,
653
+ };
654
+ }
655
+ export function appendCanonicalMessages(params) {
656
+ const canonical = loadCanonicalSession(params.agentId, params.userId);
657
+ const normalizedMessages = params.newMessages
658
+ .map((entry) => {
659
+ const content = entry.content.trim();
660
+ const sessionId = entry.sessionId.trim();
661
+ if (!content || !sessionId)
662
+ return null;
663
+ return {
664
+ role: normalizeCanonicalRole(entry.role),
665
+ content,
666
+ session_id: sessionId,
667
+ channel_id: typeof entry.channelId === 'string' && entry.channelId.trim()
668
+ ? entry.channelId.trim()
669
+ : null,
670
+ created_at: typeof entry.createdAt === 'string' && entry.createdAt.trim()
671
+ ? entry.createdAt.trim()
672
+ : new Date().toISOString(),
673
+ };
674
+ })
675
+ .filter((entry) => Boolean(entry));
676
+ if (normalizedMessages.length === 0)
677
+ return canonical;
678
+ canonical.messages.push(...normalizedMessages);
679
+ canonical.message_count += normalizedMessages.length;
680
+ const windowSize = Math.max(1, Math.floor(params.windowSize || DEFAULT_CANONICAL_WINDOW));
681
+ const compactionThreshold = Math.max(windowSize + 1, Math.floor(params.compactionThreshold || DEFAULT_CANONICAL_COMPACTION_THRESHOLD));
682
+ if (canonical.messages.length > compactionThreshold) {
683
+ const toCompact = canonical.messages.length - windowSize;
684
+ if (toCompact > canonical.compaction_cursor) {
685
+ const compacting = canonical.messages.slice(canonical.compaction_cursor, toCompact);
686
+ canonical.compacted_summary = buildCanonicalSummary({
687
+ previousSummary: canonical.compacted_summary,
688
+ compactingMessages: compacting,
689
+ });
690
+ canonical.compaction_cursor = toCompact;
691
+ canonical.messages = canonical.messages.slice(toCompact);
692
+ canonical.compaction_cursor = 0;
693
+ }
694
+ }
695
+ canonical.updated_at = new Date().toISOString();
696
+ saveCanonicalSession(canonical);
697
+ return canonical;
698
+ }
699
+ export function getCanonicalContext(params) {
700
+ const canonical = loadCanonicalSession(params.agentId, params.userId);
701
+ const windowSize = Math.max(1, Math.floor(params.windowSize || DEFAULT_CANONICAL_WINDOW));
702
+ const start = Math.max(0, canonical.messages.length - windowSize);
703
+ const recent = canonical.messages.slice(start);
704
+ const excludeSessionId = typeof params.excludeSessionId === 'string'
705
+ ? params.excludeSessionId.trim()
706
+ : '';
707
+ const filtered = excludeSessionId
708
+ ? recent.filter((message) => message.session_id !== excludeSessionId)
709
+ : recent;
710
+ return {
711
+ summary: canonical.compacted_summary,
712
+ recent_messages: filtered,
713
+ };
714
+ }
715
+ // --- Usage Tracking / Aggregation ---
716
+ function normalizeUsageWindow(window) {
717
+ if (window === 'daily' || window === 'monthly' || window === 'all') {
718
+ return window;
719
+ }
720
+ return 'all';
721
+ }
722
+ function usageWindowWhereClause(window) {
723
+ if (window === 'daily') {
724
+ return "timestamp >= datetime('now', 'start of day')";
725
+ }
726
+ if (window === 'monthly') {
727
+ return "timestamp >= datetime('now', 'start of month')";
728
+ }
729
+ return null;
730
+ }
731
+ function normalizeUsageNumber(value) {
732
+ if (typeof value === 'number' && Number.isFinite(value)) {
733
+ return Math.max(0, Math.floor(value));
734
+ }
735
+ return 0;
736
+ }
737
+ function normalizeUsageCost(value) {
738
+ if (typeof value === 'number' && Number.isFinite(value)) {
739
+ return Math.max(0, value);
740
+ }
741
+ return 0;
742
+ }
743
+ function applyUsageFilters(params) {
744
+ const agentId = params.agentId?.trim();
745
+ if (agentId) {
746
+ params.whereClauses.push('agent_id = ?');
747
+ params.args.push(agentId);
748
+ }
749
+ const window = normalizeUsageWindow(params.window);
750
+ const windowClause = usageWindowWhereClause(window);
751
+ if (windowClause)
752
+ params.whereClauses.push(windowClause);
753
+ }
754
+ export function recordUsageEvent(params) {
755
+ const sessionId = params.sessionId.trim();
756
+ const agentId = params.agentId.trim();
757
+ const model = params.model.trim() || 'unknown';
758
+ if (!sessionId || !agentId)
759
+ return;
760
+ const inputTokens = normalizeUsageNumber(params.inputTokens);
761
+ const outputTokens = normalizeUsageNumber(params.outputTokens);
762
+ const totalTokens = normalizeUsageNumber(params.totalTokens ?? inputTokens + outputTokens);
763
+ const toolCalls = normalizeUsageNumber(params.toolCalls);
764
+ const costUsd = normalizeUsageCost(params.costUsd);
765
+ const timestamp = typeof params.timestamp === 'string' && params.timestamp.trim()
766
+ ? params.timestamp.trim()
767
+ : new Date().toISOString();
768
+ db.prepare(`INSERT INTO usage_events
769
+ (id, session_id, agent_id, timestamp, model, input_tokens, output_tokens, total_tokens, cost_usd, tool_calls)
770
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(randomUUID(), sessionId, agentId, timestamp, model, inputTokens, outputTokens, totalTokens, costUsd, toolCalls);
771
+ }
772
+ export function getUsageTotals(params) {
773
+ const whereClauses = [];
774
+ const args = [];
775
+ applyUsageFilters({
776
+ whereClauses,
777
+ args,
778
+ agentId: params?.agentId,
779
+ window: params?.window,
780
+ });
781
+ const where = whereClauses.length > 0 ? `WHERE ${whereClauses.join(' AND ')}` : '';
782
+ const row = db
783
+ .prepare(`SELECT
784
+ COALESCE(SUM(input_tokens), 0) AS total_input_tokens,
785
+ COALESCE(SUM(output_tokens), 0) AS total_output_tokens,
786
+ COALESCE(SUM(total_tokens), 0) AS total_tokens,
787
+ COALESCE(SUM(cost_usd), 0.0) AS total_cost_usd,
788
+ COUNT(*) AS call_count,
789
+ COALESCE(SUM(tool_calls), 0) AS total_tool_calls
790
+ FROM usage_events
791
+ ${where}`)
792
+ .get(...args);
793
+ return {
794
+ total_input_tokens: normalizeUsageNumber(row.total_input_tokens),
795
+ total_output_tokens: normalizeUsageNumber(row.total_output_tokens),
796
+ total_tokens: normalizeUsageNumber(row.total_tokens),
797
+ total_cost_usd: normalizeUsageCost(row.total_cost_usd),
798
+ call_count: normalizeUsageNumber(row.call_count),
799
+ total_tool_calls: normalizeUsageNumber(row.total_tool_calls),
800
+ };
801
+ }
802
+ export function listUsageByModel(params) {
803
+ const whereClauses = [];
804
+ const args = [];
805
+ applyUsageFilters({
806
+ whereClauses,
807
+ args,
808
+ agentId: params?.agentId,
809
+ window: params?.window,
810
+ });
811
+ const where = whereClauses.length > 0 ? `WHERE ${whereClauses.join(' AND ')}` : '';
812
+ const rows = db
813
+ .prepare(`SELECT
814
+ model,
815
+ COALESCE(SUM(input_tokens), 0) AS total_input_tokens,
816
+ COALESCE(SUM(output_tokens), 0) AS total_output_tokens,
817
+ COALESCE(SUM(total_tokens), 0) AS total_tokens,
818
+ COALESCE(SUM(cost_usd), 0.0) AS total_cost_usd,
819
+ COUNT(*) AS call_count,
820
+ COALESCE(SUM(tool_calls), 0) AS total_tool_calls
821
+ FROM usage_events
822
+ ${where}
823
+ GROUP BY model
824
+ ORDER BY total_cost_usd DESC, total_tokens DESC, call_count DESC`)
825
+ .all(...args);
826
+ return rows.map((row) => ({
827
+ model: row.model,
828
+ total_input_tokens: normalizeUsageNumber(row.total_input_tokens),
829
+ total_output_tokens: normalizeUsageNumber(row.total_output_tokens),
830
+ total_tokens: normalizeUsageNumber(row.total_tokens),
831
+ total_cost_usd: normalizeUsageCost(row.total_cost_usd),
832
+ call_count: normalizeUsageNumber(row.call_count),
833
+ total_tool_calls: normalizeUsageNumber(row.total_tool_calls),
834
+ }));
835
+ }
836
+ export function listUsageByAgent(params) {
837
+ const whereClauses = [];
838
+ const args = [];
839
+ applyUsageFilters({
840
+ whereClauses,
841
+ args,
842
+ window: params?.window,
843
+ });
844
+ const where = whereClauses.length > 0 ? `WHERE ${whereClauses.join(' AND ')}` : '';
845
+ const rows = db
846
+ .prepare(`SELECT
847
+ agent_id,
848
+ COALESCE(SUM(input_tokens), 0) AS total_input_tokens,
849
+ COALESCE(SUM(output_tokens), 0) AS total_output_tokens,
850
+ COALESCE(SUM(total_tokens), 0) AS total_tokens,
851
+ COALESCE(SUM(cost_usd), 0.0) AS total_cost_usd,
852
+ COUNT(*) AS call_count,
853
+ COALESCE(SUM(tool_calls), 0) AS total_tool_calls
854
+ FROM usage_events
855
+ ${where}
856
+ GROUP BY agent_id
857
+ ORDER BY total_cost_usd DESC, total_tokens DESC, call_count DESC`)
858
+ .all(...args);
859
+ return rows.map((row) => ({
860
+ agent_id: row.agent_id,
861
+ total_input_tokens: normalizeUsageNumber(row.total_input_tokens),
862
+ total_output_tokens: normalizeUsageNumber(row.total_output_tokens),
863
+ total_tokens: normalizeUsageNumber(row.total_tokens),
864
+ total_cost_usd: normalizeUsageCost(row.total_cost_usd),
865
+ call_count: normalizeUsageNumber(row.call_count),
866
+ total_tool_calls: normalizeUsageNumber(row.total_tool_calls),
867
+ }));
868
+ }
869
+ export function listUsageDailyBreakdown(params) {
870
+ const days = Math.max(1, Math.min(365, Math.floor(params?.days || 30)));
871
+ const whereClauses = [
872
+ `timestamp >= datetime('now', '-${days} days')`,
873
+ ];
874
+ const args = [];
875
+ const agentId = params?.agentId?.trim();
876
+ if (agentId) {
877
+ whereClauses.push('agent_id = ?');
878
+ args.push(agentId);
879
+ }
880
+ const rows = db
881
+ .prepare(`SELECT
882
+ date(timestamp) AS day,
883
+ COALESCE(SUM(input_tokens), 0) AS total_input_tokens,
884
+ COALESCE(SUM(output_tokens), 0) AS total_output_tokens,
885
+ COALESCE(SUM(total_tokens), 0) AS total_tokens,
886
+ COALESCE(SUM(cost_usd), 0.0) AS total_cost_usd,
887
+ COUNT(*) AS call_count,
888
+ COALESCE(SUM(tool_calls), 0) AS total_tool_calls
889
+ FROM usage_events
890
+ WHERE ${whereClauses.join(' AND ')}
891
+ GROUP BY day
892
+ ORDER BY day ASC`)
893
+ .all(...args);
894
+ return rows.map((row) => ({
895
+ day: row.day,
896
+ total_input_tokens: normalizeUsageNumber(row.total_input_tokens),
897
+ total_output_tokens: normalizeUsageNumber(row.total_output_tokens),
898
+ total_tokens: normalizeUsageNumber(row.total_tokens),
899
+ total_cost_usd: normalizeUsageCost(row.total_cost_usd),
900
+ call_count: normalizeUsageNumber(row.call_count),
901
+ total_tool_calls: normalizeUsageNumber(row.total_tool_calls),
902
+ }));
903
+ }
904
+ function normalizeKnowledgeCustomValue(raw) {
905
+ const value = raw.trim().toLowerCase();
906
+ return value || 'unknown';
907
+ }
908
+ function normalizeEntityType(entityType) {
909
+ if (typeof entityType === 'object' && entityType) {
910
+ if (typeof entityType.custom === 'string') {
911
+ return { custom: normalizeKnowledgeCustomValue(entityType.custom) };
912
+ }
913
+ return { custom: 'unknown' };
914
+ }
915
+ const normalized = normalizeKnowledgeCustomValue(entityType);
916
+ switch (normalized) {
917
+ case 'person':
918
+ return KnowledgeEntityType.Person;
919
+ case 'organization':
920
+ case 'org':
921
+ return KnowledgeEntityType.Organization;
922
+ case 'project':
923
+ return KnowledgeEntityType.Project;
924
+ case 'concept':
925
+ return KnowledgeEntityType.Concept;
926
+ case 'event':
927
+ return KnowledgeEntityType.Event;
928
+ case 'location':
929
+ return KnowledgeEntityType.Location;
930
+ case 'document':
931
+ case 'doc':
932
+ return KnowledgeEntityType.Document;
933
+ case 'tool':
934
+ return KnowledgeEntityType.Tool;
935
+ default:
936
+ return { custom: normalized };
937
+ }
938
+ }
939
+ function normalizeRelationType(relation) {
940
+ if (typeof relation === 'object' && relation) {
941
+ if (typeof relation.custom === 'string') {
942
+ return { custom: normalizeKnowledgeCustomValue(relation.custom) };
943
+ }
944
+ return { custom: 'unknown' };
945
+ }
946
+ const normalized = normalizeKnowledgeCustomValue(relation)
947
+ .replace(/[\s-]+/g, '_')
948
+ .replace(/_+/g, '_');
949
+ switch (normalized) {
950
+ case 'works_at':
951
+ case 'worksat':
952
+ return KnowledgeRelationType.WorksAt;
953
+ case 'knows_about':
954
+ case 'knowsabout':
955
+ case 'knows':
956
+ return KnowledgeRelationType.KnowsAbout;
957
+ case 'related_to':
958
+ case 'relatedto':
959
+ case 'related':
960
+ return KnowledgeRelationType.RelatedTo;
961
+ case 'depends_on':
962
+ case 'dependson':
963
+ case 'depends':
964
+ return KnowledgeRelationType.DependsOn;
965
+ case 'owned_by':
966
+ case 'ownedby':
967
+ return KnowledgeRelationType.OwnedBy;
968
+ case 'created_by':
969
+ case 'createdby':
970
+ return KnowledgeRelationType.CreatedBy;
971
+ case 'located_in':
972
+ case 'locatedin':
973
+ return KnowledgeRelationType.LocatedIn;
974
+ case 'part_of':
975
+ case 'partof':
976
+ return KnowledgeRelationType.PartOf;
977
+ case 'uses':
978
+ return KnowledgeRelationType.Uses;
979
+ case 'produces':
980
+ return KnowledgeRelationType.Produces;
981
+ default:
982
+ return { custom: normalized };
983
+ }
984
+ }
985
+ function serializeEntityType(entityType) {
986
+ const normalized = normalizeEntityType(entityType);
987
+ return typeof normalized === 'string'
988
+ ? JSON.stringify(normalized)
989
+ : JSON.stringify({ custom: normalized.custom });
990
+ }
991
+ function serializeRelationType(relation) {
992
+ const normalized = normalizeRelationType(relation);
993
+ return typeof normalized === 'string'
994
+ ? JSON.stringify(normalized)
995
+ : JSON.stringify({ custom: normalized.custom });
996
+ }
997
+ function parseEntityType(raw) {
998
+ const value = (raw || '').trim();
999
+ if (!value)
1000
+ return { custom: 'unknown' };
1001
+ try {
1002
+ const parsed = JSON.parse(value);
1003
+ if (typeof parsed === 'string')
1004
+ return normalizeEntityType(parsed);
1005
+ if (parsed &&
1006
+ typeof parsed === 'object' &&
1007
+ typeof parsed.custom === 'string') {
1008
+ return normalizeEntityType({
1009
+ custom: parsed.custom,
1010
+ });
1011
+ }
1012
+ }
1013
+ catch {
1014
+ return normalizeEntityType(value);
1015
+ }
1016
+ return { custom: 'unknown' };
1017
+ }
1018
+ function parseRelationType(raw) {
1019
+ const value = (raw || '').trim();
1020
+ if (!value)
1021
+ return { custom: 'unknown' };
1022
+ try {
1023
+ const parsed = JSON.parse(value);
1024
+ if (typeof parsed === 'string')
1025
+ return normalizeRelationType(parsed);
1026
+ if (parsed &&
1027
+ typeof parsed === 'object' &&
1028
+ typeof parsed.custom === 'string') {
1029
+ return normalizeRelationType({
1030
+ custom: parsed.custom,
1031
+ });
1032
+ }
1033
+ }
1034
+ catch {
1035
+ return normalizeRelationType(value);
1036
+ }
1037
+ return { custom: 'unknown' };
1038
+ }
1039
+ function serializeKnowledgeProperties(properties) {
1040
+ if (!properties ||
1041
+ typeof properties !== 'object' ||
1042
+ Array.isArray(properties)) {
1043
+ return '{}';
1044
+ }
1045
+ try {
1046
+ return JSON.stringify(properties);
1047
+ }
1048
+ catch {
1049
+ return '{}';
1050
+ }
1051
+ }
1052
+ function parseKnowledgeProperties(raw) {
1053
+ const text = Buffer.isBuffer(raw)
1054
+ ? raw.toString('utf8')
1055
+ : raw instanceof Uint8Array
1056
+ ? Buffer.from(raw).toString('utf8')
1057
+ : typeof raw === 'string'
1058
+ ? raw
1059
+ : '{}';
1060
+ try {
1061
+ const parsed = JSON.parse(text);
1062
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
1063
+ return {};
1064
+ return parsed;
1065
+ }
1066
+ catch {
1067
+ return {};
1068
+ }
1069
+ }
1070
+ function mapKnowledgeEntity(params) {
1071
+ return {
1072
+ id: params.id,
1073
+ entity_type: parseEntityType(params.entityTypeRaw),
1074
+ name: params.name,
1075
+ properties: parseKnowledgeProperties(params.propertiesRaw),
1076
+ created_at: params.createdAt,
1077
+ updated_at: params.updatedAt,
1078
+ };
1079
+ }
1080
+ function mapKnowledgeMatchRow(row) {
1081
+ return {
1082
+ source: mapKnowledgeEntity({
1083
+ id: row.s_id,
1084
+ entityTypeRaw: row.s_type,
1085
+ name: row.s_name,
1086
+ propertiesRaw: row.s_properties,
1087
+ createdAt: row.s_created_at,
1088
+ updatedAt: row.s_updated_at,
1089
+ }),
1090
+ relation: {
1091
+ source: row.r_source,
1092
+ relation: parseRelationType(row.r_type),
1093
+ target: row.r_target,
1094
+ properties: parseKnowledgeProperties(row.r_properties),
1095
+ confidence: Math.max(0, Math.min(1, Number(row.r_confidence) || 0)),
1096
+ created_at: row.r_created_at,
1097
+ },
1098
+ target: mapKnowledgeEntity({
1099
+ id: row.t_id,
1100
+ entityTypeRaw: row.t_type,
1101
+ name: row.t_name,
1102
+ propertiesRaw: row.t_properties,
1103
+ createdAt: row.t_created_at,
1104
+ updatedAt: row.t_updated_at,
1105
+ }),
1106
+ };
1107
+ }
1108
+ export function addKnowledgeEntity(params) {
1109
+ const name = params.name.trim();
1110
+ if (!name)
1111
+ throw new Error('Knowledge graph entity name is required');
1112
+ const entityId = params.id?.trim() || randomUUID();
1113
+ const now = new Date().toISOString();
1114
+ db.prepare(`INSERT INTO entities (id, entity_type, name, properties, created_at, updated_at)
1115
+ VALUES (?, ?, ?, ?, ?, ?)
1116
+ ON CONFLICT(id) DO UPDATE SET
1117
+ name = excluded.name,
1118
+ properties = excluded.properties,
1119
+ updated_at = excluded.updated_at`).run(entityId, serializeEntityType(params.entityType), name, serializeKnowledgeProperties(params.properties), now, now);
1120
+ return entityId;
1121
+ }
1122
+ export function addKnowledgeRelation(params) {
1123
+ const source = params.source.trim();
1124
+ const target = params.target.trim();
1125
+ if (!source)
1126
+ throw new Error('Knowledge graph relation source is required');
1127
+ if (!target)
1128
+ throw new Error('Knowledge graph relation target is required');
1129
+ const id = randomUUID();
1130
+ const rawConfidence = typeof params.confidence === 'number' && Number.isFinite(params.confidence)
1131
+ ? params.confidence
1132
+ : 1;
1133
+ const confidence = Math.max(0, Math.min(1, rawConfidence));
1134
+ db.prepare(`INSERT INTO relations
1135
+ (id, source_entity, relation_type, target_entity, properties, confidence, created_at)
1136
+ VALUES (?, ?, ?, ?, ?, ?, ?)`).run(id, source, serializeRelationType(params.relation), target, serializeKnowledgeProperties(params.properties), confidence, new Date().toISOString());
1137
+ return id;
1138
+ }
1139
+ export function queryKnowledgeGraph(pattern = {}) {
1140
+ const sql = [
1141
+ `SELECT
1142
+ s.id AS s_id,
1143
+ s.entity_type AS s_type,
1144
+ s.name AS s_name,
1145
+ s.properties AS s_properties,
1146
+ s.created_at AS s_created_at,
1147
+ s.updated_at AS s_updated_at,
1148
+ r.id AS r_id,
1149
+ r.source_entity AS r_source,
1150
+ r.relation_type AS r_type,
1151
+ r.target_entity AS r_target,
1152
+ r.properties AS r_properties,
1153
+ r.confidence AS r_confidence,
1154
+ r.created_at AS r_created_at,
1155
+ t.id AS t_id,
1156
+ t.entity_type AS t_type,
1157
+ t.name AS t_name,
1158
+ t.properties AS t_properties,
1159
+ t.created_at AS t_created_at,
1160
+ t.updated_at AS t_updated_at
1161
+ FROM relations r
1162
+ JOIN entities s ON r.source_entity = s.id
1163
+ JOIN entities t ON r.target_entity = t.id
1164
+ WHERE 1 = 1`,
1165
+ ];
1166
+ const args = [];
1167
+ const source = pattern.source?.trim();
1168
+ if (source) {
1169
+ sql.push('AND (s.id = ? OR s.name = ?)');
1170
+ args.push(source, source);
1171
+ }
1172
+ if (pattern.relation) {
1173
+ sql.push('AND r.relation_type = ?');
1174
+ args.push(serializeRelationType(pattern.relation));
1175
+ }
1176
+ const target = pattern.target?.trim();
1177
+ if (target) {
1178
+ sql.push('AND (t.id = ? OR t.name = ?)');
1179
+ args.push(target, target);
1180
+ }
1181
+ // OpenFang-compatible v1 query semantics: single-hop relation scan, max 100.
1182
+ sql.push('LIMIT 100');
1183
+ const rows = db
1184
+ .prepare(sql.join('\n'))
1185
+ .all(...args);
1186
+ return rows.map(mapKnowledgeMatchRow);
1187
+ }
157
1188
  // --- Sessions ---
158
1189
  export function getOrCreateSession(sessionId, guildId, channelId) {
159
1190
  const existing = getSessionById(sessionId);
160
1191
  if (existing) {
161
- db.prepare('UPDATE sessions SET last_active = datetime(\'now\') WHERE id = ?').run(sessionId);
1192
+ db.prepare("UPDATE sessions SET last_active = datetime('now') WHERE id = ?").run(sessionId);
162
1193
  return existing;
163
1194
  }
164
1195
  db.prepare('INSERT INTO sessions (id, guild_id, channel_id) VALUES (?, ?, ?)').run(sessionId, guildId, channelId);
165
1196
  return getSessionById(sessionId);
166
1197
  }
167
1198
  export function getSessionById(sessionId) {
168
- return db
169
- .prepare('SELECT * FROM sessions WHERE id = ?')
170
- .get(sessionId);
1199
+ return db.prepare('SELECT * FROM sessions WHERE id = ?').get(sessionId);
171
1200
  }
172
1201
  export function updateSessionChatbot(sessionId, chatbotId) {
173
1202
  db.prepare('UPDATE sessions SET chatbot_id = ? WHERE id = ?').run(chatbotId, sessionId);
@@ -179,27 +1208,488 @@ export function updateSessionRag(sessionId, enableRag) {
179
1208
  db.prepare('UPDATE sessions SET enable_rag = ? WHERE id = ?').run(enableRag ? 1 : 0, sessionId);
180
1209
  }
181
1210
  export function getAllSessions() {
182
- return db.prepare('SELECT * FROM sessions ORDER BY last_active DESC').all();
1211
+ return db
1212
+ .prepare('SELECT * FROM sessions ORDER BY last_active DESC')
1213
+ .all();
183
1214
  }
184
1215
  export function getSessionCount() {
185
1216
  const row = db.prepare('SELECT COUNT(*) as count FROM sessions').get();
186
1217
  return row.count;
187
1218
  }
1219
+ export function getMostRecentSessionChannelId() {
1220
+ const row = db
1221
+ .prepare('SELECT channel_id FROM sessions ORDER BY last_active DESC LIMIT 1')
1222
+ .get();
1223
+ if (!row || typeof row.channel_id !== 'string')
1224
+ return null;
1225
+ const channelId = row.channel_id.trim();
1226
+ return channelId || null;
1227
+ }
188
1228
  export function clearSessionHistory(sessionId) {
189
- const result = db.prepare('DELETE FROM messages WHERE session_id = ?').run(sessionId);
1229
+ const result = db
1230
+ .prepare('DELETE FROM messages WHERE session_id = ?')
1231
+ .run(sessionId);
1232
+ db.prepare('DELETE FROM semantic_memories WHERE session_id = ?').run(sessionId);
190
1233
  db.prepare('UPDATE sessions SET message_count = 0, session_summary = NULL, summary_updated_at = NULL, compaction_count = 0, memory_flush_at = NULL WHERE id = ?').run(sessionId);
191
1234
  return result.changes;
192
1235
  }
193
1236
  // --- Messages ---
194
1237
  export function storeMessage(sessionId, userId, username, role, content) {
195
- db.prepare('INSERT INTO messages (session_id, user_id, username, role, content) VALUES (?, ?, ?, ?, ?)').run(sessionId, userId, username, role, content);
196
- db.prepare('UPDATE sessions SET message_count = message_count + 1, last_active = datetime(\'now\') WHERE id = ?').run(sessionId);
1238
+ const result = db
1239
+ .prepare('INSERT INTO messages (session_id, user_id, username, role, content) VALUES (?, ?, ?, ?, ?)')
1240
+ .run(sessionId, userId, username, role, content);
1241
+ db.prepare("UPDATE sessions SET message_count = message_count + 1, last_active = datetime('now') WHERE id = ?").run(sessionId);
1242
+ return result.lastInsertRowid;
197
1243
  }
198
1244
  export function getConversationHistory(sessionId, limit = 50) {
199
1245
  return db
200
1246
  .prepare('SELECT * FROM messages WHERE session_id = ? ORDER BY id DESC LIMIT ?')
201
1247
  .all(sessionId, limit);
202
1248
  }
1249
+ export function getRecentMessages(sessionId, limit) {
1250
+ const boundedLimit = typeof limit === 'number' && Number.isFinite(limit)
1251
+ ? Math.max(1, Math.floor(limit))
1252
+ : null;
1253
+ if (boundedLimit == null) {
1254
+ return db
1255
+ .prepare('SELECT * FROM messages WHERE session_id = ? ORDER BY id ASC')
1256
+ .all(sessionId);
1257
+ }
1258
+ const rows = db
1259
+ .prepare('SELECT * FROM messages WHERE session_id = ? ORDER BY id DESC LIMIT ?')
1260
+ .all(sessionId, boundedLimit);
1261
+ return rows.reverse();
1262
+ }
1263
+ function parseTimestamp(raw) {
1264
+ const value = raw.trim();
1265
+ if (!value)
1266
+ return 0;
1267
+ if (/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/.test(value)) {
1268
+ const parsed = Date.parse(`${value.replace(' ', 'T')}Z`);
1269
+ return Number.isNaN(parsed) ? 0 : parsed;
1270
+ }
1271
+ const parsed = Date.parse(value);
1272
+ return Number.isNaN(parsed) ? 0 : parsed;
1273
+ }
1274
+ function parseQueryTerms(query) {
1275
+ const lower = query
1276
+ .toLowerCase()
1277
+ .split(/[^a-z0-9_-]+/g)
1278
+ .map((term) => term.trim())
1279
+ .filter((term) => term.length >= 2);
1280
+ if (lower.length === 0)
1281
+ return [];
1282
+ const unique = new Set();
1283
+ for (const term of lower) {
1284
+ unique.add(term);
1285
+ if (unique.size >= 8)
1286
+ break;
1287
+ }
1288
+ return [...unique];
1289
+ }
1290
+ const MAX_EMBEDDING_DIMENSIONS = 2048;
1291
+ function normalizeEmbeddingInput(embedding) {
1292
+ if (!Array.isArray(embedding) || embedding.length === 0)
1293
+ return null;
1294
+ if (embedding.length > MAX_EMBEDDING_DIMENSIONS)
1295
+ return null;
1296
+ const values = [];
1297
+ for (const value of embedding) {
1298
+ if (typeof value !== 'number' || !Number.isFinite(value))
1299
+ return null;
1300
+ values.push(value);
1301
+ }
1302
+ if (values.length === 0)
1303
+ return null;
1304
+ return new Float32Array(values);
1305
+ }
1306
+ function embeddingToBlob(embedding) {
1307
+ const buffer = Buffer.allocUnsafe(embedding.length * 4);
1308
+ for (let i = 0; i < embedding.length; i += 1) {
1309
+ buffer.writeFloatLE(embedding[i], i * 4);
1310
+ }
1311
+ return buffer;
1312
+ }
1313
+ function embeddingFromBlob(raw) {
1314
+ if (!raw)
1315
+ return null;
1316
+ const bytes = Buffer.isBuffer(raw)
1317
+ ? raw
1318
+ : raw instanceof Uint8Array
1319
+ ? Buffer.from(raw)
1320
+ : null;
1321
+ if (!bytes || bytes.length === 0 || bytes.length % 4 !== 0)
1322
+ return null;
1323
+ const values = [];
1324
+ for (let i = 0; i < bytes.length; i += 4) {
1325
+ values.push(bytes.readFloatLE(i));
1326
+ }
1327
+ return values.length > 0 ? values : null;
1328
+ }
1329
+ function cosineSimilarity(a, b) {
1330
+ if (a.length === 0 || b.length === 0 || a.length !== b.length)
1331
+ return -1;
1332
+ let dot = 0;
1333
+ let normA = 0;
1334
+ let normB = 0;
1335
+ for (let i = 0; i < a.length; i += 1) {
1336
+ const bv = b[i];
1337
+ if (!Number.isFinite(bv))
1338
+ return -1;
1339
+ dot += a[i] * bv;
1340
+ normA += a[i] * a[i];
1341
+ normB += bv * bv;
1342
+ }
1343
+ if (normA <= Number.EPSILON || normB <= Number.EPSILON)
1344
+ return -1;
1345
+ return dot / Math.sqrt(normA * normB);
1346
+ }
1347
+ function scoreSemanticLikeCandidate(row, normalizedQuery, queryTerms) {
1348
+ const content = row.content.toLowerCase();
1349
+ let score = 0;
1350
+ if (content.includes(normalizedQuery))
1351
+ score += 8;
1352
+ if (content.startsWith(normalizedQuery))
1353
+ score += 3;
1354
+ let termHits = 0;
1355
+ for (const term of queryTerms) {
1356
+ if (content.includes(term))
1357
+ termHits += 1;
1358
+ }
1359
+ score += termHits * 2;
1360
+ score += Math.max(0, Math.min(1, row.confidence)) * 4;
1361
+ const hoursSinceAccess = Math.max(0, (Date.now() - parseTimestamp(row.accessed_at)) / 3_600_000);
1362
+ if (hoursSinceAccess < 24)
1363
+ score += 1;
1364
+ return score;
1365
+ }
1366
+ function parseSemanticMetadata(raw) {
1367
+ if (!raw)
1368
+ return {};
1369
+ try {
1370
+ const parsed = JSON.parse(raw);
1371
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
1372
+ return {};
1373
+ return parsed;
1374
+ }
1375
+ catch {
1376
+ return {};
1377
+ }
1378
+ }
1379
+ function serializeSemanticMetadata(metadata) {
1380
+ try {
1381
+ return JSON.stringify(metadata);
1382
+ }
1383
+ catch {
1384
+ return '{}';
1385
+ }
1386
+ }
1387
+ function mapSemanticMemoryRow(row) {
1388
+ return {
1389
+ id: row.id,
1390
+ session_id: row.session_id,
1391
+ role: row.role,
1392
+ source: (row.source || '').trim() || 'conversation',
1393
+ scope: (row.scope || '').trim() || 'episodic',
1394
+ metadata: parseSemanticMetadata(row.metadata),
1395
+ content: row.content,
1396
+ confidence: row.confidence,
1397
+ embedding: embeddingFromBlob(row.embedding),
1398
+ source_message_id: row.source_message_id,
1399
+ created_at: row.created_at,
1400
+ accessed_at: row.accessed_at,
1401
+ access_count: row.access_count,
1402
+ };
1403
+ }
1404
+ function touchSemanticMemoryRows(entries) {
1405
+ if (entries.length === 0)
1406
+ return;
1407
+ const touch = db.prepare(`UPDATE semantic_memories
1408
+ SET access_count = access_count + 1,
1409
+ accessed_at = datetime('now')
1410
+ WHERE id = ?
1411
+ AND deleted = 0`);
1412
+ const transaction = db.transaction((rows) => {
1413
+ for (const row of rows) {
1414
+ touch.run(row.id);
1415
+ }
1416
+ });
1417
+ transaction(entries);
1418
+ }
1419
+ export function touchSemanticMemories(ids) {
1420
+ const uniqueIds = [
1421
+ ...new Set(ids.map((id) => Math.floor(id)).filter((id) => id > 0)),
1422
+ ];
1423
+ if (uniqueIds.length === 0)
1424
+ return;
1425
+ const touch = db.prepare(`UPDATE semantic_memories
1426
+ SET access_count = access_count + 1,
1427
+ accessed_at = datetime('now')
1428
+ WHERE id = ?
1429
+ AND deleted = 0`);
1430
+ const transaction = db.transaction((rowIds) => {
1431
+ for (const id of rowIds) {
1432
+ touch.run(id);
1433
+ }
1434
+ });
1435
+ transaction(uniqueIds);
1436
+ }
1437
+ function applySemanticRecallFilterClauses(params) {
1438
+ if (!params.filter)
1439
+ return;
1440
+ const role = params.filter.role?.trim();
1441
+ if (role) {
1442
+ params.whereClauses.push('role = ?');
1443
+ params.args.push(role);
1444
+ }
1445
+ const source = params.filter.source?.trim();
1446
+ if (source) {
1447
+ params.whereClauses.push('source = ?');
1448
+ params.args.push(source);
1449
+ }
1450
+ const scope = params.filter.scope?.trim();
1451
+ if (scope) {
1452
+ params.whereClauses.push('scope = ?');
1453
+ params.args.push(scope);
1454
+ }
1455
+ const after = params.filter.after?.trim();
1456
+ if (after) {
1457
+ params.whereClauses.push('created_at >= ?');
1458
+ params.args.push(after);
1459
+ }
1460
+ const before = params.filter.before?.trim();
1461
+ if (before) {
1462
+ params.whereClauses.push('created_at <= ?');
1463
+ params.args.push(before);
1464
+ }
1465
+ }
1466
+ function recallSemanticMemoriesByLike(params) {
1467
+ if (params.queryTerms.length === 0)
1468
+ return [];
1469
+ const candidateLimit = Math.max(params.limit * 8, 50);
1470
+ const likePatterns = params.queryTerms.map((term) => `%${term}%`);
1471
+ const placeholders = likePatterns
1472
+ .map(() => 'LOWER(content) LIKE ?')
1473
+ .join(' OR ');
1474
+ const whereClauses = [
1475
+ 'session_id = ?',
1476
+ 'deleted = 0',
1477
+ 'confidence >= ?',
1478
+ `(${placeholders})`,
1479
+ ];
1480
+ const args = [
1481
+ params.sessionId,
1482
+ params.minConfidence,
1483
+ ...likePatterns,
1484
+ ];
1485
+ applySemanticRecallFilterClauses({
1486
+ whereClauses,
1487
+ args,
1488
+ filter: params.filter,
1489
+ });
1490
+ args.push(candidateLimit);
1491
+ const rawRows = db
1492
+ .prepare(`SELECT *
1493
+ FROM semantic_memories
1494
+ WHERE ${whereClauses.join('\n AND ')}
1495
+ ORDER BY confidence DESC, accessed_at DESC
1496
+ LIMIT ?`)
1497
+ .all(...args);
1498
+ if (rawRows.length === 0)
1499
+ return [];
1500
+ const ranked = rawRows
1501
+ .map(mapSemanticMemoryRow)
1502
+ .map((row) => ({
1503
+ row,
1504
+ score: scoreSemanticLikeCandidate(row, params.normalizedQuery, params.queryTerms),
1505
+ }))
1506
+ .filter((entry) => entry.score > 0)
1507
+ .sort((a, b) => {
1508
+ if (b.score !== a.score)
1509
+ return b.score - a.score;
1510
+ if (b.row.confidence !== a.row.confidence) {
1511
+ return b.row.confidence - a.row.confidence;
1512
+ }
1513
+ return (parseTimestamp(b.row.accessed_at) - parseTimestamp(a.row.accessed_at));
1514
+ })
1515
+ .slice(0, params.limit)
1516
+ .map((entry) => entry.row);
1517
+ touchSemanticMemoryRows(ranked);
1518
+ return ranked;
1519
+ }
1520
+ function recallSemanticMemoriesByVector(params) {
1521
+ const candidateLimit = Math.max(params.limit * 10, 100);
1522
+ const whereClauses = [
1523
+ 'session_id = ?',
1524
+ 'deleted = 0',
1525
+ 'confidence >= ?',
1526
+ ];
1527
+ const args = [params.sessionId, params.minConfidence];
1528
+ applySemanticRecallFilterClauses({
1529
+ whereClauses,
1530
+ args,
1531
+ filter: params.filter,
1532
+ });
1533
+ args.push(candidateLimit);
1534
+ const rawRows = db
1535
+ .prepare(`SELECT *
1536
+ FROM semantic_memories
1537
+ WHERE ${whereClauses.join('\n AND ')}
1538
+ ORDER BY accessed_at DESC, confidence DESC
1539
+ LIMIT ?`)
1540
+ .all(...args);
1541
+ if (rawRows.length === 0)
1542
+ return [];
1543
+ const rows = rawRows.map(mapSemanticMemoryRow);
1544
+ const ranked = rows
1545
+ .map((row) => {
1546
+ const similarity = row.embedding
1547
+ ? cosineSimilarity(params.queryEmbedding, row.embedding)
1548
+ : -1;
1549
+ return {
1550
+ row,
1551
+ similarity,
1552
+ };
1553
+ })
1554
+ .sort((a, b) => {
1555
+ if (b.similarity !== a.similarity)
1556
+ return b.similarity - a.similarity;
1557
+ if (b.row.confidence !== a.row.confidence) {
1558
+ return b.row.confidence - a.row.confidence;
1559
+ }
1560
+ return (parseTimestamp(b.row.accessed_at) - parseTimestamp(a.row.accessed_at));
1561
+ })
1562
+ .slice(0, params.limit)
1563
+ .map((entry) => entry.row);
1564
+ touchSemanticMemoryRows(ranked);
1565
+ return ranked;
1566
+ }
1567
+ function recallSemanticMemoriesByRecent(params) {
1568
+ const whereClauses = [
1569
+ 'session_id = ?',
1570
+ 'deleted = 0',
1571
+ 'confidence >= ?',
1572
+ ];
1573
+ const args = [params.sessionId, params.minConfidence];
1574
+ applySemanticRecallFilterClauses({
1575
+ whereClauses,
1576
+ args,
1577
+ filter: params.filter,
1578
+ });
1579
+ args.push(params.limit);
1580
+ const rows = db
1581
+ .prepare(`SELECT *
1582
+ FROM semantic_memories
1583
+ WHERE ${whereClauses.join('\n AND ')}
1584
+ ORDER BY accessed_at DESC, confidence DESC
1585
+ LIMIT ?`)
1586
+ .all(...args);
1587
+ const mapped = rows.map(mapSemanticMemoryRow);
1588
+ touchSemanticMemoryRows(mapped);
1589
+ return mapped;
1590
+ }
1591
+ export function storeSemanticMemory(params) {
1592
+ const normalizedContent = params.content.trim();
1593
+ const source = (params.source || '').trim() || 'conversation';
1594
+ const scope = (params.scope || '').trim() || 'episodic';
1595
+ const metadata = typeof params.metadata === 'string'
1596
+ ? parseSemanticMetadata(params.metadata)
1597
+ : params.metadata && typeof params.metadata === 'object'
1598
+ ? params.metadata
1599
+ : {};
1600
+ const metadataJson = serializeSemanticMetadata(metadata);
1601
+ const deleted = params.deleted === true || params.deleted === 1 ? 1 : 0;
1602
+ const rawConfidence = typeof params.confidence === 'number' && Number.isFinite(params.confidence)
1603
+ ? params.confidence
1604
+ : 1;
1605
+ const boundedConfidence = Math.max(0, Math.min(1, rawConfidence));
1606
+ const normalizedEmbedding = normalizeEmbeddingInput(params.embedding);
1607
+ const embeddingBlob = normalizedEmbedding
1608
+ ? embeddingToBlob(normalizedEmbedding)
1609
+ : null;
1610
+ const createdAt = params.createdAt?.trim() || null;
1611
+ const accessedAt = params.accessedAt?.trim() || createdAt || null;
1612
+ const result = db
1613
+ .prepare(`INSERT INTO semantic_memories
1614
+ (session_id, role, source, scope, metadata, content, confidence, embedding, source_message_id, created_at, accessed_at, access_count, deleted)
1615
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, COALESCE(?, datetime('now')), COALESCE(?, datetime('now')), 0, ?)`)
1616
+ .run(params.sessionId, params.role, source, scope, metadataJson, normalizedContent, boundedConfidence, embeddingBlob, params.sourceMessageId ?? null, createdAt, accessedAt, deleted);
1617
+ return result.lastInsertRowid;
1618
+ }
1619
+ export function recallSemanticMemories(params) {
1620
+ const normalizedQuery = params.query.trim().toLowerCase();
1621
+ const queryTerms = parseQueryTerms(normalizedQuery);
1622
+ const queryEmbedding = normalizeEmbeddingInput(params.queryEmbedding);
1623
+ const limit = Math.max(1, Math.min(Math.floor(params.limit || 5), 50));
1624
+ const rawMinConfidence = typeof params.minConfidence === 'number' &&
1625
+ Number.isFinite(params.minConfidence)
1626
+ ? params.minConfidence
1627
+ : 0.2;
1628
+ const minConfidence = Math.max(0, Math.min(1, rawMinConfidence));
1629
+ if (!queryEmbedding && queryTerms.length === 0) {
1630
+ return recallSemanticMemoriesByRecent({
1631
+ sessionId: params.sessionId,
1632
+ limit,
1633
+ minConfidence,
1634
+ filter: params.filter,
1635
+ });
1636
+ }
1637
+ if (queryEmbedding) {
1638
+ return recallSemanticMemoriesByVector({
1639
+ sessionId: params.sessionId,
1640
+ queryEmbedding,
1641
+ limit,
1642
+ minConfidence,
1643
+ filter: params.filter,
1644
+ });
1645
+ }
1646
+ return recallSemanticMemoriesByLike({
1647
+ sessionId: params.sessionId,
1648
+ normalizedQuery,
1649
+ queryTerms,
1650
+ limit,
1651
+ minConfidence,
1652
+ filter: params.filter,
1653
+ });
1654
+ }
1655
+ export function forgetSemanticMemory(id) {
1656
+ const normalizedId = Math.floor(id);
1657
+ if (!Number.isFinite(normalizedId) || normalizedId <= 0)
1658
+ return false;
1659
+ const result = db
1660
+ .prepare(`UPDATE semantic_memories
1661
+ SET deleted = 1
1662
+ WHERE id = ?
1663
+ AND deleted = 0`)
1664
+ .run(normalizedId);
1665
+ return result.changes > 0;
1666
+ }
1667
+ export function decaySemanticMemories(params) {
1668
+ const rawDecayRate = typeof params?.decayRate === 'number' && Number.isFinite(params.decayRate)
1669
+ ? params.decayRate
1670
+ : 0.1;
1671
+ const decayRate = Math.max(0, Math.min(0.95, rawDecayRate));
1672
+ const decayFactor = 1 - decayRate;
1673
+ const rawStaleAfterDays = typeof params?.staleAfterDays === 'number' &&
1674
+ Number.isFinite(params.staleAfterDays)
1675
+ ? params.staleAfterDays
1676
+ : 7;
1677
+ const staleAfterDays = Math.max(1, Math.min(365, Math.floor(rawStaleAfterDays)));
1678
+ const rawMinConfidence = typeof params?.minConfidence === 'number' &&
1679
+ Number.isFinite(params.minConfidence)
1680
+ ? params.minConfidence
1681
+ : 0.1;
1682
+ const minConfidence = Math.max(0, Math.min(0.95, rawMinConfidence));
1683
+ const cutoff = `-${staleAfterDays} days`;
1684
+ const result = db
1685
+ .prepare(`UPDATE semantic_memories
1686
+ SET confidence = MAX(?, confidence * ?)
1687
+ WHERE deleted = 0
1688
+ AND confidence > ?
1689
+ AND accessed_at < datetime('now', ?)`)
1690
+ .run(minConfidence, decayFactor, minConfidence, cutoff);
1691
+ return result.changes;
1692
+ }
203
1693
  export function getCompactionCandidateMessages(sessionId, keepRecent) {
204
1694
  const keep = Math.max(1, Math.floor(keepRecent));
205
1695
  const cutoffRow = db
@@ -221,19 +1711,21 @@ export function deleteMessagesBeforeId(sessionId, cutoffId) {
221
1711
  const result = db
222
1712
  .prepare('DELETE FROM messages WHERE session_id = ? AND id < ?')
223
1713
  .run(sessionId, cutoffId);
224
- db.prepare('UPDATE sessions SET message_count = (SELECT COUNT(*) FROM messages WHERE session_id = ?), last_active = datetime(\'now\') WHERE id = ?').run(sessionId, sessionId);
1714
+ db.prepare("UPDATE sessions SET message_count = (SELECT COUNT(*) FROM messages WHERE session_id = ?), last_active = datetime('now') WHERE id = ?").run(sessionId, sessionId);
225
1715
  return result.changes;
226
1716
  }
227
1717
  export function updateSessionSummary(sessionId, summary) {
228
1718
  const normalized = summary.trim();
229
- db.prepare('UPDATE sessions SET session_summary = ?, summary_updated_at = datetime(\'now\'), compaction_count = compaction_count + 1 WHERE id = ?').run(normalized || null, sessionId);
1719
+ db.prepare("UPDATE sessions SET session_summary = ?, summary_updated_at = datetime('now'), compaction_count = compaction_count + 1 WHERE id = ?").run(normalized || null, sessionId);
230
1720
  }
231
1721
  export function markSessionMemoryFlush(sessionId) {
232
- db.prepare('UPDATE sessions SET memory_flush_at = datetime(\'now\') WHERE id = ?').run(sessionId);
1722
+ db.prepare("UPDATE sessions SET memory_flush_at = datetime('now') WHERE id = ?").run(sessionId);
233
1723
  }
234
1724
  // --- Tasks ---
235
1725
  export function createTask(sessionId, channelId, cronExpr, prompt, runAt, everyMs) {
236
- const result = db.prepare('INSERT INTO tasks (session_id, channel_id, cron_expr, prompt, run_at, every_ms) VALUES (?, ?, ?, ?, ?, ?)').run(sessionId, channelId, cronExpr, prompt, runAt || null, everyMs || null);
1726
+ const result = db
1727
+ .prepare('INSERT INTO tasks (session_id, channel_id, cron_expr, prompt, run_at, every_ms) VALUES (?, ?, ?, ?, ?, ?)')
1728
+ .run(sessionId, channelId, cronExpr, prompt, runAt || null, everyMs || null);
237
1729
  return result.lastInsertRowid;
238
1730
  }
239
1731
  export function getTasksForSession(sessionId) {
@@ -242,14 +1734,40 @@ export function getTasksForSession(sessionId) {
242
1734
  .all(sessionId);
243
1735
  }
244
1736
  export function getAllEnabledTasks() {
245
- return db.prepare('SELECT * FROM tasks WHERE enabled = 1').all();
1737
+ return db
1738
+ .prepare('SELECT * FROM tasks WHERE enabled = 1')
1739
+ .all();
246
1740
  }
247
1741
  export function updateTaskLastRun(taskId) {
248
- db.prepare('UPDATE tasks SET last_run = datetime(\'now\') WHERE id = ?').run(taskId);
1742
+ db.prepare("UPDATE tasks SET last_run = datetime('now') WHERE id = ?").run(taskId);
1743
+ }
1744
+ export function markTaskSuccess(taskId) {
1745
+ db.prepare('UPDATE tasks SET last_status = ?, consecutive_errors = 0 WHERE id = ?').run('success', taskId);
1746
+ }
1747
+ export function markTaskFailure(taskId, maxConsecutiveErrors = 5) {
1748
+ const row = db
1749
+ .prepare('SELECT consecutive_errors FROM tasks WHERE id = ?')
1750
+ .get(taskId);
1751
+ if (!row) {
1752
+ return { disabled: false, consecutiveErrors: 0 };
1753
+ }
1754
+ const nextCount = Math.max(0, Math.floor(row.consecutive_errors || 0)) + 1;
1755
+ const shouldDisable = nextCount >= Math.max(1, Math.floor(maxConsecutiveErrors));
1756
+ db.prepare('UPDATE tasks SET last_status = ?, consecutive_errors = ?, enabled = ? WHERE id = ?').run('error', nextCount, shouldDisable ? 0 : 1, taskId);
1757
+ return {
1758
+ disabled: shouldDisable,
1759
+ consecutiveErrors: nextCount,
1760
+ };
249
1761
  }
250
1762
  export function toggleTask(taskId, enabled) {
251
1763
  db.prepare('UPDATE tasks SET enabled = ? WHERE id = ?').run(enabled ? 1 : 0, taskId);
252
1764
  }
1765
+ export function pauseTask(taskId) {
1766
+ toggleTask(taskId, false);
1767
+ }
1768
+ export function resumeTask(taskId) {
1769
+ toggleTask(taskId, true);
1770
+ }
253
1771
  export function deleteTask(taskId) {
254
1772
  db.prepare('DELETE FROM tasks WHERE id = ?').run(taskId);
255
1773
  }
@@ -397,7 +1915,7 @@ export function deleteObservabilityIngestToken(tokenKey) {
397
1915
  }
398
1916
  export function enqueueProactiveMessage(channelId, text, source, maxQueueSize) {
399
1917
  const boundedMax = Math.max(1, Math.floor(maxQueueSize));
400
- db.prepare('INSERT INTO proactive_message_queue (channel_id, text, source, queued_at) VALUES (?, ?, ?, datetime(\'now\'))').run(channelId, text, source);
1918
+ db.prepare("INSERT INTO proactive_message_queue (channel_id, text, source, queued_at) VALUES (?, ?, ?, datetime('now'))").run(channelId, text, source);
401
1919
  const countRow = db
402
1920
  .prepare('SELECT COUNT(*) as count FROM proactive_message_queue')
403
1921
  .get();