@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.
- package/.github/workflows/ci.yml +70 -0
- package/.husky/pre-commit +1 -0
- package/CHANGELOG.md +85 -0
- package/CONTRIBUTING.md +33 -0
- package/README.md +41 -16
- package/SECURITY.md +17 -0
- package/biome.json +35 -0
- package/config.example.json +71 -8
- package/container/package-lock.json +2 -2
- package/container/package.json +1 -1
- package/container/src/approval-policy.ts +1303 -0
- package/container/src/browser-tools.ts +431 -136
- package/container/src/extensions.ts +36 -12
- package/container/src/hybridai-client.ts +34 -13
- package/container/src/index.ts +451 -109
- package/container/src/ipc.ts +5 -3
- package/container/src/token-usage.ts +20 -10
- package/container/src/tools.ts +599 -225
- package/container/src/types.ts +32 -2
- package/container/src/web-fetch.ts +89 -32
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +10 -2
- package/dist/agent.js.map +1 -1
- package/dist/audit-cli.d.ts.map +1 -1
- package/dist/audit-cli.js +4 -2
- package/dist/audit-cli.js.map +1 -1
- package/dist/audit-events.d.ts.map +1 -1
- package/dist/audit-events.js +53 -3
- package/dist/audit-events.js.map +1 -1
- package/dist/audit-trail.d.ts.map +1 -1
- package/dist/audit-trail.js +17 -8
- package/dist/audit-trail.js.map +1 -1
- package/dist/channels/discord/attachments.d.ts.map +1 -1
- package/dist/channels/discord/attachments.js +14 -7
- package/dist/channels/discord/attachments.js.map +1 -1
- package/dist/channels/discord/debounce.d.ts +9 -0
- package/dist/channels/discord/debounce.d.ts.map +1 -0
- package/dist/channels/discord/debounce.js +20 -0
- package/dist/channels/discord/debounce.js.map +1 -0
- package/dist/channels/discord/delivery.d.ts +4 -1
- package/dist/channels/discord/delivery.d.ts.map +1 -1
- package/dist/channels/discord/delivery.js +19 -3
- package/dist/channels/discord/delivery.js.map +1 -1
- package/dist/channels/discord/human-delay.d.ts +16 -0
- package/dist/channels/discord/human-delay.d.ts.map +1 -0
- package/dist/channels/discord/human-delay.js +29 -0
- package/dist/channels/discord/human-delay.js.map +1 -0
- package/dist/channels/discord/inbound.d.ts +4 -0
- package/dist/channels/discord/inbound.d.ts.map +1 -1
- package/dist/channels/discord/inbound.js +45 -4
- package/dist/channels/discord/inbound.js.map +1 -1
- package/dist/channels/discord/mentions.d.ts.map +1 -1
- package/dist/channels/discord/mentions.js +16 -4
- package/dist/channels/discord/mentions.js.map +1 -1
- package/dist/channels/discord/presence.d.ts +33 -0
- package/dist/channels/discord/presence.d.ts.map +1 -0
- package/dist/channels/discord/presence.js +111 -0
- package/dist/channels/discord/presence.js.map +1 -0
- package/dist/channels/discord/rate-limiter.d.ts +14 -0
- package/dist/channels/discord/rate-limiter.d.ts.map +1 -0
- package/dist/channels/discord/rate-limiter.js +49 -0
- package/dist/channels/discord/rate-limiter.js.map +1 -0
- package/dist/channels/discord/reactions.d.ts +38 -0
- package/dist/channels/discord/reactions.d.ts.map +1 -0
- package/dist/channels/discord/reactions.js +151 -0
- package/dist/channels/discord/reactions.js.map +1 -0
- package/dist/channels/discord/runtime.d.ts +6 -3
- package/dist/channels/discord/runtime.d.ts.map +1 -1
- package/dist/channels/discord/runtime.js +621 -125
- package/dist/channels/discord/runtime.js.map +1 -1
- package/dist/channels/discord/stream.d.ts +4 -1
- package/dist/channels/discord/stream.d.ts.map +1 -1
- package/dist/channels/discord/stream.js +16 -8
- package/dist/channels/discord/stream.js.map +1 -1
- package/dist/channels/discord/tool-actions.d.ts.map +1 -1
- package/dist/channels/discord/tool-actions.js +24 -12
- package/dist/channels/discord/tool-actions.js.map +1 -1
- package/dist/channels/discord/typing.d.ts +15 -0
- package/dist/channels/discord/typing.d.ts.map +1 -0
- package/dist/channels/discord/typing.js +106 -0
- package/dist/channels/discord/typing.js.map +1 -0
- package/dist/chunk.d.ts.map +1 -1
- package/dist/chunk.js +4 -2
- package/dist/chunk.js.map +1 -1
- package/dist/cli.js +47 -22
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +19 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +103 -18
- package/dist/config.js.map +1 -1
- package/dist/container-runner.d.ts.map +1 -1
- package/dist/container-runner.js +58 -26
- package/dist/container-runner.js.map +1 -1
- package/dist/container-setup.d.ts.map +1 -1
- package/dist/container-setup.js +10 -9
- package/dist/container-setup.js.map +1 -1
- package/dist/conversation.d.ts +2 -2
- package/dist/conversation.d.ts.map +1 -1
- package/dist/conversation.js +1 -1
- package/dist/conversation.js.map +1 -1
- package/dist/db.d.ts +118 -2
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +1568 -50
- package/dist/db.js.map +1 -1
- package/dist/delegation-manager.d.ts.map +1 -1
- package/dist/delegation-manager.js +3 -2
- package/dist/delegation-manager.js.map +1 -1
- package/dist/gateway-client.d.ts +2 -2
- package/dist/gateway-client.d.ts.map +1 -1
- package/dist/gateway-client.js +10 -4
- package/dist/gateway-client.js.map +1 -1
- package/dist/gateway-service.d.ts +3 -3
- package/dist/gateway-service.d.ts.map +1 -1
- package/dist/gateway-service.js +563 -73
- package/dist/gateway-service.js.map +1 -1
- package/dist/gateway-types.d.ts +24 -0
- package/dist/gateway-types.d.ts.map +1 -1
- package/dist/gateway-types.js.map +1 -1
- package/dist/gateway.js +179 -24
- package/dist/gateway.js.map +1 -1
- package/dist/health.d.ts.map +1 -1
- package/dist/health.js +20 -10
- package/dist/health.js.map +1 -1
- package/dist/heartbeat.d.ts +4 -0
- package/dist/heartbeat.d.ts.map +1 -1
- package/dist/heartbeat.js +48 -20
- package/dist/heartbeat.js.map +1 -1
- package/dist/hybridai-bots.d.ts.map +1 -1
- package/dist/hybridai-bots.js +4 -2
- package/dist/hybridai-bots.js.map +1 -1
- package/dist/instruction-approval-audit.d.ts.map +1 -1
- package/dist/instruction-approval-audit.js.map +1 -1
- package/dist/instruction-integrity.d.ts.map +1 -1
- package/dist/instruction-integrity.js +8 -2
- package/dist/instruction-integrity.js.map +1 -1
- package/dist/ipc.d.ts.map +1 -1
- package/dist/ipc.js +6 -1
- package/dist/ipc.js.map +1 -1
- package/dist/logger.js.map +1 -1
- package/dist/memory-consolidation.d.ts +17 -0
- package/dist/memory-consolidation.d.ts.map +1 -0
- package/dist/memory-consolidation.js +25 -0
- package/dist/memory-consolidation.js.map +1 -0
- package/dist/memory-service.d.ts +200 -0
- package/dist/memory-service.d.ts.map +1 -0
- package/dist/memory-service.js +294 -0
- package/dist/memory-service.js.map +1 -0
- package/dist/mount-security.d.ts.map +1 -1
- package/dist/mount-security.js +31 -7
- package/dist/mount-security.js.map +1 -1
- package/dist/observability-ingest.d.ts.map +1 -1
- package/dist/observability-ingest.js +32 -11
- package/dist/observability-ingest.js.map +1 -1
- package/dist/onboarding.d.ts.map +1 -1
- package/dist/onboarding.js +32 -9
- package/dist/onboarding.js.map +1 -1
- package/dist/proactive-policy.d.ts.map +1 -1
- package/dist/proactive-policy.js +2 -1
- package/dist/proactive-policy.js.map +1 -1
- package/dist/prompt-hooks.d.ts.map +1 -1
- package/dist/prompt-hooks.js +9 -7
- package/dist/prompt-hooks.js.map +1 -1
- package/dist/runtime-config.d.ts +98 -1
- package/dist/runtime-config.d.ts.map +1 -1
- package/dist/runtime-config.js +477 -23
- package/dist/runtime-config.js.map +1 -1
- package/dist/scheduled-task-runner.d.ts +1 -0
- package/dist/scheduled-task-runner.d.ts.map +1 -1
- package/dist/scheduled-task-runner.js +29 -10
- package/dist/scheduled-task-runner.js.map +1 -1
- package/dist/scheduler.d.ts +43 -4
- package/dist/scheduler.d.ts.map +1 -1
- package/dist/scheduler.js +530 -56
- package/dist/scheduler.js.map +1 -1
- package/dist/session-export.d.ts +26 -0
- package/dist/session-export.d.ts.map +1 -0
- package/dist/session-export.js +149 -0
- package/dist/session-export.js.map +1 -0
- package/dist/session-maintenance.d.ts.map +1 -1
- package/dist/session-maintenance.js +75 -13
- package/dist/session-maintenance.js.map +1 -1
- package/dist/session-transcripts.d.ts.map +1 -1
- package/dist/session-transcripts.js.map +1 -1
- package/dist/side-effects.d.ts.map +1 -1
- package/dist/side-effects.js +14 -2
- package/dist/side-effects.js.map +1 -1
- package/dist/skills-guard.d.ts.map +1 -1
- package/dist/skills-guard.js +893 -130
- package/dist/skills-guard.js.map +1 -1
- package/dist/skills.d.ts +5 -0
- package/dist/skills.d.ts.map +1 -1
- package/dist/skills.js +29 -15
- package/dist/skills.js.map +1 -1
- package/dist/token-efficiency.d.ts.map +1 -1
- package/dist/token-efficiency.js.map +1 -1
- package/dist/tui.js +92 -11
- package/dist/tui.js.map +1 -1
- package/dist/types.d.ts +146 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +24 -1
- package/dist/types.js.map +1 -1
- package/dist/update.d.ts.map +1 -1
- package/dist/update.js +42 -14
- package/dist/update.js.map +1 -1
- package/dist/workspace.d.ts.map +1 -1
- package/dist/workspace.js +49 -9
- package/dist/workspace.js.map +1 -1
- package/docs/chat.html +9 -3
- package/docs/index.html +37 -13
- package/package.json +8 -2
- package/src/agent.ts +16 -3
- package/src/audit-cli.ts +44 -16
- package/src/audit-events.ts +69 -5
- package/src/audit-trail.ts +41 -15
- package/src/channels/discord/attachments.ts +81 -27
- package/src/channels/discord/debounce.ts +25 -0
- package/src/channels/discord/delivery.ts +57 -13
- package/src/channels/discord/human-delay.ts +48 -0
- package/src/channels/discord/inbound.ts +66 -7
- package/src/channels/discord/mentions.ts +42 -18
- package/src/channels/discord/presence.ts +148 -0
- package/src/channels/discord/rate-limiter.ts +58 -0
- package/src/channels/discord/reactions.ts +211 -0
- package/src/channels/discord/runtime.ts +1048 -182
- package/src/channels/discord/stream.ts +73 -27
- package/src/channels/discord/tool-actions.ts +78 -37
- package/src/channels/discord/typing.ts +140 -0
- package/src/chunk.ts +12 -4
- package/src/cli.ts +141 -56
- package/src/config.ts +192 -34
- package/src/container-runner.ts +132 -42
- package/src/container-setup.ts +57 -22
- package/src/conversation.ts +9 -7
- package/src/db.ts +2217 -84
- package/src/delegation-manager.ts +6 -2
- package/src/gateway-client.ts +41 -17
- package/src/gateway-service.ts +1019 -201
- package/src/gateway-types.ts +33 -0
- package/src/gateway.ts +321 -48
- package/src/health.ts +66 -26
- package/src/heartbeat.ts +84 -22
- package/src/hybridai-bots.ts +14 -5
- package/src/instruction-approval-audit.ts +4 -1
- package/src/instruction-integrity.ts +30 -9
- package/src/ipc.ts +23 -5
- package/src/logger.ts +4 -1
- package/src/memory-consolidation.ts +41 -0
- package/src/memory-service.ts +606 -0
- package/src/mount-security.ts +58 -13
- package/src/observability-ingest.ts +134 -35
- package/src/onboarding.ts +126 -35
- package/src/proactive-policy.ts +3 -1
- package/src/prompt-hooks.ts +40 -17
- package/src/runtime-config.ts +1114 -99
- package/src/scheduled-task-runner.ts +63 -11
- package/src/scheduler.ts +683 -60
- package/src/session-export.ts +196 -0
- package/src/session-maintenance.ts +125 -22
- package/src/session-transcripts.ts +12 -3
- package/src/side-effects.ts +28 -5
- package/src/skills-guard.ts +1067 -219
- package/src/skills.ts +163 -65
- package/src/token-efficiency.ts +31 -9
- package/src/tui.ts +166 -25
- package/src/types.ts +195 -2
- package/src/update.ts +79 -23
- package/src/workspace.ts +63 -11
- package/tests/approval-policy.test.ts +224 -0
- package/tests/discord.basic.test.ts +82 -2
- package/tests/discord.human-presence.test.ts +85 -0
- package/tests/gateway-service.media-routing.test.ts +8 -2
- package/tests/memory-service.test.ts +1114 -0
- package/tests/token-efficiency.basic.test.ts +8 -2
- package/vitest.e2e.config.ts +3 -1
- package/vitest.integration.config.ts +3 -1
- package/vitest.live.config.ts +3 -1
- 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
|
-
|
|
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
|
|
200
|
+
function migrateV2(database, opts) {
|
|
113
201
|
const quiet = opts?.quiet === true;
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
database
|
|
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
|
|
317
|
+
logger.info('Migrated legacy memory_kv rows into kv_store');
|
|
139
318
|
}
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
-
|
|
153
|
-
|
|
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(
|
|
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
|
|
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
|
|
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
|
-
|
|
196
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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
|
|
1737
|
+
return db
|
|
1738
|
+
.prepare('SELECT * FROM tasks WHERE enabled = 1')
|
|
1739
|
+
.all();
|
|
246
1740
|
}
|
|
247
1741
|
export function updateTaskLastRun(taskId) {
|
|
248
|
-
db.prepare(
|
|
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(
|
|
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();
|