prism-mcp-server 2.5.2 → 3.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 +58 -3
- package/dist/config.js +8 -0
- package/dist/dashboard/server.js +60 -2
- package/dist/dashboard/ui.js +322 -1
- package/dist/server.js +26 -3
- package/dist/storage/configStorage.js +73 -0
- package/dist/storage/index.js +8 -5
- package/dist/storage/sqlite.js +237 -20
- package/dist/storage/supabase.js +84 -106
- package/dist/tools/agentRegistryDefinitions.js +104 -0
- package/dist/tools/agentRegistryHandlers.js +114 -0
- package/dist/tools/index.js +5 -0
- package/dist/tools/sessionMemoryDefinitions.js +12 -0
- package/dist/tools/sessionMemoryHandlers.js +7 -4
- package/package.json +7 -2
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { createClient } from "@libsql/client";
|
|
2
|
+
import { resolve } from "path";
|
|
3
|
+
import { homedir } from "os";
|
|
4
|
+
// We use a small, dedicated DB just for configuration settings.
|
|
5
|
+
// This solves the chicken-and-egg problem: we need to know WHICH
|
|
6
|
+
// storage backend to boot *before* we can use that backend.
|
|
7
|
+
//
|
|
8
|
+
// Stored in ~/.prism-mcp/prism-config.db — the same root directory
|
|
9
|
+
// used by sqlite.ts and autoCapture.ts for all Prism files.
|
|
10
|
+
//
|
|
11
|
+
// ⚡ BOOT SETTINGS NOTE:
|
|
12
|
+
// Settings in this store that affect server initialization (e.g.
|
|
13
|
+
// PRISM_STORAGE, PRISM_ENABLE_HIVEMIND) are read only at startup.
|
|
14
|
+
// Changing them at runtime requires a server restart to take effect.
|
|
15
|
+
// Runtime-only settings (e.g. dashboard_theme) take effect immediately.
|
|
16
|
+
const CONFIG_PATH = resolve(homedir(), ".prism-mcp", "prism-config.db");
|
|
17
|
+
let configClient = null;
|
|
18
|
+
let initialized = false;
|
|
19
|
+
function getClient() {
|
|
20
|
+
if (!configClient) {
|
|
21
|
+
configClient = createClient({
|
|
22
|
+
url: `file:${CONFIG_PATH}`,
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
return configClient;
|
|
26
|
+
}
|
|
27
|
+
export async function initConfigStorage() {
|
|
28
|
+
if (initialized)
|
|
29
|
+
return;
|
|
30
|
+
const client = getClient();
|
|
31
|
+
await client.execute(`
|
|
32
|
+
CREATE TABLE IF NOT EXISTS system_settings (
|
|
33
|
+
key TEXT PRIMARY KEY,
|
|
34
|
+
value TEXT NOT NULL,
|
|
35
|
+
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
36
|
+
)
|
|
37
|
+
`);
|
|
38
|
+
initialized = true;
|
|
39
|
+
}
|
|
40
|
+
export async function getSetting(key, defaultValue = "") {
|
|
41
|
+
await initConfigStorage();
|
|
42
|
+
const client = getClient();
|
|
43
|
+
const rs = await client.execute({
|
|
44
|
+
sql: "SELECT value FROM system_settings WHERE key = ?",
|
|
45
|
+
args: [key],
|
|
46
|
+
});
|
|
47
|
+
if (rs.rows.length > 0) {
|
|
48
|
+
return rs.rows[0].value;
|
|
49
|
+
}
|
|
50
|
+
return defaultValue;
|
|
51
|
+
}
|
|
52
|
+
export async function setSetting(key, value) {
|
|
53
|
+
await initConfigStorage();
|
|
54
|
+
const client = getClient();
|
|
55
|
+
await client.execute({
|
|
56
|
+
sql: `
|
|
57
|
+
INSERT INTO system_settings (key, value, updated_at)
|
|
58
|
+
VALUES (?, ?, CURRENT_TIMESTAMP)
|
|
59
|
+
ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = CURRENT_TIMESTAMP
|
|
60
|
+
`,
|
|
61
|
+
args: [key, value],
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
export async function getAllSettings() {
|
|
65
|
+
await initConfigStorage();
|
|
66
|
+
const client = getClient();
|
|
67
|
+
const rs = await client.execute("SELECT key, value FROM system_settings");
|
|
68
|
+
const settings = {};
|
|
69
|
+
for (const row of rs.rows) {
|
|
70
|
+
settings[row.key] = row.value;
|
|
71
|
+
}
|
|
72
|
+
return settings;
|
|
73
|
+
}
|
package/dist/storage/index.js
CHANGED
|
@@ -9,10 +9,12 @@
|
|
|
9
9
|
* const storage = await getStorage();
|
|
10
10
|
* // Pass `storage` to all session memory handlers
|
|
11
11
|
*/
|
|
12
|
-
import { PRISM_STORAGE } from "../config.js";
|
|
12
|
+
import { PRISM_STORAGE as ENV_PRISM_STORAGE } from "../config.js";
|
|
13
13
|
import { debugLog } from "../utils/logger.js";
|
|
14
14
|
import { SupabaseStorage } from "./supabase.js";
|
|
15
|
+
import { getSetting } from "./configStorage.js";
|
|
15
16
|
let storageInstance = null;
|
|
17
|
+
export let activeStorageBackend = "local";
|
|
16
18
|
/**
|
|
17
19
|
* Returns the singleton storage backend.
|
|
18
20
|
*
|
|
@@ -25,16 +27,17 @@ let storageInstance = null;
|
|
|
25
27
|
export async function getStorage() {
|
|
26
28
|
if (storageInstance)
|
|
27
29
|
return storageInstance;
|
|
28
|
-
|
|
29
|
-
|
|
30
|
+
activeStorageBackend = await getSetting("PRISM_STORAGE", ENV_PRISM_STORAGE);
|
|
31
|
+
debugLog(`[Prism Storage] Initializing backend: ${activeStorageBackend}`);
|
|
32
|
+
if (activeStorageBackend === "local") {
|
|
30
33
|
const { SqliteStorage } = await import("./sqlite.js");
|
|
31
34
|
storageInstance = new SqliteStorage();
|
|
32
35
|
}
|
|
33
|
-
else if (
|
|
36
|
+
else if (activeStorageBackend === "supabase") {
|
|
34
37
|
storageInstance = new SupabaseStorage();
|
|
35
38
|
}
|
|
36
39
|
else {
|
|
37
|
-
throw new Error(`Unknown PRISM_STORAGE value: "${
|
|
40
|
+
throw new Error(`Unknown PRISM_STORAGE value: "${activeStorageBackend}". ` +
|
|
38
41
|
`Must be "local" or "supabase".`);
|
|
39
42
|
}
|
|
40
43
|
await storageInstance.initialize();
|
package/dist/storage/sqlite.js
CHANGED
|
@@ -182,6 +182,92 @@ export class SqliteStorage {
|
|
|
182
182
|
// Index for fast WHERE deleted_at IS NULL queries.
|
|
183
183
|
// CREATE INDEX IF NOT EXISTS is safe to run repeatedly (no try/catch needed).
|
|
184
184
|
await this.db.execute(`CREATE INDEX IF NOT EXISTS idx_ledger_deleted ON session_ledger(deleted_at)`);
|
|
185
|
+
// ─── v3.0 Migration: Agent Hivemind ──────────────────────────
|
|
186
|
+
//
|
|
187
|
+
// 1. Add `role` column to session_ledger (simple ALTER TABLE — no UNIQUE issue)
|
|
188
|
+
// 2. Rebuild session_handoffs with new UNIQUE(project, user_id, role)
|
|
189
|
+
// Using 'global' default instead of NULL to avoid SQLite NULL uniqueness trap.
|
|
190
|
+
// 3. Create agent_registry table
|
|
191
|
+
// session_ledger: simple column add
|
|
192
|
+
try {
|
|
193
|
+
await this.db.execute(`ALTER TABLE session_ledger ADD COLUMN role TEXT NOT NULL DEFAULT 'global'`);
|
|
194
|
+
debugLog("[SqliteStorage] v3.0 migration: added role column to session_ledger");
|
|
195
|
+
}
|
|
196
|
+
catch (e) {
|
|
197
|
+
if (!e.message?.includes("duplicate column name"))
|
|
198
|
+
throw e;
|
|
199
|
+
}
|
|
200
|
+
await this.db.execute(`CREATE INDEX IF NOT EXISTS idx_ledger_role ON session_ledger(role)`);
|
|
201
|
+
// session_handoffs: 4-step table rebuild for UNIQUE constraint change
|
|
202
|
+
// Check if we need to do the rebuild by looking for the role column
|
|
203
|
+
try {
|
|
204
|
+
await this.db.execute(`SELECT role FROM session_handoffs LIMIT 1`);
|
|
205
|
+
// Column exists — migration already ran
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
// Column doesn't exist — do the table rebuild
|
|
209
|
+
debugLog("[SqliteStorage] v3.0 migration: rebuilding session_handoffs with role column");
|
|
210
|
+
// Step 1: Create new table with correct constraint
|
|
211
|
+
await this.db.execute(`
|
|
212
|
+
CREATE TABLE session_handoffs_v2 (
|
|
213
|
+
id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),
|
|
214
|
+
project TEXT NOT NULL,
|
|
215
|
+
user_id TEXT NOT NULL DEFAULT 'default',
|
|
216
|
+
role TEXT NOT NULL DEFAULT 'global',
|
|
217
|
+
last_summary TEXT DEFAULT NULL,
|
|
218
|
+
pending_todo TEXT DEFAULT '[]',
|
|
219
|
+
active_decisions TEXT DEFAULT '[]',
|
|
220
|
+
keywords TEXT DEFAULT '[]',
|
|
221
|
+
key_context TEXT DEFAULT NULL,
|
|
222
|
+
active_branch TEXT DEFAULT NULL,
|
|
223
|
+
version INTEGER NOT NULL DEFAULT 1,
|
|
224
|
+
metadata TEXT DEFAULT '{}',
|
|
225
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
226
|
+
updated_at TEXT DEFAULT (datetime('now')),
|
|
227
|
+
UNIQUE(project, user_id, role)
|
|
228
|
+
)
|
|
229
|
+
`);
|
|
230
|
+
// Step 2: Copy data with explicit column names (Pro-Tip 2)
|
|
231
|
+
await this.db.execute(`
|
|
232
|
+
INSERT INTO session_handoffs_v2
|
|
233
|
+
(id, project, user_id, role, last_summary, pending_todo,
|
|
234
|
+
active_decisions, keywords, key_context, active_branch,
|
|
235
|
+
version, metadata, created_at, updated_at)
|
|
236
|
+
SELECT
|
|
237
|
+
id, project, user_id, 'global', last_summary, pending_todo,
|
|
238
|
+
active_decisions, keywords, key_context, active_branch,
|
|
239
|
+
version, metadata, created_at, updated_at
|
|
240
|
+
FROM session_handoffs
|
|
241
|
+
`);
|
|
242
|
+
// Step 3: Drop old and rename
|
|
243
|
+
await this.db.execute(`DROP TABLE session_handoffs`);
|
|
244
|
+
await this.db.execute(`ALTER TABLE session_handoffs_v2 RENAME TO session_handoffs`);
|
|
245
|
+
debugLog("[SqliteStorage] v3.0 migration: session_handoffs rebuilt with UNIQUE(project, user_id, role)");
|
|
246
|
+
}
|
|
247
|
+
// agent_registry: new table for Hivemind coordination
|
|
248
|
+
await this.db.execute(`
|
|
249
|
+
CREATE TABLE IF NOT EXISTS agent_registry (
|
|
250
|
+
id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),
|
|
251
|
+
project TEXT NOT NULL,
|
|
252
|
+
user_id TEXT NOT NULL DEFAULT 'default',
|
|
253
|
+
role TEXT NOT NULL,
|
|
254
|
+
agent_name TEXT DEFAULT NULL,
|
|
255
|
+
status TEXT NOT NULL DEFAULT 'active',
|
|
256
|
+
current_task TEXT DEFAULT NULL,
|
|
257
|
+
last_heartbeat TEXT DEFAULT (datetime('now')),
|
|
258
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
259
|
+
UNIQUE(project, user_id, role)
|
|
260
|
+
)
|
|
261
|
+
`);
|
|
262
|
+
await this.db.execute(`CREATE INDEX IF NOT EXISTS idx_registry_project ON agent_registry(project, user_id)`);
|
|
263
|
+
// system_settings: key-value store for dashboard runtime settings (v3.0)
|
|
264
|
+
await this.db.execute(`
|
|
265
|
+
CREATE TABLE IF NOT EXISTS system_settings (
|
|
266
|
+
key TEXT PRIMARY KEY,
|
|
267
|
+
value TEXT NOT NULL,
|
|
268
|
+
updated_at TEXT DEFAULT (datetime('now'))
|
|
269
|
+
)
|
|
270
|
+
`);
|
|
185
271
|
}
|
|
186
272
|
// ─── PostgREST Filter Parser ───────────────────────────────
|
|
187
273
|
//
|
|
@@ -297,14 +383,15 @@ export class SqliteStorage {
|
|
|
297
383
|
const now = new Date().toISOString();
|
|
298
384
|
await this.db.execute({
|
|
299
385
|
sql: `INSERT INTO session_ledger
|
|
300
|
-
(id, project, conversation_id, user_id, summary, todos, files_changed,
|
|
386
|
+
(id, project, conversation_id, user_id, role, summary, todos, files_changed,
|
|
301
387
|
decisions, keywords, is_rollup, rollup_count, title, agent_name, created_at, session_date)
|
|
302
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
388
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
303
389
|
args: [
|
|
304
390
|
id,
|
|
305
391
|
entry.project,
|
|
306
392
|
entry.conversation_id,
|
|
307
393
|
entry.user_id,
|
|
394
|
+
entry.role || "global", // v3.0: default to 'global'
|
|
308
395
|
entry.summary,
|
|
309
396
|
JSON.stringify(entry.todos || []),
|
|
310
397
|
JSON.stringify(entry.files_changed || []),
|
|
@@ -404,19 +491,21 @@ export class SqliteStorage {
|
|
|
404
491
|
}
|
|
405
492
|
// ─── Handoff Operations (OCC) ──────────────────────────────
|
|
406
493
|
async saveHandoff(handoff, expectedVersion) {
|
|
494
|
+
const role = handoff.role || "global"; // v3.0: default to 'global'
|
|
407
495
|
// CASE 1: No expectedVersion → UPSERT (create or force-update)
|
|
408
496
|
if (expectedVersion === null || expectedVersion === undefined) {
|
|
409
497
|
// Try INSERT first
|
|
410
498
|
try {
|
|
411
499
|
await this.db.execute({
|
|
412
500
|
sql: `INSERT INTO session_handoffs
|
|
413
|
-
(id, project, user_id, last_summary, pending_todo, active_decisions,
|
|
501
|
+
(id, project, user_id, role, last_summary, pending_todo, active_decisions,
|
|
414
502
|
keywords, key_context, active_branch, version, metadata)
|
|
415
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?)`,
|
|
503
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?)`,
|
|
416
504
|
args: [
|
|
417
505
|
randomUUID(),
|
|
418
506
|
handoff.project,
|
|
419
507
|
handoff.user_id,
|
|
508
|
+
role,
|
|
420
509
|
handoff.last_summary ?? null,
|
|
421
510
|
JSON.stringify(handoff.pending_todo ?? []),
|
|
422
511
|
JSON.stringify(handoff.active_decisions ?? []),
|
|
@@ -437,7 +526,7 @@ export class SqliteStorage {
|
|
|
437
526
|
SET last_summary = ?, pending_todo = ?, active_decisions = ?,
|
|
438
527
|
keywords = ?, key_context = ?, active_branch = ?,
|
|
439
528
|
metadata = ?, version = version + 1, updated_at = datetime('now')
|
|
440
|
-
WHERE project = ? AND user_id = ?
|
|
529
|
+
WHERE project = ? AND user_id = ? AND role = ?
|
|
441
530
|
RETURNING version`,
|
|
442
531
|
args: [
|
|
443
532
|
handoff.last_summary ?? null,
|
|
@@ -449,6 +538,7 @@ export class SqliteStorage {
|
|
|
449
538
|
JSON.stringify(handoff.metadata ?? {}),
|
|
450
539
|
handoff.project,
|
|
451
540
|
handoff.user_id,
|
|
541
|
+
role,
|
|
452
542
|
],
|
|
453
543
|
});
|
|
454
544
|
const newVersion = result.rows[0]?.version;
|
|
@@ -463,7 +553,7 @@ export class SqliteStorage {
|
|
|
463
553
|
SET last_summary = ?, pending_todo = ?, active_decisions = ?,
|
|
464
554
|
keywords = ?, key_context = ?, active_branch = ?,
|
|
465
555
|
metadata = ?, version = version + 1, updated_at = datetime('now')
|
|
466
|
-
WHERE project = ? AND user_id = ? AND version = ?
|
|
556
|
+
WHERE project = ? AND user_id = ? AND role = ? AND version = ?
|
|
467
557
|
RETURNING version`,
|
|
468
558
|
args: [
|
|
469
559
|
handoff.last_summary ?? null,
|
|
@@ -475,14 +565,15 @@ export class SqliteStorage {
|
|
|
475
565
|
JSON.stringify(handoff.metadata ?? {}),
|
|
476
566
|
handoff.project,
|
|
477
567
|
handoff.user_id,
|
|
568
|
+
role,
|
|
478
569
|
expectedVersion,
|
|
479
570
|
],
|
|
480
571
|
});
|
|
481
572
|
if (result.rows.length === 0) {
|
|
482
573
|
// Version mismatch — detect the actual current version
|
|
483
574
|
const check = await this.db.execute({
|
|
484
|
-
sql: "SELECT version FROM session_handoffs WHERE project = ? AND user_id = ?",
|
|
485
|
-
args: [handoff.project, handoff.user_id],
|
|
575
|
+
sql: "SELECT version FROM session_handoffs WHERE project = ? AND user_id = ? AND role = ?",
|
|
576
|
+
args: [handoff.project, handoff.user_id, role],
|
|
486
577
|
});
|
|
487
578
|
if (check.rows.length > 0) {
|
|
488
579
|
return {
|
|
@@ -505,11 +596,13 @@ export class SqliteStorage {
|
|
|
505
596
|
});
|
|
506
597
|
}
|
|
507
598
|
// ─── Load Context (Progressive) ────────────────────────────
|
|
508
|
-
async loadContext(project, level, userId
|
|
509
|
-
|
|
599
|
+
async loadContext(project, level, userId, role // v3.0: optional role filter
|
|
600
|
+
) {
|
|
601
|
+
const effectiveRole = role || "global";
|
|
602
|
+
// Fetch handoff state (role-scoped)
|
|
510
603
|
const handoffResult = await this.db.execute({
|
|
511
|
-
sql: "SELECT * FROM session_handoffs WHERE project = ? AND user_id = ?",
|
|
512
|
-
args: [project, userId],
|
|
604
|
+
sql: "SELECT * FROM session_handoffs WHERE project = ? AND user_id = ? AND role = ?",
|
|
605
|
+
args: [project, userId, effectiveRole],
|
|
513
606
|
});
|
|
514
607
|
if (handoffResult.rows.length === 0)
|
|
515
608
|
return null;
|
|
@@ -517,6 +610,7 @@ export class SqliteStorage {
|
|
|
517
610
|
// Base context (always returned)
|
|
518
611
|
const context = {
|
|
519
612
|
project: handoff.project,
|
|
613
|
+
role: handoff.role, // v3.0: include role in response
|
|
520
614
|
keywords: this.parseJsonColumn(handoff.keywords),
|
|
521
615
|
pending_todo: this.parseJsonColumn(handoff.pending_todo),
|
|
522
616
|
version: handoff.version,
|
|
@@ -531,32 +625,56 @@ export class SqliteStorage {
|
|
|
531
625
|
context.active_branch = handoff.active_branch;
|
|
532
626
|
context.key_context = handoff.key_context;
|
|
533
627
|
if (level === "standard") {
|
|
534
|
-
// Add recent ledger entries
|
|
535
|
-
// Phase 2: AND deleted_at IS NULL — exclude soft-deleted entries
|
|
628
|
+
// Add recent ledger entries (role-scoped)
|
|
536
629
|
const recentLedger = await this.db.execute({
|
|
537
630
|
sql: `SELECT summary, decisions, session_date, created_at
|
|
538
631
|
FROM session_ledger
|
|
539
|
-
WHERE project = ? AND user_id = ? AND
|
|
632
|
+
WHERE project = ? AND user_id = ? AND role = ?
|
|
633
|
+
AND archived_at IS NULL AND deleted_at IS NULL
|
|
540
634
|
ORDER BY created_at DESC
|
|
541
635
|
LIMIT 5`,
|
|
542
|
-
args: [project, userId],
|
|
636
|
+
args: [project, userId, effectiveRole],
|
|
543
637
|
});
|
|
544
638
|
context.recent_sessions = recentLedger.rows.map(r => ({
|
|
545
639
|
summary: r.summary,
|
|
546
640
|
decisions: this.parseJsonColumn(r.decisions),
|
|
547
641
|
session_date: r.session_date || r.created_at,
|
|
548
642
|
}));
|
|
643
|
+
// v3.0: Team Roster injection — show active teammates
|
|
644
|
+
if (role && role !== "global") {
|
|
645
|
+
try {
|
|
646
|
+
const teamResult = await this.db.execute({
|
|
647
|
+
sql: `SELECT role, status, current_task, last_heartbeat
|
|
648
|
+
FROM agent_registry
|
|
649
|
+
WHERE project = ? AND user_id = ? AND role != ?
|
|
650
|
+
AND last_heartbeat > datetime('now', '-30 minutes')
|
|
651
|
+
ORDER BY last_heartbeat DESC`,
|
|
652
|
+
args: [project, userId, role],
|
|
653
|
+
});
|
|
654
|
+
if (teamResult.rows.length > 0) {
|
|
655
|
+
context.active_team = teamResult.rows.map(r => ({
|
|
656
|
+
role: r.role,
|
|
657
|
+
status: r.status,
|
|
658
|
+
current_task: r.current_task,
|
|
659
|
+
last_heartbeat: r.last_heartbeat,
|
|
660
|
+
}));
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
catch {
|
|
664
|
+
// agent_registry may not exist yet — graceful degradation
|
|
665
|
+
}
|
|
666
|
+
}
|
|
549
667
|
return context;
|
|
550
668
|
}
|
|
551
|
-
// Deep: add full session history
|
|
552
|
-
// Phase 2: AND deleted_at IS NULL — exclude soft-deleted entries
|
|
669
|
+
// Deep: add full session history (role-scoped)
|
|
553
670
|
const fullLedger = await this.db.execute({
|
|
554
671
|
sql: `SELECT summary, decisions, files_changed, todos, session_date, created_at
|
|
555
672
|
FROM session_ledger
|
|
556
|
-
WHERE project = ? AND user_id = ? AND
|
|
673
|
+
WHERE project = ? AND user_id = ? AND role = ?
|
|
674
|
+
AND archived_at IS NULL AND deleted_at IS NULL
|
|
557
675
|
ORDER BY created_at DESC
|
|
558
676
|
LIMIT 50`,
|
|
559
|
-
args: [project, userId],
|
|
677
|
+
args: [project, userId, effectiveRole],
|
|
560
678
|
});
|
|
561
679
|
context.session_history = fullLedger.rows.map(r => ({
|
|
562
680
|
summary: r.summary,
|
|
@@ -913,4 +1031,103 @@ export class SqliteStorage {
|
|
|
913
1031
|
totalRollups, // grand total of rollup entries
|
|
914
1032
|
};
|
|
915
1033
|
}
|
|
1034
|
+
// ─── v3.0: Agent Registry (Hivemind) ───────────────────────
|
|
1035
|
+
async registerAgent(entry) {
|
|
1036
|
+
const id = randomUUID();
|
|
1037
|
+
const role = entry.role;
|
|
1038
|
+
const status = entry.status || "active";
|
|
1039
|
+
try {
|
|
1040
|
+
// Try INSERT first
|
|
1041
|
+
await this.db.execute({
|
|
1042
|
+
sql: `INSERT INTO agent_registry
|
|
1043
|
+
(id, project, user_id, role, agent_name, status, current_task)
|
|
1044
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
1045
|
+
args: [
|
|
1046
|
+
id,
|
|
1047
|
+
entry.project,
|
|
1048
|
+
entry.user_id,
|
|
1049
|
+
role,
|
|
1050
|
+
entry.agent_name ?? null,
|
|
1051
|
+
status,
|
|
1052
|
+
entry.current_task ?? null,
|
|
1053
|
+
],
|
|
1054
|
+
});
|
|
1055
|
+
debugLog(`[SqliteStorage] Agent registered: ${entry.project}/${role}`);
|
|
1056
|
+
return { ...entry, id, status };
|
|
1057
|
+
}
|
|
1058
|
+
catch (err) {
|
|
1059
|
+
// UNIQUE constraint → update existing
|
|
1060
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1061
|
+
if (msg.includes("UNIQUE") || msg.includes("constraint")) {
|
|
1062
|
+
await this.db.execute({
|
|
1063
|
+
sql: `UPDATE agent_registry
|
|
1064
|
+
SET agent_name = ?, status = ?, current_task = ?,
|
|
1065
|
+
last_heartbeat = datetime('now')
|
|
1066
|
+
WHERE project = ? AND user_id = ? AND role = ?`,
|
|
1067
|
+
args: [
|
|
1068
|
+
entry.agent_name ?? null,
|
|
1069
|
+
status,
|
|
1070
|
+
entry.current_task ?? null,
|
|
1071
|
+
entry.project,
|
|
1072
|
+
entry.user_id,
|
|
1073
|
+
role,
|
|
1074
|
+
],
|
|
1075
|
+
});
|
|
1076
|
+
debugLog(`[SqliteStorage] Agent re-registered: ${entry.project}/${role}`);
|
|
1077
|
+
return { ...entry, status };
|
|
1078
|
+
}
|
|
1079
|
+
throw err;
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
async heartbeatAgent(project, userId, role, currentTask) {
|
|
1083
|
+
const setClauses = ["last_heartbeat = datetime('now')"];
|
|
1084
|
+
const args = [];
|
|
1085
|
+
if (currentTask !== undefined) {
|
|
1086
|
+
setClauses.push("current_task = ?");
|
|
1087
|
+
args.push(currentTask);
|
|
1088
|
+
}
|
|
1089
|
+
args.push(project, userId, role);
|
|
1090
|
+
await this.db.execute({
|
|
1091
|
+
sql: `UPDATE agent_registry
|
|
1092
|
+
SET ${setClauses.join(", ")}
|
|
1093
|
+
WHERE project = ? AND user_id = ? AND role = ?`,
|
|
1094
|
+
args,
|
|
1095
|
+
});
|
|
1096
|
+
}
|
|
1097
|
+
async listTeam(project, userId, staleMinutes = 30) {
|
|
1098
|
+
// Auto-prune stale agents first
|
|
1099
|
+
await this.db.execute({
|
|
1100
|
+
sql: `DELETE FROM agent_registry
|
|
1101
|
+
WHERE project = ? AND user_id = ?
|
|
1102
|
+
AND last_heartbeat < datetime('now', '-' || ? || ' minutes')`,
|
|
1103
|
+
args: [project, userId, staleMinutes],
|
|
1104
|
+
});
|
|
1105
|
+
// Fetch remaining active agents
|
|
1106
|
+
const result = await this.db.execute({
|
|
1107
|
+
sql: `SELECT id, project, user_id, role, agent_name, status,
|
|
1108
|
+
current_task, last_heartbeat, created_at
|
|
1109
|
+
FROM agent_registry
|
|
1110
|
+
WHERE project = ? AND user_id = ?
|
|
1111
|
+
ORDER BY last_heartbeat DESC`,
|
|
1112
|
+
args: [project, userId],
|
|
1113
|
+
});
|
|
1114
|
+
return result.rows.map(r => ({
|
|
1115
|
+
id: r.id,
|
|
1116
|
+
project: r.project,
|
|
1117
|
+
user_id: r.user_id,
|
|
1118
|
+
role: r.role,
|
|
1119
|
+
agent_name: r.agent_name,
|
|
1120
|
+
status: r.status,
|
|
1121
|
+
current_task: r.current_task,
|
|
1122
|
+
last_heartbeat: r.last_heartbeat,
|
|
1123
|
+
created_at: r.created_at,
|
|
1124
|
+
}));
|
|
1125
|
+
}
|
|
1126
|
+
async deregisterAgent(project, userId, role) {
|
|
1127
|
+
await this.db.execute({
|
|
1128
|
+
sql: "DELETE FROM agent_registry WHERE project = ? AND user_id = ? AND role = ?",
|
|
1129
|
+
args: [project, userId, role],
|
|
1130
|
+
});
|
|
1131
|
+
debugLog(`[SqliteStorage] Agent deregistered: ${project}/${role}`);
|
|
1132
|
+
}
|
|
916
1133
|
}
|