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.
Files changed (132) hide show
  1. package/README.md +66 -0
  2. package/bin/npm-twinclaw.js +17 -0
  3. package/bin/run-twinbot-cli.js +36 -0
  4. package/bin/twinbot.js +4 -0
  5. package/bin/twinclaw.js +4 -0
  6. package/dist/api/handlers/browser.js +160 -0
  7. package/dist/api/handlers/callback.js +80 -0
  8. package/dist/api/handlers/config-validate.js +19 -0
  9. package/dist/api/handlers/health.js +117 -0
  10. package/dist/api/handlers/local-state-backup.js +118 -0
  11. package/dist/api/handlers/persona-state.js +59 -0
  12. package/dist/api/handlers/skill-packages.js +94 -0
  13. package/dist/api/router.js +278 -0
  14. package/dist/api/runtime-event-producer.js +99 -0
  15. package/dist/api/shared.js +82 -0
  16. package/dist/api/websocket-hub.js +305 -0
  17. package/dist/config/config-loader.js +2 -0
  18. package/dist/config/env-schema.js +202 -0
  19. package/dist/config/env-validator.js +223 -0
  20. package/dist/config/identity-bootstrap.js +115 -0
  21. package/dist/config/json-config.js +344 -0
  22. package/dist/config/workspace.js +186 -0
  23. package/dist/core/channels-cli.js +77 -0
  24. package/dist/core/cli.js +119 -0
  25. package/dist/core/context-assembly.js +33 -0
  26. package/dist/core/doctor.js +365 -0
  27. package/dist/core/gateway-cli.js +323 -0
  28. package/dist/core/gateway.js +416 -0
  29. package/dist/core/heartbeat.js +54 -0
  30. package/dist/core/install-cli.js +320 -0
  31. package/dist/core/lane-executor.js +134 -0
  32. package/dist/core/logs-cli.js +70 -0
  33. package/dist/core/onboarding.js +760 -0
  34. package/dist/core/pairing-cli.js +78 -0
  35. package/dist/core/secret-vault-cli.js +204 -0
  36. package/dist/core/types.js +1 -0
  37. package/dist/index.js +404 -0
  38. package/dist/interfaces/dispatcher.js +214 -0
  39. package/dist/interfaces/telegram_handler.js +82 -0
  40. package/dist/interfaces/tui-dashboard.js +53 -0
  41. package/dist/interfaces/whatsapp_handler.js +94 -0
  42. package/dist/release/cli.js +97 -0
  43. package/dist/release/mvp-gate-cli.js +118 -0
  44. package/dist/release/twinbot-config-schema.js +162 -0
  45. package/dist/release/twinclaw-config-schema.js +162 -0
  46. package/dist/services/block-chunker.js +174 -0
  47. package/dist/services/browser-service.js +334 -0
  48. package/dist/services/context-lifecycle.js +314 -0
  49. package/dist/services/db.js +1055 -0
  50. package/dist/services/delivery-tracker.js +110 -0
  51. package/dist/services/dm-pairing.js +245 -0
  52. package/dist/services/embedding-service.js +125 -0
  53. package/dist/services/file-watcher.js +125 -0
  54. package/dist/services/inbound-debounce.js +92 -0
  55. package/dist/services/incident-manager.js +516 -0
  56. package/dist/services/job-scheduler.js +176 -0
  57. package/dist/services/local-state-backup.js +682 -0
  58. package/dist/services/mcp-client-adapter.js +291 -0
  59. package/dist/services/mcp-server-manager.js +143 -0
  60. package/dist/services/model-router.js +927 -0
  61. package/dist/services/mvp-gate.js +845 -0
  62. package/dist/services/orchestration-service.js +422 -0
  63. package/dist/services/persona-state.js +256 -0
  64. package/dist/services/policy-engine.js +92 -0
  65. package/dist/services/proactive-notifier.js +94 -0
  66. package/dist/services/queue-service.js +146 -0
  67. package/dist/services/release-pipeline.js +652 -0
  68. package/dist/services/runtime-budget-governor.js +415 -0
  69. package/dist/services/secret-vault.js +704 -0
  70. package/dist/services/semantic-memory.js +249 -0
  71. package/dist/services/skill-package-manager.js +806 -0
  72. package/dist/services/skill-registry.js +122 -0
  73. package/dist/services/streaming-output.js +75 -0
  74. package/dist/services/stt-service.js +39 -0
  75. package/dist/services/tts-service.js +44 -0
  76. package/dist/skills/builtin.js +250 -0
  77. package/dist/skills/shell.js +87 -0
  78. package/dist/skills/types.js +1 -0
  79. package/dist/types/api.js +1 -0
  80. package/dist/types/context-budget.js +1 -0
  81. package/dist/types/doctor.js +1 -0
  82. package/dist/types/file-watcher.js +1 -0
  83. package/dist/types/incident.js +1 -0
  84. package/dist/types/local-state-backup.js +1 -0
  85. package/dist/types/mcp.js +1 -0
  86. package/dist/types/messaging.js +1 -0
  87. package/dist/types/model-routing.js +1 -0
  88. package/dist/types/mvp-gate.js +2 -0
  89. package/dist/types/orchestration.js +1 -0
  90. package/dist/types/persona-state.js +22 -0
  91. package/dist/types/policy.js +1 -0
  92. package/dist/types/reasoning-graph.js +1 -0
  93. package/dist/types/release.js +1 -0
  94. package/dist/types/reliability.js +1 -0
  95. package/dist/types/runtime-budget.js +1 -0
  96. package/dist/types/scheduler.js +1 -0
  97. package/dist/types/secret-vault.js +1 -0
  98. package/dist/types/skill-packages.js +1 -0
  99. package/dist/types/websocket.js +14 -0
  100. package/dist/utils/logger.js +57 -0
  101. package/dist/utils/retry.js +61 -0
  102. package/dist/utils/secret-scan.js +208 -0
  103. package/mcp-servers.json +179 -0
  104. package/package.json +81 -0
  105. package/skill-packages.json +92 -0
  106. package/skill-packages.lock.json +5 -0
  107. package/src/skills/builtin.ts +275 -0
  108. package/src/skills/shell.ts +118 -0
  109. package/src/skills/types.ts +30 -0
  110. package/src/types/api.ts +252 -0
  111. package/src/types/blessed-contrib.d.ts +4 -0
  112. package/src/types/context-budget.ts +76 -0
  113. package/src/types/doctor.ts +29 -0
  114. package/src/types/file-watcher.ts +26 -0
  115. package/src/types/incident.ts +57 -0
  116. package/src/types/local-state-backup.ts +121 -0
  117. package/src/types/mcp.ts +106 -0
  118. package/src/types/messaging.ts +35 -0
  119. package/src/types/model-routing.ts +61 -0
  120. package/src/types/mvp-gate.ts +99 -0
  121. package/src/types/orchestration.ts +65 -0
  122. package/src/types/persona-state.ts +61 -0
  123. package/src/types/policy.ts +27 -0
  124. package/src/types/reasoning-graph.ts +58 -0
  125. package/src/types/release.ts +115 -0
  126. package/src/types/reliability.ts +43 -0
  127. package/src/types/runtime-budget.ts +85 -0
  128. package/src/types/scheduler.ts +47 -0
  129. package/src/types/secret-vault.ts +62 -0
  130. package/src/types/skill-packages.ts +81 -0
  131. package/src/types/sqlite-vec.d.ts +5 -0
  132. 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
+ }