twinclaw 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +66 -0
- package/bin/npm-twinclaw.js +17 -0
- package/bin/run-twinbot-cli.js +36 -0
- package/bin/twinbot.js +4 -0
- package/bin/twinclaw.js +4 -0
- package/dist/api/handlers/browser.js +160 -0
- package/dist/api/handlers/callback.js +80 -0
- package/dist/api/handlers/config-validate.js +19 -0
- package/dist/api/handlers/health.js +117 -0
- package/dist/api/handlers/local-state-backup.js +118 -0
- package/dist/api/handlers/persona-state.js +59 -0
- package/dist/api/handlers/skill-packages.js +94 -0
- package/dist/api/router.js +278 -0
- package/dist/api/runtime-event-producer.js +99 -0
- package/dist/api/shared.js +82 -0
- package/dist/api/websocket-hub.js +305 -0
- package/dist/config/config-loader.js +2 -0
- package/dist/config/env-schema.js +202 -0
- package/dist/config/env-validator.js +223 -0
- package/dist/config/identity-bootstrap.js +115 -0
- package/dist/config/json-config.js +344 -0
- package/dist/config/workspace.js +186 -0
- package/dist/core/channels-cli.js +77 -0
- package/dist/core/cli.js +119 -0
- package/dist/core/context-assembly.js +33 -0
- package/dist/core/doctor.js +365 -0
- package/dist/core/gateway-cli.js +323 -0
- package/dist/core/gateway.js +416 -0
- package/dist/core/heartbeat.js +54 -0
- package/dist/core/install-cli.js +320 -0
- package/dist/core/lane-executor.js +134 -0
- package/dist/core/logs-cli.js +70 -0
- package/dist/core/onboarding.js +760 -0
- package/dist/core/pairing-cli.js +78 -0
- package/dist/core/secret-vault-cli.js +204 -0
- package/dist/core/types.js +1 -0
- package/dist/index.js +404 -0
- package/dist/interfaces/dispatcher.js +214 -0
- package/dist/interfaces/telegram_handler.js +82 -0
- package/dist/interfaces/tui-dashboard.js +53 -0
- package/dist/interfaces/whatsapp_handler.js +94 -0
- package/dist/release/cli.js +97 -0
- package/dist/release/mvp-gate-cli.js +118 -0
- package/dist/release/twinbot-config-schema.js +162 -0
- package/dist/release/twinclaw-config-schema.js +162 -0
- package/dist/services/block-chunker.js +174 -0
- package/dist/services/browser-service.js +334 -0
- package/dist/services/context-lifecycle.js +314 -0
- package/dist/services/db.js +1055 -0
- package/dist/services/delivery-tracker.js +110 -0
- package/dist/services/dm-pairing.js +245 -0
- package/dist/services/embedding-service.js +125 -0
- package/dist/services/file-watcher.js +125 -0
- package/dist/services/inbound-debounce.js +92 -0
- package/dist/services/incident-manager.js +516 -0
- package/dist/services/job-scheduler.js +176 -0
- package/dist/services/local-state-backup.js +682 -0
- package/dist/services/mcp-client-adapter.js +291 -0
- package/dist/services/mcp-server-manager.js +143 -0
- package/dist/services/model-router.js +927 -0
- package/dist/services/mvp-gate.js +845 -0
- package/dist/services/orchestration-service.js +422 -0
- package/dist/services/persona-state.js +256 -0
- package/dist/services/policy-engine.js +92 -0
- package/dist/services/proactive-notifier.js +94 -0
- package/dist/services/queue-service.js +146 -0
- package/dist/services/release-pipeline.js +652 -0
- package/dist/services/runtime-budget-governor.js +415 -0
- package/dist/services/secret-vault.js +704 -0
- package/dist/services/semantic-memory.js +249 -0
- package/dist/services/skill-package-manager.js +806 -0
- package/dist/services/skill-registry.js +122 -0
- package/dist/services/streaming-output.js +75 -0
- package/dist/services/stt-service.js +39 -0
- package/dist/services/tts-service.js +44 -0
- package/dist/skills/builtin.js +250 -0
- package/dist/skills/shell.js +87 -0
- package/dist/skills/types.js +1 -0
- package/dist/types/api.js +1 -0
- package/dist/types/context-budget.js +1 -0
- package/dist/types/doctor.js +1 -0
- package/dist/types/file-watcher.js +1 -0
- package/dist/types/incident.js +1 -0
- package/dist/types/local-state-backup.js +1 -0
- package/dist/types/mcp.js +1 -0
- package/dist/types/messaging.js +1 -0
- package/dist/types/model-routing.js +1 -0
- package/dist/types/mvp-gate.js +2 -0
- package/dist/types/orchestration.js +1 -0
- package/dist/types/persona-state.js +22 -0
- package/dist/types/policy.js +1 -0
- package/dist/types/reasoning-graph.js +1 -0
- package/dist/types/release.js +1 -0
- package/dist/types/reliability.js +1 -0
- package/dist/types/runtime-budget.js +1 -0
- package/dist/types/scheduler.js +1 -0
- package/dist/types/secret-vault.js +1 -0
- package/dist/types/skill-packages.js +1 -0
- package/dist/types/websocket.js +14 -0
- package/dist/utils/logger.js +57 -0
- package/dist/utils/retry.js +61 -0
- package/dist/utils/secret-scan.js +208 -0
- package/mcp-servers.json +179 -0
- package/package.json +81 -0
- package/skill-packages.json +92 -0
- package/skill-packages.lock.json +5 -0
- package/src/skills/builtin.ts +275 -0
- package/src/skills/shell.ts +118 -0
- package/src/skills/types.ts +30 -0
- package/src/types/api.ts +252 -0
- package/src/types/blessed-contrib.d.ts +4 -0
- package/src/types/context-budget.ts +76 -0
- package/src/types/doctor.ts +29 -0
- package/src/types/file-watcher.ts +26 -0
- package/src/types/incident.ts +57 -0
- package/src/types/local-state-backup.ts +121 -0
- package/src/types/mcp.ts +106 -0
- package/src/types/messaging.ts +35 -0
- package/src/types/model-routing.ts +61 -0
- package/src/types/mvp-gate.ts +99 -0
- package/src/types/orchestration.ts +65 -0
- package/src/types/persona-state.ts +61 -0
- package/src/types/policy.ts +27 -0
- package/src/types/reasoning-graph.ts +58 -0
- package/src/types/release.ts +115 -0
- package/src/types/reliability.ts +43 -0
- package/src/types/runtime-budget.ts +85 -0
- package/src/types/scheduler.ts +47 -0
- package/src/types/secret-vault.ts +62 -0
- package/src/types/skill-packages.ts +81 -0
- package/src/types/sqlite-vec.d.ts +5 -0
- package/src/types/websocket.ts +122 -0
|
@@ -0,0 +1,1055 @@
|
|
|
1
|
+
import Database from 'better-sqlite3';
|
|
2
|
+
import * as sqliteVec from 'sqlite-vec';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import fs from 'fs';
|
|
5
|
+
import { getConfigValue } from '../config/config-loader.js';
|
|
6
|
+
import { getDatabasePath, ensureWorkspaceSubdirs } from '../config/workspace.js';
|
|
7
|
+
ensureWorkspaceSubdirs();
|
|
8
|
+
const DB_PATH = getDatabasePath();
|
|
9
|
+
const MEMORY_EMBEDDING_DIM = Number(getConfigValue('MEMORY_EMBEDDING_DIM') ?? '1536') || 1536;
|
|
10
|
+
if (!fs.existsSync(path.dirname(DB_PATH))) {
|
|
11
|
+
fs.mkdirSync(path.dirname(DB_PATH), { recursive: true });
|
|
12
|
+
}
|
|
13
|
+
export const db = new Database(DB_PATH);
|
|
14
|
+
// Load sqlite-vec C extension
|
|
15
|
+
sqliteVec.load(db);
|
|
16
|
+
db.pragma('foreign_keys = ON');
|
|
17
|
+
db.exec(`
|
|
18
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
19
|
+
session_id TEXT PRIMARY KEY,
|
|
20
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
CREATE TABLE IF NOT EXISTS messages (
|
|
24
|
+
id TEXT PRIMARY KEY,
|
|
25
|
+
session_id TEXT,
|
|
26
|
+
role TEXT,
|
|
27
|
+
content TEXT,
|
|
28
|
+
FOREIGN KEY(session_id) REFERENCES sessions(session_id)
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
CREATE TABLE IF NOT EXISTS orchestration_jobs (
|
|
32
|
+
id TEXT PRIMARY KEY,
|
|
33
|
+
session_id TEXT NOT NULL,
|
|
34
|
+
parent_message TEXT NOT NULL,
|
|
35
|
+
brief_json TEXT NOT NULL,
|
|
36
|
+
state TEXT NOT NULL,
|
|
37
|
+
attempt INTEGER NOT NULL DEFAULT 1,
|
|
38
|
+
output TEXT,
|
|
39
|
+
error TEXT,
|
|
40
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
41
|
+
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
42
|
+
started_at DATETIME,
|
|
43
|
+
completed_at DATETIME,
|
|
44
|
+
FOREIGN KEY(session_id) REFERENCES sessions(session_id)
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
CREATE TABLE IF NOT EXISTS orchestration_events (
|
|
48
|
+
id TEXT PRIMARY KEY,
|
|
49
|
+
job_id TEXT NOT NULL,
|
|
50
|
+
session_id TEXT NOT NULL,
|
|
51
|
+
state TEXT NOT NULL,
|
|
52
|
+
detail TEXT,
|
|
53
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
54
|
+
FOREIGN KEY(job_id) REFERENCES orchestration_jobs(id),
|
|
55
|
+
FOREIGN KEY(session_id) REFERENCES sessions(session_id)
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
-- Virtual table for vector search
|
|
59
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS vec_memory USING vec0(
|
|
60
|
+
embedding float[${MEMORY_EMBEDDING_DIM}],
|
|
61
|
+
session_id TEXT,
|
|
62
|
+
fact_text TEXT
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
CREATE TABLE IF NOT EXISTS reasoning_nodes (
|
|
66
|
+
node_id TEXT PRIMARY KEY,
|
|
67
|
+
claim_key TEXT NOT NULL,
|
|
68
|
+
node_type TEXT NOT NULL,
|
|
69
|
+
source_role TEXT NOT NULL,
|
|
70
|
+
canonical_text TEXT NOT NULL,
|
|
71
|
+
polarity INTEGER NOT NULL DEFAULT 1,
|
|
72
|
+
confidence REAL NOT NULL DEFAULT 0.5,
|
|
73
|
+
first_session_id TEXT NOT NULL,
|
|
74
|
+
last_session_id TEXT NOT NULL,
|
|
75
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
76
|
+
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
CREATE INDEX IF NOT EXISTS idx_reasoning_nodes_claim_key
|
|
80
|
+
ON reasoning_nodes(claim_key);
|
|
81
|
+
|
|
82
|
+
CREATE INDEX IF NOT EXISTS idx_reasoning_nodes_updated_at
|
|
83
|
+
ON reasoning_nodes(updated_at DESC);
|
|
84
|
+
|
|
85
|
+
CREATE TABLE IF NOT EXISTS reasoning_edges (
|
|
86
|
+
edge_id TEXT PRIMARY KEY,
|
|
87
|
+
from_node_id TEXT NOT NULL,
|
|
88
|
+
to_node_id TEXT NOT NULL,
|
|
89
|
+
relation TEXT NOT NULL,
|
|
90
|
+
weight REAL NOT NULL DEFAULT 1.0,
|
|
91
|
+
provenance TEXT NOT NULL,
|
|
92
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
93
|
+
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
94
|
+
FOREIGN KEY(from_node_id) REFERENCES reasoning_nodes(node_id),
|
|
95
|
+
FOREIGN KEY(to_node_id) REFERENCES reasoning_nodes(node_id),
|
|
96
|
+
UNIQUE(from_node_id, to_node_id, relation)
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
CREATE INDEX IF NOT EXISTS idx_reasoning_edges_from
|
|
100
|
+
ON reasoning_edges(from_node_id);
|
|
101
|
+
|
|
102
|
+
CREATE INDEX IF NOT EXISTS idx_reasoning_edges_to
|
|
103
|
+
ON reasoning_edges(to_node_id);
|
|
104
|
+
|
|
105
|
+
CREATE TABLE IF NOT EXISTS memory_provenance (
|
|
106
|
+
memory_rowid INTEGER PRIMARY KEY,
|
|
107
|
+
node_id TEXT NOT NULL,
|
|
108
|
+
session_id TEXT NOT NULL,
|
|
109
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
110
|
+
FOREIGN KEY(node_id) REFERENCES reasoning_nodes(node_id)
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
CREATE TABLE IF NOT EXISTS policy_audit_logs (
|
|
114
|
+
id TEXT PRIMARY KEY,
|
|
115
|
+
session_id TEXT,
|
|
116
|
+
skill_name TEXT NOT NULL,
|
|
117
|
+
action TEXT NOT NULL,
|
|
118
|
+
reason TEXT NOT NULL,
|
|
119
|
+
profile_id TEXT NOT NULL,
|
|
120
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
CREATE TABLE IF NOT EXISTS callback_receipts (
|
|
124
|
+
idempotency_key TEXT PRIMARY KEY,
|
|
125
|
+
status_code INTEGER NOT NULL,
|
|
126
|
+
outcome TEXT NOT NULL,
|
|
127
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
CREATE TABLE IF NOT EXISTS delivery_queue (
|
|
131
|
+
id TEXT PRIMARY KEY,
|
|
132
|
+
platform TEXT NOT NULL,
|
|
133
|
+
chat_id TEXT NOT NULL,
|
|
134
|
+
text_payload TEXT NOT NULL,
|
|
135
|
+
state TEXT NOT NULL,
|
|
136
|
+
attempts INTEGER NOT NULL DEFAULT 0,
|
|
137
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
138
|
+
next_attempt_at DATETIME,
|
|
139
|
+
resolved_at DATETIME
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
CREATE TABLE IF NOT EXISTS delivery_attempts (
|
|
143
|
+
id TEXT PRIMARY KEY,
|
|
144
|
+
delivery_id TEXT NOT NULL,
|
|
145
|
+
attempt_number INTEGER NOT NULL,
|
|
146
|
+
started_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
147
|
+
completed_at DATETIME,
|
|
148
|
+
error TEXT,
|
|
149
|
+
duration_ms INTEGER,
|
|
150
|
+
FOREIGN KEY(delivery_id) REFERENCES delivery_queue(id)
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
CREATE TABLE IF NOT EXISTS mcp_health_events (
|
|
154
|
+
id TEXT PRIMARY KEY,
|
|
155
|
+
server_id TEXT NOT NULL,
|
|
156
|
+
prev_state TEXT NOT NULL,
|
|
157
|
+
new_state TEXT NOT NULL,
|
|
158
|
+
reason TEXT NOT NULL,
|
|
159
|
+
metrics_json TEXT NOT NULL,
|
|
160
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
CREATE TABLE IF NOT EXISTS mcp_scope_audit_logs (
|
|
164
|
+
id TEXT PRIMARY KEY,
|
|
165
|
+
session_id TEXT,
|
|
166
|
+
server_id TEXT NOT NULL,
|
|
167
|
+
tool_name TEXT NOT NULL,
|
|
168
|
+
scope TEXT NOT NULL,
|
|
169
|
+
outcome TEXT NOT NULL, -- 'allowed' | 'denied'
|
|
170
|
+
reason TEXT,
|
|
171
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
CREATE TABLE IF NOT EXISTS model_routing_events (
|
|
175
|
+
id TEXT PRIMARY KEY,
|
|
176
|
+
event_type TEXT NOT NULL,
|
|
177
|
+
model_id TEXT,
|
|
178
|
+
model_name TEXT,
|
|
179
|
+
provider TEXT,
|
|
180
|
+
fallback_mode TEXT NOT NULL,
|
|
181
|
+
detail_json TEXT NOT NULL DEFAULT '{}',
|
|
182
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
CREATE INDEX IF NOT EXISTS idx_model_routing_events_created
|
|
186
|
+
ON model_routing_events(created_at DESC);
|
|
187
|
+
|
|
188
|
+
CREATE TABLE IF NOT EXISTS model_routing_settings (
|
|
189
|
+
setting_key TEXT PRIMARY KEY,
|
|
190
|
+
setting_value TEXT NOT NULL,
|
|
191
|
+
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
CREATE TABLE IF NOT EXISTS local_state_snapshots (
|
|
195
|
+
snapshot_id TEXT PRIMARY KEY,
|
|
196
|
+
trigger_type TEXT NOT NULL,
|
|
197
|
+
status TEXT NOT NULL,
|
|
198
|
+
scopes_json TEXT NOT NULL,
|
|
199
|
+
entry_count INTEGER NOT NULL,
|
|
200
|
+
manifest_path TEXT NOT NULL,
|
|
201
|
+
checksum TEXT,
|
|
202
|
+
detail TEXT,
|
|
203
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
204
|
+
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
CREATE TABLE IF NOT EXISTS local_state_restore_events (
|
|
208
|
+
id TEXT PRIMARY KEY,
|
|
209
|
+
snapshot_id TEXT,
|
|
210
|
+
outcome TEXT NOT NULL,
|
|
211
|
+
dry_run INTEGER NOT NULL DEFAULT 0,
|
|
212
|
+
scopes_json TEXT NOT NULL,
|
|
213
|
+
restored_paths_json TEXT NOT NULL,
|
|
214
|
+
skipped_paths_json TEXT NOT NULL,
|
|
215
|
+
validation_errors_json TEXT NOT NULL,
|
|
216
|
+
rollback_applied INTEGER NOT NULL DEFAULT 0,
|
|
217
|
+
detail TEXT,
|
|
218
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
CREATE INDEX IF NOT EXISTS idx_local_state_snapshots_created
|
|
222
|
+
ON local_state_snapshots(created_at DESC);
|
|
223
|
+
|
|
224
|
+
CREATE INDEX IF NOT EXISTS idx_local_state_restore_events_created
|
|
225
|
+
ON local_state_restore_events(created_at DESC);
|
|
226
|
+
|
|
227
|
+
CREATE TABLE IF NOT EXISTS incidents (
|
|
228
|
+
id TEXT PRIMARY KEY,
|
|
229
|
+
incident_type TEXT NOT NULL,
|
|
230
|
+
severity TEXT NOT NULL,
|
|
231
|
+
status TEXT NOT NULL,
|
|
232
|
+
summary TEXT NOT NULL,
|
|
233
|
+
evidence_json TEXT NOT NULL,
|
|
234
|
+
remediation_action TEXT NOT NULL DEFAULT 'none',
|
|
235
|
+
remediation_attempts INTEGER NOT NULL DEFAULT 0,
|
|
236
|
+
cooldown_until DATETIME,
|
|
237
|
+
escalated INTEGER NOT NULL DEFAULT 0,
|
|
238
|
+
recommended_actions_json TEXT NOT NULL DEFAULT '[]',
|
|
239
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
240
|
+
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
241
|
+
resolved_at DATETIME
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
CREATE TABLE IF NOT EXISTS incident_timeline (
|
|
245
|
+
id TEXT PRIMARY KEY,
|
|
246
|
+
incident_id TEXT NOT NULL,
|
|
247
|
+
incident_type TEXT NOT NULL,
|
|
248
|
+
event_type TEXT NOT NULL,
|
|
249
|
+
detail_json TEXT NOT NULL,
|
|
250
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
251
|
+
FOREIGN KEY(incident_id) REFERENCES incidents(id)
|
|
252
|
+
);
|
|
253
|
+
|
|
254
|
+
CREATE INDEX IF NOT EXISTS idx_incidents_status_updated
|
|
255
|
+
ON incidents(status, updated_at DESC);
|
|
256
|
+
CREATE INDEX IF NOT EXISTS idx_incident_timeline_created
|
|
257
|
+
ON incident_timeline(created_at DESC);
|
|
258
|
+
|
|
259
|
+
CREATE TABLE IF NOT EXISTS runtime_usage_events (
|
|
260
|
+
id TEXT PRIMARY KEY,
|
|
261
|
+
session_id TEXT,
|
|
262
|
+
model_id TEXT NOT NULL,
|
|
263
|
+
provider_id TEXT NOT NULL,
|
|
264
|
+
profile TEXT NOT NULL,
|
|
265
|
+
stage TEXT NOT NULL,
|
|
266
|
+
request_tokens INTEGER NOT NULL DEFAULT 0,
|
|
267
|
+
response_tokens INTEGER NOT NULL DEFAULT 0,
|
|
268
|
+
latency_ms INTEGER NOT NULL DEFAULT 0,
|
|
269
|
+
status_code INTEGER,
|
|
270
|
+
error TEXT,
|
|
271
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
272
|
+
);
|
|
273
|
+
|
|
274
|
+
CREATE TABLE IF NOT EXISTS runtime_budget_events (
|
|
275
|
+
id TEXT PRIMARY KEY,
|
|
276
|
+
session_id TEXT,
|
|
277
|
+
severity TEXT NOT NULL,
|
|
278
|
+
profile TEXT NOT NULL,
|
|
279
|
+
action TEXT NOT NULL,
|
|
280
|
+
reason TEXT NOT NULL,
|
|
281
|
+
detail_json TEXT NOT NULL,
|
|
282
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
283
|
+
);
|
|
284
|
+
|
|
285
|
+
CREATE TABLE IF NOT EXISTS runtime_budget_state (
|
|
286
|
+
key TEXT PRIMARY KEY,
|
|
287
|
+
value TEXT NOT NULL,
|
|
288
|
+
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
289
|
+
);
|
|
290
|
+
|
|
291
|
+
CREATE INDEX IF NOT EXISTS idx_runtime_usage_events_created
|
|
292
|
+
ON runtime_usage_events(created_at DESC);
|
|
293
|
+
CREATE INDEX IF NOT EXISTS idx_runtime_usage_events_provider
|
|
294
|
+
ON runtime_usage_events(provider_id, created_at DESC);
|
|
295
|
+
CREATE INDEX IF NOT EXISTS idx_runtime_budget_events_created
|
|
296
|
+
ON runtime_budget_events(created_at DESC);
|
|
297
|
+
`);
|
|
298
|
+
function serializeEmbedding(embedding) {
|
|
299
|
+
const sqliteVecWithSerializer = sqliteVec;
|
|
300
|
+
if (typeof sqliteVecWithSerializer.serializeFloat32 === 'function') {
|
|
301
|
+
return sqliteVecWithSerializer.serializeFloat32(embedding);
|
|
302
|
+
}
|
|
303
|
+
return Buffer.from(new Float32Array(embedding).buffer);
|
|
304
|
+
}
|
|
305
|
+
export function createSession(sessionId) {
|
|
306
|
+
const stmt = db.prepare('INSERT OR IGNORE INTO sessions (session_id) VALUES (?)');
|
|
307
|
+
stmt.run(sessionId);
|
|
308
|
+
}
|
|
309
|
+
export function saveMessage(id, sessionId, role, content) {
|
|
310
|
+
const stmt = db.prepare('INSERT INTO messages (id, session_id, role, content) VALUES (?, ?, ?, ?)');
|
|
311
|
+
stmt.run(id, sessionId, role, content);
|
|
312
|
+
}
|
|
313
|
+
export function getSessionMessages(sessionId) {
|
|
314
|
+
const stmt = db.prepare('SELECT * FROM messages WHERE session_id = ? ORDER BY rowid ASC');
|
|
315
|
+
return stmt.all(sessionId);
|
|
316
|
+
}
|
|
317
|
+
export function saveMemoryEmbedding(sessionId, factText, embedding) {
|
|
318
|
+
const stmt = db.prepare('INSERT INTO vec_memory (embedding, session_id, fact_text) VALUES (?, ?, ?)');
|
|
319
|
+
const result = stmt.run(serializeEmbedding(embedding), sessionId, factText);
|
|
320
|
+
return Number(result.lastInsertRowid);
|
|
321
|
+
}
|
|
322
|
+
export function getNearestMemories(queryEmbedding, topK = 5, currentSessionId) {
|
|
323
|
+
const matcher = serializeEmbedding(queryEmbedding);
|
|
324
|
+
const stmt = db.prepare('SELECT rowid AS memory_rowid, session_id, fact_text, distance FROM vec_memory WHERE embedding MATCH ? ORDER BY distance ASC LIMIT ?');
|
|
325
|
+
const rows = stmt.all(matcher, topK * 3);
|
|
326
|
+
if (!currentSessionId) {
|
|
327
|
+
return rows.slice(0, topK);
|
|
328
|
+
}
|
|
329
|
+
const scoped = rows.filter((row) => row.session_id === currentSessionId);
|
|
330
|
+
const global = rows.filter((row) => row.session_id !== currentSessionId);
|
|
331
|
+
return [...scoped, ...global].slice(0, topK);
|
|
332
|
+
}
|
|
333
|
+
export function upsertReasoningNode(input) {
|
|
334
|
+
const stmt = db.prepare(`
|
|
335
|
+
INSERT INTO reasoning_nodes (
|
|
336
|
+
node_id,
|
|
337
|
+
claim_key,
|
|
338
|
+
node_type,
|
|
339
|
+
source_role,
|
|
340
|
+
canonical_text,
|
|
341
|
+
polarity,
|
|
342
|
+
confidence,
|
|
343
|
+
first_session_id,
|
|
344
|
+
last_session_id
|
|
345
|
+
)
|
|
346
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
347
|
+
ON CONFLICT(node_id) DO UPDATE SET
|
|
348
|
+
claim_key = excluded.claim_key,
|
|
349
|
+
node_type = excluded.node_type,
|
|
350
|
+
source_role = excluded.source_role,
|
|
351
|
+
canonical_text = excluded.canonical_text,
|
|
352
|
+
polarity = excluded.polarity,
|
|
353
|
+
confidence = excluded.confidence,
|
|
354
|
+
last_session_id = excluded.last_session_id,
|
|
355
|
+
updated_at = CURRENT_TIMESTAMP
|
|
356
|
+
`);
|
|
357
|
+
stmt.run(input.nodeId, input.claimKey, input.nodeType, input.sourceRole, input.canonicalText, input.polarity, input.confidence, input.sessionId, input.sessionId);
|
|
358
|
+
}
|
|
359
|
+
export function upsertReasoningEdge(input) {
|
|
360
|
+
const stmt = db.prepare(`
|
|
361
|
+
INSERT INTO reasoning_edges (
|
|
362
|
+
edge_id,
|
|
363
|
+
from_node_id,
|
|
364
|
+
to_node_id,
|
|
365
|
+
relation,
|
|
366
|
+
weight,
|
|
367
|
+
provenance
|
|
368
|
+
)
|
|
369
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
370
|
+
ON CONFLICT(from_node_id, to_node_id, relation) DO UPDATE SET
|
|
371
|
+
weight = excluded.weight,
|
|
372
|
+
provenance = excluded.provenance,
|
|
373
|
+
updated_at = CURRENT_TIMESTAMP
|
|
374
|
+
`);
|
|
375
|
+
stmt.run(input.edgeId, input.fromNodeId, input.toNodeId, input.relation, input.weight, input.provenance);
|
|
376
|
+
}
|
|
377
|
+
export function linkMemoryProvenance(memoryRowId, nodeId, sessionId) {
|
|
378
|
+
const stmt = db.prepare(`
|
|
379
|
+
INSERT INTO memory_provenance (memory_rowid, node_id, session_id)
|
|
380
|
+
VALUES (?, ?, ?)
|
|
381
|
+
ON CONFLICT(memory_rowid) DO UPDATE SET
|
|
382
|
+
node_id = excluded.node_id,
|
|
383
|
+
session_id = excluded.session_id,
|
|
384
|
+
created_at = CURRENT_TIMESTAMP
|
|
385
|
+
`);
|
|
386
|
+
stmt.run(memoryRowId, nodeId, sessionId);
|
|
387
|
+
}
|
|
388
|
+
export function getReasoningNodesByClaimKey(claimKey) {
|
|
389
|
+
const stmt = db.prepare(`
|
|
390
|
+
SELECT node_id, polarity
|
|
391
|
+
FROM reasoning_nodes
|
|
392
|
+
WHERE claim_key = ?
|
|
393
|
+
ORDER BY updated_at DESC
|
|
394
|
+
LIMIT 24
|
|
395
|
+
`);
|
|
396
|
+
return stmt.all(claimKey);
|
|
397
|
+
}
|
|
398
|
+
export function getMemoryProvenanceRows(memoryRowIds) {
|
|
399
|
+
if (memoryRowIds.length === 0) {
|
|
400
|
+
return [];
|
|
401
|
+
}
|
|
402
|
+
const placeholders = memoryRowIds.map(() => '?').join(', ');
|
|
403
|
+
const stmt = db.prepare(`
|
|
404
|
+
SELECT
|
|
405
|
+
mp.memory_rowid AS memoryRowId,
|
|
406
|
+
mp.node_id AS nodeId,
|
|
407
|
+
rn.claim_key AS claimKey,
|
|
408
|
+
rn.node_type AS nodeType,
|
|
409
|
+
rn.polarity AS polarity,
|
|
410
|
+
rn.canonical_text AS canonicalText,
|
|
411
|
+
rn.updated_at AS updatedAt,
|
|
412
|
+
SUM(CASE WHEN re.relation = 'supports' THEN 1 ELSE 0 END) AS supportsCount,
|
|
413
|
+
SUM(CASE WHEN re.relation = 'contradicts' THEN 1 ELSE 0 END) AS contradictsCount,
|
|
414
|
+
SUM(CASE WHEN re.relation = 'depends_on' THEN 1 ELSE 0 END) AS dependsCount,
|
|
415
|
+
SUM(CASE WHEN re.relation = 'derived_from' THEN 1 ELSE 0 END) AS derivedCount
|
|
416
|
+
FROM memory_provenance mp
|
|
417
|
+
INNER JOIN reasoning_nodes rn ON rn.node_id = mp.node_id
|
|
418
|
+
LEFT JOIN reasoning_edges re ON re.from_node_id = rn.node_id OR re.to_node_id = rn.node_id
|
|
419
|
+
WHERE mp.memory_rowid IN (${placeholders})
|
|
420
|
+
GROUP BY
|
|
421
|
+
mp.memory_rowid,
|
|
422
|
+
mp.node_id,
|
|
423
|
+
rn.claim_key,
|
|
424
|
+
rn.node_type,
|
|
425
|
+
rn.polarity,
|
|
426
|
+
rn.canonical_text,
|
|
427
|
+
rn.updated_at
|
|
428
|
+
`);
|
|
429
|
+
return stmt.all(...memoryRowIds).map((row) => ({
|
|
430
|
+
memoryRowId: Number(row.memoryRowId),
|
|
431
|
+
nodeId: String(row.nodeId),
|
|
432
|
+
claimKey: String(row.claimKey),
|
|
433
|
+
nodeType: String(row.nodeType),
|
|
434
|
+
polarity: Number(row.polarity) < 0 ? -1 : 1,
|
|
435
|
+
canonicalText: String(row.canonicalText),
|
|
436
|
+
updatedAt: String(row.updatedAt),
|
|
437
|
+
supportsCount: Number(row.supportsCount ?? 0),
|
|
438
|
+
contradictsCount: Number(row.contradictsCount ?? 0),
|
|
439
|
+
dependsCount: Number(row.dependsCount ?? 0),
|
|
440
|
+
derivedCount: Number(row.derivedCount ?? 0),
|
|
441
|
+
}));
|
|
442
|
+
}
|
|
443
|
+
export function getReasoningEvidenceExpansion(seedNodeIds, maxDepth, limit) {
|
|
444
|
+
if (seedNodeIds.length === 0 || maxDepth < 1 || limit < 1) {
|
|
445
|
+
return [];
|
|
446
|
+
}
|
|
447
|
+
const visited = new Set(seedNodeIds);
|
|
448
|
+
let frontier = new Set(seedNodeIds);
|
|
449
|
+
const collected = new Map();
|
|
450
|
+
for (let depth = 0; depth < maxDepth; depth += 1) {
|
|
451
|
+
if (frontier.size === 0 || collected.size >= limit) {
|
|
452
|
+
break;
|
|
453
|
+
}
|
|
454
|
+
const frontierIds = Array.from(frontier);
|
|
455
|
+
const placeholders = frontierIds.map(() => '?').join(', ');
|
|
456
|
+
const remaining = Math.max(1, limit - collected.size);
|
|
457
|
+
const stmt = db.prepare(`
|
|
458
|
+
SELECT edge_id, from_node_id, to_node_id, relation, weight, provenance, updated_at
|
|
459
|
+
FROM reasoning_edges
|
|
460
|
+
WHERE from_node_id IN (${placeholders}) OR to_node_id IN (${placeholders})
|
|
461
|
+
ORDER BY updated_at DESC
|
|
462
|
+
LIMIT ?
|
|
463
|
+
`);
|
|
464
|
+
const rows = stmt.all(...frontierIds, ...frontierIds, remaining);
|
|
465
|
+
const nextFrontier = new Set();
|
|
466
|
+
for (const row of rows) {
|
|
467
|
+
if (!collected.has(row.edge_id)) {
|
|
468
|
+
collected.set(row.edge_id, {
|
|
469
|
+
edgeId: row.edge_id,
|
|
470
|
+
fromNodeId: row.from_node_id,
|
|
471
|
+
toNodeId: row.to_node_id,
|
|
472
|
+
relation: row.relation,
|
|
473
|
+
weight: row.weight,
|
|
474
|
+
provenance: row.provenance,
|
|
475
|
+
updatedAt: row.updated_at,
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
if (!visited.has(row.from_node_id)) {
|
|
479
|
+
visited.add(row.from_node_id);
|
|
480
|
+
nextFrontier.add(row.from_node_id);
|
|
481
|
+
}
|
|
482
|
+
if (!visited.has(row.to_node_id)) {
|
|
483
|
+
visited.add(row.to_node_id);
|
|
484
|
+
nextFrontier.add(row.to_node_id);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
frontier = nextFrontier;
|
|
488
|
+
}
|
|
489
|
+
return Array.from(collected.values()).slice(0, limit);
|
|
490
|
+
}
|
|
491
|
+
export function createOrchestrationJob(id, sessionId, parentMessage, briefJson) {
|
|
492
|
+
const stmt = db.prepare(`
|
|
493
|
+
INSERT INTO orchestration_jobs (id, session_id, parent_message, brief_json, state, attempt)
|
|
494
|
+
VALUES (?, ?, ?, ?, 'queued', 1)
|
|
495
|
+
`);
|
|
496
|
+
stmt.run(id, sessionId, parentMessage, briefJson);
|
|
497
|
+
}
|
|
498
|
+
export function markOrchestrationJobRunning(id, attempt) {
|
|
499
|
+
const stmt = db.prepare(`
|
|
500
|
+
UPDATE orchestration_jobs
|
|
501
|
+
SET state = 'running',
|
|
502
|
+
attempt = ?,
|
|
503
|
+
started_at = COALESCE(started_at, CURRENT_TIMESTAMP),
|
|
504
|
+
updated_at = CURRENT_TIMESTAMP
|
|
505
|
+
WHERE id = ?
|
|
506
|
+
`);
|
|
507
|
+
stmt.run(attempt, id);
|
|
508
|
+
}
|
|
509
|
+
export function completeOrchestrationJob(id, output) {
|
|
510
|
+
const stmt = db.prepare(`
|
|
511
|
+
UPDATE orchestration_jobs
|
|
512
|
+
SET state = 'completed',
|
|
513
|
+
output = ?,
|
|
514
|
+
error = NULL,
|
|
515
|
+
completed_at = CURRENT_TIMESTAMP,
|
|
516
|
+
updated_at = CURRENT_TIMESTAMP
|
|
517
|
+
WHERE id = ?
|
|
518
|
+
`);
|
|
519
|
+
stmt.run(output, id);
|
|
520
|
+
}
|
|
521
|
+
export function failOrchestrationJob(id, error) {
|
|
522
|
+
const stmt = db.prepare(`
|
|
523
|
+
UPDATE orchestration_jobs
|
|
524
|
+
SET state = 'failed',
|
|
525
|
+
error = ?,
|
|
526
|
+
completed_at = CURRENT_TIMESTAMP,
|
|
527
|
+
updated_at = CURRENT_TIMESTAMP
|
|
528
|
+
WHERE id = ?
|
|
529
|
+
`);
|
|
530
|
+
stmt.run(error, id);
|
|
531
|
+
}
|
|
532
|
+
export function cancelOrchestrationJob(id, reason) {
|
|
533
|
+
const stmt = db.prepare(`
|
|
534
|
+
UPDATE orchestration_jobs
|
|
535
|
+
SET state = 'cancelled',
|
|
536
|
+
error = ?,
|
|
537
|
+
completed_at = CURRENT_TIMESTAMP,
|
|
538
|
+
updated_at = CURRENT_TIMESTAMP
|
|
539
|
+
WHERE id = ?
|
|
540
|
+
`);
|
|
541
|
+
stmt.run(reason, id);
|
|
542
|
+
}
|
|
543
|
+
export function queueOrchestrationRetry(id, attempt, detail) {
|
|
544
|
+
const stmt = db.prepare(`
|
|
545
|
+
UPDATE orchestration_jobs
|
|
546
|
+
SET state = 'queued',
|
|
547
|
+
attempt = ?,
|
|
548
|
+
error = ?,
|
|
549
|
+
updated_at = CURRENT_TIMESTAMP
|
|
550
|
+
WHERE id = ?
|
|
551
|
+
`);
|
|
552
|
+
stmt.run(attempt, detail, id);
|
|
553
|
+
}
|
|
554
|
+
export function saveOrchestrationEvent(input) {
|
|
555
|
+
const stmt = db.prepare(`
|
|
556
|
+
INSERT INTO orchestration_events (id, job_id, session_id, state, detail)
|
|
557
|
+
VALUES (?, ?, ?, ?, ?)
|
|
558
|
+
`);
|
|
559
|
+
stmt.run(input.id, input.jobId, input.sessionId, input.state, input.detail);
|
|
560
|
+
}
|
|
561
|
+
export function savePolicyAuditLog(id, sessionId, skillName, action, reason, profileId) {
|
|
562
|
+
const stmt = db.prepare(`
|
|
563
|
+
INSERT INTO policy_audit_logs (id, session_id, skill_name, action, reason, profile_id)
|
|
564
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
565
|
+
`);
|
|
566
|
+
stmt.run(id, sessionId, skillName, action, reason, profileId);
|
|
567
|
+
}
|
|
568
|
+
export function recordCallbackReceipt(idempotencyKey, statusCode, outcome) {
|
|
569
|
+
const stmt = db.prepare(`
|
|
570
|
+
INSERT OR IGNORE INTO callback_receipts (idempotency_key, status_code, outcome)
|
|
571
|
+
VALUES (?, ?, ?)
|
|
572
|
+
`);
|
|
573
|
+
stmt.run(idempotencyKey, statusCode, outcome);
|
|
574
|
+
}
|
|
575
|
+
export function getCallbackReceipt(idempotencyKey) {
|
|
576
|
+
const stmt = db.prepare(`
|
|
577
|
+
SELECT idempotency_key, status_code, outcome, created_at
|
|
578
|
+
FROM callback_receipts
|
|
579
|
+
WHERE idempotency_key = ?
|
|
580
|
+
`);
|
|
581
|
+
return stmt.get(idempotencyKey);
|
|
582
|
+
}
|
|
583
|
+
export function enqueueDelivery(id, platform, chatId, textPayload) {
|
|
584
|
+
const stmt = db.prepare(`
|
|
585
|
+
INSERT INTO delivery_queue (id, platform, chat_id, text_payload, state, attempts, next_attempt_at)
|
|
586
|
+
VALUES (?, ?, ?, ?, 'queued', 0, CURRENT_TIMESTAMP)
|
|
587
|
+
`);
|
|
588
|
+
stmt.run(id, platform, chatId, textPayload);
|
|
589
|
+
}
|
|
590
|
+
export function getDelivery(id) {
|
|
591
|
+
return db.prepare('SELECT * FROM delivery_queue WHERE id = ?').get(id);
|
|
592
|
+
}
|
|
593
|
+
export function updateDeliveryState(id, state, resolvedAt = null) {
|
|
594
|
+
const stmt = db.prepare(`
|
|
595
|
+
UPDATE delivery_queue
|
|
596
|
+
SET state = ?, resolved_at = ?
|
|
597
|
+
WHERE id = ?
|
|
598
|
+
`);
|
|
599
|
+
stmt.run(state, resolvedAt, id);
|
|
600
|
+
}
|
|
601
|
+
export function updateDeliveryAttempts(id, attempts, nextAttemptAt = null) {
|
|
602
|
+
const stmt = db.prepare(`
|
|
603
|
+
UPDATE delivery_queue
|
|
604
|
+
SET attempts = ?, next_attempt_at = ?
|
|
605
|
+
WHERE id = ?
|
|
606
|
+
`);
|
|
607
|
+
stmt.run(attempts, nextAttemptAt, id);
|
|
608
|
+
}
|
|
609
|
+
export function dequeueDeliveries(limit) {
|
|
610
|
+
// Use a transaction to safely pick deliveries and mark them as 'dispatching'
|
|
611
|
+
const tx = db.transaction((limit) => {
|
|
612
|
+
const fetchStmt = db.prepare(`
|
|
613
|
+
SELECT * FROM delivery_queue
|
|
614
|
+
WHERE (state = 'queued' OR state = 'failed')
|
|
615
|
+
AND (next_attempt_at IS NULL OR next_attempt_at <= CURRENT_TIMESTAMP)
|
|
616
|
+
ORDER BY next_attempt_at ASC, created_at ASC
|
|
617
|
+
LIMIT ?
|
|
618
|
+
`);
|
|
619
|
+
const rows = fetchStmt.all(limit);
|
|
620
|
+
if (rows.length > 0) {
|
|
621
|
+
const ids = rows.map((r) => r.id);
|
|
622
|
+
const updateStmt = db.prepare(`
|
|
623
|
+
UPDATE delivery_queue
|
|
624
|
+
SET state = 'dispatching',
|
|
625
|
+
attempts = attempts + 1
|
|
626
|
+
WHERE id IN (${ids.map(() => '?').join(',')})
|
|
627
|
+
`);
|
|
628
|
+
updateStmt.run(...ids);
|
|
629
|
+
// Return the incremented attempt counts so callers dont have to guess
|
|
630
|
+
for (const row of rows) {
|
|
631
|
+
row.state = 'dispatching';
|
|
632
|
+
row.attempts += 1;
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
return rows;
|
|
636
|
+
});
|
|
637
|
+
return tx(limit);
|
|
638
|
+
}
|
|
639
|
+
export function recordDeliveryAttemptStart(attemptId, deliveryId, attemptNumber, startedAt) {
|
|
640
|
+
const stmt = db.prepare(`
|
|
641
|
+
INSERT INTO delivery_attempts (id, delivery_id, attempt_number, started_at)
|
|
642
|
+
VALUES (?, ?, ?, ?)
|
|
643
|
+
`);
|
|
644
|
+
stmt.run(attemptId, deliveryId, attemptNumber, startedAt);
|
|
645
|
+
}
|
|
646
|
+
export function recordDeliveryAttemptEnd(attemptId, completedAt, error, durationMs) {
|
|
647
|
+
const stmt = db.prepare(`
|
|
648
|
+
UPDATE delivery_attempts
|
|
649
|
+
SET completed_at = ?,
|
|
650
|
+
error = ?,
|
|
651
|
+
duration_ms = ?
|
|
652
|
+
WHERE id = ?
|
|
653
|
+
`);
|
|
654
|
+
stmt.run(completedAt, error, durationMs, attemptId);
|
|
655
|
+
}
|
|
656
|
+
export function getDeliveryAttempts(deliveryId) {
|
|
657
|
+
return db.prepare('SELECT * FROM delivery_attempts WHERE delivery_id = ? ORDER BY attempt_number ASC').all(deliveryId);
|
|
658
|
+
}
|
|
659
|
+
export function getDeliveryMetrics(limit) {
|
|
660
|
+
return db.prepare('SELECT * FROM delivery_queue ORDER BY created_at DESC LIMIT ?').all(limit);
|
|
661
|
+
}
|
|
662
|
+
export function getDeliveryStateCounts() {
|
|
663
|
+
const rows = db
|
|
664
|
+
.prepare('SELECT state, COUNT(*) as count FROM delivery_queue GROUP BY state')
|
|
665
|
+
.all();
|
|
666
|
+
const counts = {};
|
|
667
|
+
for (const row of rows) {
|
|
668
|
+
counts[row.state] = row.count;
|
|
669
|
+
}
|
|
670
|
+
return counts;
|
|
671
|
+
}
|
|
672
|
+
export function getDeadLetters() {
|
|
673
|
+
return db
|
|
674
|
+
.prepare("SELECT * FROM delivery_queue WHERE state = 'dead_letter' ORDER BY COALESCE(resolved_at, created_at) DESC")
|
|
675
|
+
.all();
|
|
676
|
+
}
|
|
677
|
+
// ── MCP Audit & Health ─────────────────────────────────────────────────────
|
|
678
|
+
export function saveMcpHealthEvent(input) {
|
|
679
|
+
const stmt = db.prepare(`
|
|
680
|
+
INSERT INTO mcp_health_events (id, server_id, prev_state, new_state, reason, metrics_json)
|
|
681
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
682
|
+
`);
|
|
683
|
+
stmt.run(input.id, input.serverId, input.prevState, input.newState, input.reason, JSON.stringify(input.metrics));
|
|
684
|
+
}
|
|
685
|
+
export function saveMcpScopeAuditLog(input) {
|
|
686
|
+
const stmt = db.prepare(`
|
|
687
|
+
INSERT INTO mcp_scope_audit_logs (id, session_id, server_id, tool_name, scope, outcome, reason)
|
|
688
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
689
|
+
`);
|
|
690
|
+
stmt.run(input.id, input.sessionId, input.serverId, input.toolName, input.scope, input.outcome, input.reason ?? null);
|
|
691
|
+
}
|
|
692
|
+
export function saveModelRoutingEvent(input, maxRows = 500) {
|
|
693
|
+
const stmt = db.prepare(`
|
|
694
|
+
INSERT INTO model_routing_events (
|
|
695
|
+
id,
|
|
696
|
+
event_type,
|
|
697
|
+
model_id,
|
|
698
|
+
model_name,
|
|
699
|
+
provider,
|
|
700
|
+
fallback_mode,
|
|
701
|
+
detail_json,
|
|
702
|
+
created_at
|
|
703
|
+
)
|
|
704
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
705
|
+
`);
|
|
706
|
+
stmt.run(input.id, input.eventType, input.modelId, input.modelName, input.provider, input.fallbackMode, input.detailJson, input.createdAt ?? new Date().toISOString());
|
|
707
|
+
const boundedLimit = Math.max(50, Math.floor(maxRows));
|
|
708
|
+
db.prepare(`
|
|
709
|
+
DELETE FROM model_routing_events
|
|
710
|
+
WHERE id IN (
|
|
711
|
+
SELECT id
|
|
712
|
+
FROM model_routing_events
|
|
713
|
+
ORDER BY datetime(created_at) DESC
|
|
714
|
+
LIMIT -1 OFFSET ?
|
|
715
|
+
)
|
|
716
|
+
`).run(boundedLimit);
|
|
717
|
+
}
|
|
718
|
+
export function listModelRoutingEvents(limit = 80) {
|
|
719
|
+
const boundedLimit = Math.max(1, Math.min(500, Math.floor(limit)));
|
|
720
|
+
return db.prepare(`
|
|
721
|
+
SELECT id, event_type, model_id, model_name, provider, fallback_mode, detail_json, created_at
|
|
722
|
+
FROM model_routing_events
|
|
723
|
+
ORDER BY datetime(created_at) DESC
|
|
724
|
+
LIMIT ?
|
|
725
|
+
`).all(boundedLimit);
|
|
726
|
+
}
|
|
727
|
+
export function saveModelRoutingSetting(settingKey, settingValue) {
|
|
728
|
+
db.prepare(`
|
|
729
|
+
INSERT INTO model_routing_settings (setting_key, setting_value, updated_at)
|
|
730
|
+
VALUES (?, ?, CURRENT_TIMESTAMP)
|
|
731
|
+
ON CONFLICT(setting_key) DO UPDATE SET
|
|
732
|
+
setting_value = excluded.setting_value,
|
|
733
|
+
updated_at = CURRENT_TIMESTAMP
|
|
734
|
+
`).run(settingKey, settingValue);
|
|
735
|
+
}
|
|
736
|
+
export function getModelRoutingSetting(settingKey) {
|
|
737
|
+
const row = db.prepare(`
|
|
738
|
+
SELECT setting_value
|
|
739
|
+
FROM model_routing_settings
|
|
740
|
+
WHERE setting_key = ?
|
|
741
|
+
`).get(settingKey);
|
|
742
|
+
return row?.setting_value ?? null;
|
|
743
|
+
}
|
|
744
|
+
export function upsertLocalStateSnapshotRecord(input) {
|
|
745
|
+
const createdAt = input.createdAt ?? new Date().toISOString();
|
|
746
|
+
db.prepare(`
|
|
747
|
+
INSERT INTO local_state_snapshots (
|
|
748
|
+
snapshot_id,
|
|
749
|
+
trigger_type,
|
|
750
|
+
status,
|
|
751
|
+
scopes_json,
|
|
752
|
+
entry_count,
|
|
753
|
+
manifest_path,
|
|
754
|
+
checksum,
|
|
755
|
+
detail,
|
|
756
|
+
created_at,
|
|
757
|
+
updated_at
|
|
758
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
759
|
+
ON CONFLICT(snapshot_id) DO UPDATE SET
|
|
760
|
+
trigger_type = excluded.trigger_type,
|
|
761
|
+
status = excluded.status,
|
|
762
|
+
scopes_json = excluded.scopes_json,
|
|
763
|
+
entry_count = excluded.entry_count,
|
|
764
|
+
manifest_path = excluded.manifest_path,
|
|
765
|
+
checksum = excluded.checksum,
|
|
766
|
+
detail = excluded.detail,
|
|
767
|
+
updated_at = excluded.updated_at
|
|
768
|
+
`).run(input.snapshotId, input.triggerType, input.status, JSON.stringify(input.scopes), input.entryCount, input.manifestPath, input.checksum, input.detail ?? null, createdAt, createdAt);
|
|
769
|
+
}
|
|
770
|
+
export function listLocalStateSnapshotRecords(limit = 40) {
|
|
771
|
+
const boundedLimit = Math.max(1, Math.min(500, Math.floor(limit)));
|
|
772
|
+
return db.prepare(`
|
|
773
|
+
SELECT
|
|
774
|
+
snapshot_id,
|
|
775
|
+
trigger_type,
|
|
776
|
+
status,
|
|
777
|
+
scopes_json,
|
|
778
|
+
entry_count,
|
|
779
|
+
manifest_path,
|
|
780
|
+
checksum,
|
|
781
|
+
detail,
|
|
782
|
+
created_at,
|
|
783
|
+
updated_at
|
|
784
|
+
FROM local_state_snapshots
|
|
785
|
+
ORDER BY datetime(created_at) DESC
|
|
786
|
+
LIMIT ?
|
|
787
|
+
`).all(boundedLimit);
|
|
788
|
+
}
|
|
789
|
+
export function removeLocalStateSnapshotRecords(snapshotIds) {
|
|
790
|
+
if (snapshotIds.length === 0) {
|
|
791
|
+
return;
|
|
792
|
+
}
|
|
793
|
+
const placeholders = snapshotIds.map(() => '?').join(', ');
|
|
794
|
+
db.prepare(`
|
|
795
|
+
DELETE FROM local_state_snapshots
|
|
796
|
+
WHERE snapshot_id IN (${placeholders})
|
|
797
|
+
`).run(...snapshotIds);
|
|
798
|
+
}
|
|
799
|
+
export function saveLocalStateRestoreEvent(input) {
|
|
800
|
+
db.prepare(`
|
|
801
|
+
INSERT INTO local_state_restore_events (
|
|
802
|
+
id,
|
|
803
|
+
snapshot_id,
|
|
804
|
+
outcome,
|
|
805
|
+
dry_run,
|
|
806
|
+
scopes_json,
|
|
807
|
+
restored_paths_json,
|
|
808
|
+
skipped_paths_json,
|
|
809
|
+
validation_errors_json,
|
|
810
|
+
rollback_applied,
|
|
811
|
+
detail,
|
|
812
|
+
created_at
|
|
813
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
814
|
+
`).run(input.id, input.snapshotId, input.outcome, input.dryRun ? 1 : 0, JSON.stringify(input.scopes), JSON.stringify(input.restoredPaths), JSON.stringify(input.skippedPaths), JSON.stringify(input.validationErrors), input.rollbackApplied ? 1 : 0, input.detail ?? null, input.createdAt ?? new Date().toISOString());
|
|
815
|
+
}
|
|
816
|
+
export function listLocalStateRestoreEvents(limit = 50) {
|
|
817
|
+
const boundedLimit = Math.max(1, Math.min(500, Math.floor(limit)));
|
|
818
|
+
return db.prepare(`
|
|
819
|
+
SELECT
|
|
820
|
+
id,
|
|
821
|
+
snapshot_id,
|
|
822
|
+
outcome,
|
|
823
|
+
dry_run,
|
|
824
|
+
scopes_json,
|
|
825
|
+
restored_paths_json,
|
|
826
|
+
skipped_paths_json,
|
|
827
|
+
validation_errors_json,
|
|
828
|
+
rollback_applied,
|
|
829
|
+
detail,
|
|
830
|
+
created_at
|
|
831
|
+
FROM local_state_restore_events
|
|
832
|
+
ORDER BY datetime(created_at) DESC
|
|
833
|
+
LIMIT ?
|
|
834
|
+
`).all(boundedLimit);
|
|
835
|
+
}
|
|
836
|
+
export function upsertIncidentRecord(input) {
|
|
837
|
+
const stmt = db.prepare(`
|
|
838
|
+
INSERT INTO incidents (
|
|
839
|
+
id,
|
|
840
|
+
incident_type,
|
|
841
|
+
severity,
|
|
842
|
+
status,
|
|
843
|
+
summary,
|
|
844
|
+
evidence_json,
|
|
845
|
+
remediation_action,
|
|
846
|
+
remediation_attempts,
|
|
847
|
+
cooldown_until,
|
|
848
|
+
escalated,
|
|
849
|
+
recommended_actions_json,
|
|
850
|
+
resolved_at
|
|
851
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
852
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
853
|
+
severity = excluded.severity,
|
|
854
|
+
status = excluded.status,
|
|
855
|
+
summary = excluded.summary,
|
|
856
|
+
evidence_json = excluded.evidence_json,
|
|
857
|
+
remediation_action = excluded.remediation_action,
|
|
858
|
+
remediation_attempts = excluded.remediation_attempts,
|
|
859
|
+
cooldown_until = excluded.cooldown_until,
|
|
860
|
+
escalated = excluded.escalated,
|
|
861
|
+
recommended_actions_json = excluded.recommended_actions_json,
|
|
862
|
+
resolved_at = excluded.resolved_at,
|
|
863
|
+
updated_at = CURRENT_TIMESTAMP
|
|
864
|
+
`);
|
|
865
|
+
stmt.run(input.id, input.incidentType, input.severity, input.status, input.summary, input.evidenceJson, input.remediationAction, input.remediationAttempts, input.cooldownUntil, input.escalated ? 1 : 0, input.recommendedActionsJson, input.resolvedAt ?? null);
|
|
866
|
+
}
|
|
867
|
+
export function appendIncidentTimelineEntry(input) {
|
|
868
|
+
const stmt = db.prepare(`
|
|
869
|
+
INSERT INTO incident_timeline (id, incident_id, incident_type, event_type, detail_json)
|
|
870
|
+
VALUES (?, ?, ?, ?, ?)
|
|
871
|
+
`);
|
|
872
|
+
stmt.run(input.id, input.incidentId, input.incidentType, input.eventType, input.detailJson);
|
|
873
|
+
}
|
|
874
|
+
export function listIncidentRecords(limit = 100, statuses = []) {
|
|
875
|
+
const boundedLimit = Math.max(1, Math.min(500, Math.floor(limit)));
|
|
876
|
+
if (!statuses.length) {
|
|
877
|
+
return db
|
|
878
|
+
.prepare(`
|
|
879
|
+
SELECT *
|
|
880
|
+
FROM incidents
|
|
881
|
+
ORDER BY datetime(updated_at) DESC
|
|
882
|
+
LIMIT ?
|
|
883
|
+
`)
|
|
884
|
+
.all(boundedLimit);
|
|
885
|
+
}
|
|
886
|
+
const placeholders = statuses.map(() => '?').join(', ');
|
|
887
|
+
return db
|
|
888
|
+
.prepare(`
|
|
889
|
+
SELECT *
|
|
890
|
+
FROM incidents
|
|
891
|
+
WHERE status IN (${placeholders})
|
|
892
|
+
ORDER BY datetime(updated_at) DESC
|
|
893
|
+
LIMIT ?
|
|
894
|
+
`)
|
|
895
|
+
.all(...statuses, boundedLimit);
|
|
896
|
+
}
|
|
897
|
+
export function listIncidentTimeline(limit = 200) {
|
|
898
|
+
const boundedLimit = Math.max(1, Math.min(1_000, Math.floor(limit)));
|
|
899
|
+
return db
|
|
900
|
+
.prepare(`
|
|
901
|
+
SELECT *
|
|
902
|
+
FROM incident_timeline
|
|
903
|
+
ORDER BY datetime(created_at) DESC
|
|
904
|
+
LIMIT ?
|
|
905
|
+
`)
|
|
906
|
+
.all(boundedLimit);
|
|
907
|
+
}
|
|
908
|
+
export function getCallbackOutcomeCounts(sinceMinutes) {
|
|
909
|
+
const useWindow = Number.isFinite(sinceMinutes) && (sinceMinutes ?? 0) > 0;
|
|
910
|
+
const rows = useWindow
|
|
911
|
+
? db
|
|
912
|
+
.prepare(`
|
|
913
|
+
SELECT outcome, COUNT(*) as count
|
|
914
|
+
FROM callback_receipts
|
|
915
|
+
WHERE created_at >= datetime('now', ?)
|
|
916
|
+
GROUP BY outcome
|
|
917
|
+
`)
|
|
918
|
+
.all(`-${Math.floor(sinceMinutes ?? 0)} minutes`)
|
|
919
|
+
: db
|
|
920
|
+
.prepare(`
|
|
921
|
+
SELECT outcome, COUNT(*) as count
|
|
922
|
+
FROM callback_receipts
|
|
923
|
+
GROUP BY outcome
|
|
924
|
+
`)
|
|
925
|
+
.all();
|
|
926
|
+
return {
|
|
927
|
+
accepted: rows.find((row) => row.outcome === 'accepted')?.count ?? 0,
|
|
928
|
+
duplicate: rows.find((row) => row.outcome === 'duplicate')?.count ?? 0,
|
|
929
|
+
rejected: rows.find((row) => row.outcome === 'rejected')?.count ?? 0,
|
|
930
|
+
};
|
|
931
|
+
}
|
|
932
|
+
export function recordRuntimeUsageEvent(input) {
|
|
933
|
+
const stmt = db.prepare(`
|
|
934
|
+
INSERT INTO runtime_usage_events (
|
|
935
|
+
id,
|
|
936
|
+
session_id,
|
|
937
|
+
model_id,
|
|
938
|
+
provider_id,
|
|
939
|
+
profile,
|
|
940
|
+
stage,
|
|
941
|
+
request_tokens,
|
|
942
|
+
response_tokens,
|
|
943
|
+
latency_ms,
|
|
944
|
+
status_code,
|
|
945
|
+
error
|
|
946
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
947
|
+
`);
|
|
948
|
+
stmt.run(input.id, input.sessionId, input.modelId, input.providerId, input.profile, input.stage, input.requestTokens, input.responseTokens, input.latencyMs, input.statusCode ?? null, input.error ?? null);
|
|
949
|
+
}
|
|
950
|
+
export function getRuntimeDailyUsageAggregate() {
|
|
951
|
+
return db
|
|
952
|
+
.prepare(`
|
|
953
|
+
SELECT
|
|
954
|
+
COUNT(*) AS request_count,
|
|
955
|
+
COALESCE(SUM(request_tokens), 0) AS request_tokens,
|
|
956
|
+
COALESCE(SUM(response_tokens), 0) AS response_tokens,
|
|
957
|
+
COALESCE(SUM(CASE WHEN stage = 'failure' THEN 1 ELSE 0 END), 0) AS failure_count,
|
|
958
|
+
COALESCE(SUM(CASE WHEN stage = 'skipped' THEN 1 ELSE 0 END), 0) AS skipped_count
|
|
959
|
+
FROM runtime_usage_events
|
|
960
|
+
WHERE date(created_at) = date('now')
|
|
961
|
+
`)
|
|
962
|
+
.get() ?? {
|
|
963
|
+
request_count: 0,
|
|
964
|
+
request_tokens: 0,
|
|
965
|
+
response_tokens: 0,
|
|
966
|
+
failure_count: 0,
|
|
967
|
+
skipped_count: 0,
|
|
968
|
+
};
|
|
969
|
+
}
|
|
970
|
+
export function getRuntimeSessionUsageAggregate(sessionId) {
|
|
971
|
+
return db
|
|
972
|
+
.prepare(`
|
|
973
|
+
SELECT
|
|
974
|
+
COUNT(*) AS request_count,
|
|
975
|
+
COALESCE(SUM(request_tokens), 0) AS request_tokens,
|
|
976
|
+
COALESCE(SUM(response_tokens), 0) AS response_tokens,
|
|
977
|
+
COALESCE(SUM(CASE WHEN stage = 'failure' THEN 1 ELSE 0 END), 0) AS failure_count,
|
|
978
|
+
COALESCE(SUM(CASE WHEN stage = 'skipped' THEN 1 ELSE 0 END), 0) AS skipped_count
|
|
979
|
+
FROM runtime_usage_events
|
|
980
|
+
WHERE session_id = ?
|
|
981
|
+
`)
|
|
982
|
+
.get(sessionId) ?? {
|
|
983
|
+
request_count: 0,
|
|
984
|
+
request_tokens: 0,
|
|
985
|
+
response_tokens: 0,
|
|
986
|
+
failure_count: 0,
|
|
987
|
+
skipped_count: 0,
|
|
988
|
+
};
|
|
989
|
+
}
|
|
990
|
+
export function listRuntimeProviderUsageAggregates() {
|
|
991
|
+
return db
|
|
992
|
+
.prepare(`
|
|
993
|
+
SELECT
|
|
994
|
+
provider_id,
|
|
995
|
+
COUNT(*) AS request_count,
|
|
996
|
+
COALESCE(SUM(request_tokens), 0) AS request_tokens,
|
|
997
|
+
COALESCE(SUM(response_tokens), 0) AS response_tokens,
|
|
998
|
+
COALESCE(SUM(CASE WHEN stage = 'failure' THEN 1 ELSE 0 END), 0) AS failure_count,
|
|
999
|
+
COALESCE(SUM(CASE WHEN stage = 'skipped' THEN 1 ELSE 0 END), 0) AS skipped_count
|
|
1000
|
+
FROM runtime_usage_events
|
|
1001
|
+
WHERE date(created_at) = date('now')
|
|
1002
|
+
GROUP BY provider_id
|
|
1003
|
+
ORDER BY request_count DESC, provider_id ASC
|
|
1004
|
+
`)
|
|
1005
|
+
.all();
|
|
1006
|
+
}
|
|
1007
|
+
export function recordRuntimeBudgetEvent(input) {
|
|
1008
|
+
const stmt = db.prepare(`
|
|
1009
|
+
INSERT INTO runtime_budget_events (
|
|
1010
|
+
id,
|
|
1011
|
+
session_id,
|
|
1012
|
+
severity,
|
|
1013
|
+
profile,
|
|
1014
|
+
action,
|
|
1015
|
+
reason,
|
|
1016
|
+
detail_json
|
|
1017
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
1018
|
+
`);
|
|
1019
|
+
stmt.run(input.id, input.sessionId, input.severity, input.profile, input.action, input.reason, input.detailJson);
|
|
1020
|
+
}
|
|
1021
|
+
export function listRuntimeBudgetEvents(limit = 100) {
|
|
1022
|
+
const boundedLimit = Math.max(1, Math.min(500, Math.floor(limit)));
|
|
1023
|
+
return db
|
|
1024
|
+
.prepare(`
|
|
1025
|
+
SELECT *
|
|
1026
|
+
FROM runtime_budget_events
|
|
1027
|
+
ORDER BY datetime(created_at) DESC
|
|
1028
|
+
LIMIT ?
|
|
1029
|
+
`)
|
|
1030
|
+
.all(boundedLimit);
|
|
1031
|
+
}
|
|
1032
|
+
export function setRuntimeBudgetState(key, value) {
|
|
1033
|
+
const stmt = db.prepare(`
|
|
1034
|
+
INSERT INTO runtime_budget_state (key, value)
|
|
1035
|
+
VALUES (?, ?)
|
|
1036
|
+
ON CONFLICT(key) DO UPDATE SET
|
|
1037
|
+
value = excluded.value,
|
|
1038
|
+
updated_at = CURRENT_TIMESTAMP
|
|
1039
|
+
`);
|
|
1040
|
+
stmt.run(key, value);
|
|
1041
|
+
}
|
|
1042
|
+
export function getRuntimeBudgetState(key) {
|
|
1043
|
+
const row = db
|
|
1044
|
+
.prepare(`
|
|
1045
|
+
SELECT value
|
|
1046
|
+
FROM runtime_budget_state
|
|
1047
|
+
WHERE key = ?
|
|
1048
|
+
LIMIT 1
|
|
1049
|
+
`)
|
|
1050
|
+
.get(key);
|
|
1051
|
+
return row?.value ?? null;
|
|
1052
|
+
}
|
|
1053
|
+
export function clearRuntimeBudgetState(key) {
|
|
1054
|
+
db.prepare('DELETE FROM runtime_budget_state WHERE key = ?').run(key);
|
|
1055
|
+
}
|