@grec0/memory-bank-mcp 0.1.21 → 0.1.23
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/dist/common/agentBoard.js +188 -291
- package/dist/common/agentBoardSqlite.js +486 -0
- package/dist/common/database.js +199 -0
- package/dist/common/projectKnowledgeService.js +23 -10
- package/dist/common/sessionLogger.js +171 -25
- package/dist/common/sessionState.js +24 -0
- package/dist/common/version.js +1 -1
- package/dist/index.js +6 -6
- package/dist/tools/generateProjectDocs.js +32 -5
- package/dist/tools/getProjectDocs.js +25 -8
- package/dist/tools/indexCode.js +25 -0
- package/dist/tools/manageAgents.js +30 -17
- package/dist/tools/readFile.js +24 -0
- package/dist/tools/searchMemory.js +6 -4
- package/dist/tools/writeFile.js +37 -0
- package/package.json +5 -3
|
@@ -0,0 +1,486 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview SQLite-based Agent Board Implementation
|
|
3
|
+
* Replaces Markdown-based agentBoard with proper database operations.
|
|
4
|
+
* Provides project-centric task management and multi-agent coordination.
|
|
5
|
+
*/
|
|
6
|
+
import { databaseManager } from './database.js';
|
|
7
|
+
import * as crypto from 'crypto';
|
|
8
|
+
// ============================================================================
|
|
9
|
+
// Agent Board SQLite Implementation
|
|
10
|
+
// ============================================================================
|
|
11
|
+
export class AgentBoardSqlite {
|
|
12
|
+
projectId;
|
|
13
|
+
constructor(projectId) {
|
|
14
|
+
this.projectId = projectId;
|
|
15
|
+
}
|
|
16
|
+
// ========================================================================
|
|
17
|
+
// Agent Management
|
|
18
|
+
// ========================================================================
|
|
19
|
+
/**
|
|
20
|
+
* Register a new agent for this project.
|
|
21
|
+
* Automatically deactivates any previous active agent.
|
|
22
|
+
* The MCP generates the hash suffix - client only provides base ID.
|
|
23
|
+
*/
|
|
24
|
+
registerAgent(baseAgentId, sessionId) {
|
|
25
|
+
const db = databaseManager.getConnection();
|
|
26
|
+
// Generate unique agent ID with hash suffix
|
|
27
|
+
const suffix = crypto.randomUUID().slice(0, 8);
|
|
28
|
+
const fullAgentId = `${baseAgentId}-${suffix}`;
|
|
29
|
+
// Generate session ID if not provided
|
|
30
|
+
const effectiveSessionId = sessionId || crypto.randomUUID();
|
|
31
|
+
const now = new Date().toISOString();
|
|
32
|
+
return databaseManager.transaction(() => {
|
|
33
|
+
// Deactivate any currently active agents for this project
|
|
34
|
+
db.prepare(`
|
|
35
|
+
UPDATE agents
|
|
36
|
+
SET status = 'INACTIVE', last_heartbeat = ?
|
|
37
|
+
WHERE project_id = ? AND status = 'ACTIVE'
|
|
38
|
+
`).run(now, this.projectId);
|
|
39
|
+
// Insert new agent as ACTIVE
|
|
40
|
+
db.prepare(`
|
|
41
|
+
INSERT INTO agents (id, project_id, session_id, status, focus, last_heartbeat)
|
|
42
|
+
VALUES (?, ?, ?, 'ACTIVE', '-', ?)
|
|
43
|
+
`).run(fullAgentId, this.projectId, effectiveSessionId, now);
|
|
44
|
+
// Log the registration
|
|
45
|
+
this.logMessage(fullAgentId, `Agent registered and activated`);
|
|
46
|
+
return { agentId: fullAgentId, sessionId: effectiveSessionId };
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Get the currently active agent for this project
|
|
51
|
+
*/
|
|
52
|
+
getActiveAgent() {
|
|
53
|
+
const db = databaseManager.getConnection();
|
|
54
|
+
const row = db.prepare(`
|
|
55
|
+
SELECT id, project_id, session_id, status, focus, last_heartbeat, created_at
|
|
56
|
+
FROM agents
|
|
57
|
+
WHERE project_id = ? AND status = 'ACTIVE'
|
|
58
|
+
ORDER BY last_heartbeat DESC
|
|
59
|
+
LIMIT 1
|
|
60
|
+
`).get(this.projectId);
|
|
61
|
+
if (!row)
|
|
62
|
+
return null;
|
|
63
|
+
return {
|
|
64
|
+
id: row.id,
|
|
65
|
+
projectId: row.project_id,
|
|
66
|
+
sessionId: row.session_id,
|
|
67
|
+
status: row.status,
|
|
68
|
+
focus: row.focus,
|
|
69
|
+
lastHeartbeat: row.last_heartbeat,
|
|
70
|
+
createdAt: row.created_at
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Get session ID for a specific agent
|
|
75
|
+
*/
|
|
76
|
+
getSessionId(agentId) {
|
|
77
|
+
const db = databaseManager.getConnection();
|
|
78
|
+
const row = db.prepare(`
|
|
79
|
+
SELECT session_id FROM agents
|
|
80
|
+
WHERE id = ? AND project_id = ?
|
|
81
|
+
`).get(agentId, this.projectId);
|
|
82
|
+
return row?.session_id || null;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Resolve an agent ID - find the active agent matching a base ID prefix
|
|
86
|
+
*/
|
|
87
|
+
resolveActiveAgentId(baseId) {
|
|
88
|
+
const db = databaseManager.getConnection();
|
|
89
|
+
// First try exact match
|
|
90
|
+
const exact = db.prepare(`
|
|
91
|
+
SELECT id FROM agents
|
|
92
|
+
WHERE id = ? AND project_id = ? AND status = 'ACTIVE'
|
|
93
|
+
`).get(baseId, this.projectId);
|
|
94
|
+
if (exact)
|
|
95
|
+
return exact.id;
|
|
96
|
+
// Try prefix match - find most recent active agent starting with baseId
|
|
97
|
+
const prefixMatch = db.prepare(`
|
|
98
|
+
SELECT id FROM agents
|
|
99
|
+
WHERE project_id = ? AND status = 'ACTIVE' AND id LIKE ?
|
|
100
|
+
ORDER BY last_heartbeat DESC
|
|
101
|
+
LIMIT 1
|
|
102
|
+
`).get(this.projectId, `${baseId}-%`);
|
|
103
|
+
return prefixMatch?.id || baseId;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Update agent status and focus
|
|
107
|
+
*/
|
|
108
|
+
updateStatus(agentId, status, focus) {
|
|
109
|
+
const db = databaseManager.getConnection();
|
|
110
|
+
const now = new Date().toISOString();
|
|
111
|
+
// Resolve to actual agent ID if base ID provided
|
|
112
|
+
const resolvedId = this.resolveActiveAgentId(agentId);
|
|
113
|
+
db.prepare(`
|
|
114
|
+
UPDATE agents
|
|
115
|
+
SET status = ?, focus = ?, last_heartbeat = ?
|
|
116
|
+
WHERE id = ? AND project_id = ?
|
|
117
|
+
`).run(status, focus, now, resolvedId, this.projectId);
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Update heartbeat for an agent
|
|
121
|
+
*/
|
|
122
|
+
heartbeat(agentId) {
|
|
123
|
+
const db = databaseManager.getConnection();
|
|
124
|
+
const now = new Date().toISOString();
|
|
125
|
+
db.prepare(`
|
|
126
|
+
UPDATE agents
|
|
127
|
+
SET last_heartbeat = ?
|
|
128
|
+
WHERE id = ? AND project_id = ?
|
|
129
|
+
`).run(now, agentId, this.projectId);
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Get all agents for this project (for session history view)
|
|
133
|
+
*/
|
|
134
|
+
getAllAgents() {
|
|
135
|
+
const db = databaseManager.getConnection();
|
|
136
|
+
const rows = db.prepare(`
|
|
137
|
+
SELECT id, project_id, session_id, status, focus, last_heartbeat, created_at
|
|
138
|
+
FROM agents
|
|
139
|
+
WHERE project_id = ?
|
|
140
|
+
ORDER BY created_at DESC
|
|
141
|
+
`).all(this.projectId);
|
|
142
|
+
return rows.map(row => ({
|
|
143
|
+
id: row.id,
|
|
144
|
+
projectId: row.project_id,
|
|
145
|
+
sessionId: row.session_id,
|
|
146
|
+
status: row.status,
|
|
147
|
+
focus: row.focus,
|
|
148
|
+
lastHeartbeat: row.last_heartbeat,
|
|
149
|
+
createdAt: row.created_at
|
|
150
|
+
}));
|
|
151
|
+
}
|
|
152
|
+
// ========================================================================
|
|
153
|
+
// Task Management (Project-centric)
|
|
154
|
+
// ========================================================================
|
|
155
|
+
/**
|
|
156
|
+
* Create a task for this project
|
|
157
|
+
*/
|
|
158
|
+
createTask(title, description, fromAgent) {
|
|
159
|
+
const db = databaseManager.getConnection();
|
|
160
|
+
const taskId = `TASK-${Date.now().toString().slice(-6)}`;
|
|
161
|
+
const now = new Date().toISOString();
|
|
162
|
+
db.prepare(`
|
|
163
|
+
INSERT INTO tasks (id, project_id, title, description, from_agent, status, created_at)
|
|
164
|
+
VALUES (?, ?, ?, ?, ?, 'PENDING', ?)
|
|
165
|
+
`).run(taskId, this.projectId, title, description || null, fromAgent || null, now);
|
|
166
|
+
this.logMessage(fromAgent || 'SYSTEM', `Created task ${taskId}: ${title}`);
|
|
167
|
+
return taskId;
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Create an external task (cross-project delegation)
|
|
171
|
+
*/
|
|
172
|
+
createExternalTask(title, fromProjectId, context) {
|
|
173
|
+
const db = databaseManager.getConnection();
|
|
174
|
+
const taskId = `EXT-${Date.now().toString().slice(-6)}`;
|
|
175
|
+
const now = new Date().toISOString();
|
|
176
|
+
db.prepare(`
|
|
177
|
+
INSERT INTO tasks (id, project_id, title, description, from_project, status, created_at)
|
|
178
|
+
VALUES (?, ?, ?, ?, ?, 'PENDING', ?)
|
|
179
|
+
`).run(taskId, this.projectId, title, context || null, fromProjectId, now);
|
|
180
|
+
this.logMessage('SYSTEM', `External task ${taskId} from ${fromProjectId}: ${title}`);
|
|
181
|
+
return taskId;
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Get pending tasks for this project
|
|
185
|
+
*/
|
|
186
|
+
getPendingTasks() {
|
|
187
|
+
const db = databaseManager.getConnection();
|
|
188
|
+
const rows = db.prepare(`
|
|
189
|
+
SELECT * FROM tasks
|
|
190
|
+
WHERE project_id = ? AND status IN ('PENDING', 'IN_PROGRESS')
|
|
191
|
+
ORDER BY created_at ASC
|
|
192
|
+
`).all(this.projectId);
|
|
193
|
+
return rows.map(this.mapTaskRow);
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Claim a task (agent takes ownership)
|
|
197
|
+
*/
|
|
198
|
+
claimTask(taskId, agentId) {
|
|
199
|
+
const db = databaseManager.getConnection();
|
|
200
|
+
const now = new Date().toISOString();
|
|
201
|
+
const result = db.prepare(`
|
|
202
|
+
UPDATE tasks
|
|
203
|
+
SET status = 'IN_PROGRESS', claimed_by = ?, claimed_at = ?
|
|
204
|
+
WHERE id = ? AND project_id = ? AND status = 'PENDING'
|
|
205
|
+
`).run(agentId, now, taskId, this.projectId);
|
|
206
|
+
if (result.changes > 0) {
|
|
207
|
+
this.logMessage(agentId, `Claimed task ${taskId}`);
|
|
208
|
+
return true;
|
|
209
|
+
}
|
|
210
|
+
return false;
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Complete a task
|
|
214
|
+
*/
|
|
215
|
+
completeTask(taskId, agentId) {
|
|
216
|
+
const db = databaseManager.getConnection();
|
|
217
|
+
const now = new Date().toISOString();
|
|
218
|
+
const result = db.prepare(`
|
|
219
|
+
UPDATE tasks
|
|
220
|
+
SET status = 'COMPLETED', completed_at = ?
|
|
221
|
+
WHERE id = ? AND project_id = ?
|
|
222
|
+
`).run(now, taskId, this.projectId);
|
|
223
|
+
if (result.changes > 0) {
|
|
224
|
+
this.logMessage(agentId, `Completed task ${taskId}`);
|
|
225
|
+
return true;
|
|
226
|
+
}
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
mapTaskRow(row) {
|
|
230
|
+
return {
|
|
231
|
+
id: row.id,
|
|
232
|
+
projectId: row.project_id,
|
|
233
|
+
title: row.title,
|
|
234
|
+
description: row.description,
|
|
235
|
+
fromProject: row.from_project,
|
|
236
|
+
fromAgent: row.from_agent,
|
|
237
|
+
status: row.status,
|
|
238
|
+
claimedBy: row.claimed_by,
|
|
239
|
+
createdAt: row.created_at,
|
|
240
|
+
claimedAt: row.claimed_at,
|
|
241
|
+
completedAt: row.completed_at
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
// ========================================================================
|
|
245
|
+
// Resource Locks
|
|
246
|
+
// ========================================================================
|
|
247
|
+
/**
|
|
248
|
+
* Claim a resource lock
|
|
249
|
+
*/
|
|
250
|
+
claimResource(agentId, resource) {
|
|
251
|
+
const db = databaseManager.getConnection();
|
|
252
|
+
const now = new Date().toISOString();
|
|
253
|
+
try {
|
|
254
|
+
return databaseManager.transaction(() => {
|
|
255
|
+
// Check if already locked by someone else
|
|
256
|
+
const existing = db.prepare(`
|
|
257
|
+
SELECT agent_id FROM locks
|
|
258
|
+
WHERE resource = ? AND project_id = ?
|
|
259
|
+
`).get(resource, this.projectId);
|
|
260
|
+
if (existing && existing.agent_id !== agentId) {
|
|
261
|
+
return false; // Already locked by another agent
|
|
262
|
+
}
|
|
263
|
+
// Insert or update lock
|
|
264
|
+
db.prepare(`
|
|
265
|
+
INSERT OR REPLACE INTO locks (resource, project_id, agent_id, acquired_at)
|
|
266
|
+
VALUES (?, ?, ?, ?)
|
|
267
|
+
`).run(resource, this.projectId, agentId, now);
|
|
268
|
+
this.logMessage(agentId, `Claimed lock on ${resource}`);
|
|
269
|
+
return true;
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
catch {
|
|
273
|
+
return false;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Release a resource lock
|
|
278
|
+
*/
|
|
279
|
+
releaseResource(agentId, resource) {
|
|
280
|
+
const db = databaseManager.getConnection();
|
|
281
|
+
db.prepare(`
|
|
282
|
+
DELETE FROM locks
|
|
283
|
+
WHERE resource = ? AND project_id = ? AND agent_id = ?
|
|
284
|
+
`).run(resource, this.projectId, agentId);
|
|
285
|
+
this.logMessage(agentId, `Released lock on ${resource}`);
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Get all locks for this project
|
|
289
|
+
*/
|
|
290
|
+
getLocks() {
|
|
291
|
+
const db = databaseManager.getConnection();
|
|
292
|
+
const rows = db.prepare(`
|
|
293
|
+
SELECT resource, project_id, agent_id, acquired_at
|
|
294
|
+
FROM locks
|
|
295
|
+
WHERE project_id = ?
|
|
296
|
+
`).all(this.projectId);
|
|
297
|
+
return rows.map(row => ({
|
|
298
|
+
resource: row.resource,
|
|
299
|
+
projectId: row.project_id,
|
|
300
|
+
agentId: row.agent_id,
|
|
301
|
+
acquiredAt: row.acquired_at
|
|
302
|
+
}));
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* Release all locks held by an agent (cleanup)
|
|
306
|
+
*/
|
|
307
|
+
releaseAllLocks(agentId) {
|
|
308
|
+
const db = databaseManager.getConnection();
|
|
309
|
+
const result = db.prepare(`
|
|
310
|
+
DELETE FROM locks
|
|
311
|
+
WHERE agent_id = ? AND project_id = ?
|
|
312
|
+
`).run(agentId, this.projectId);
|
|
313
|
+
return result.changes;
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Cleanup orphaned locks (locks from inactive agents)
|
|
317
|
+
*/
|
|
318
|
+
cleanupOrphanedLocks() {
|
|
319
|
+
const db = databaseManager.getConnection();
|
|
320
|
+
const result = db.prepare(`
|
|
321
|
+
DELETE FROM locks
|
|
322
|
+
WHERE project_id = ? AND agent_id NOT IN (
|
|
323
|
+
SELECT id FROM agents WHERE project_id = ? AND status = 'ACTIVE'
|
|
324
|
+
)
|
|
325
|
+
`).run(this.projectId, this.projectId);
|
|
326
|
+
return result.changes;
|
|
327
|
+
}
|
|
328
|
+
// ========================================================================
|
|
329
|
+
// Session Events
|
|
330
|
+
// ========================================================================
|
|
331
|
+
/**
|
|
332
|
+
* Log a session event
|
|
333
|
+
*/
|
|
334
|
+
logSessionEvent(sessionId, eventType, eventData, agentId) {
|
|
335
|
+
const db = databaseManager.getConnection();
|
|
336
|
+
const now = new Date().toISOString();
|
|
337
|
+
db.prepare(`
|
|
338
|
+
INSERT INTO session_events (project_id, session_id, agent_id, event_type, event_data, timestamp)
|
|
339
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
340
|
+
`).run(this.projectId, sessionId, agentId || null, eventType, JSON.stringify(eventData), now);
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* Get session history for a specific session
|
|
344
|
+
*/
|
|
345
|
+
getSessionHistory(sessionId) {
|
|
346
|
+
const db = databaseManager.getConnection();
|
|
347
|
+
const rows = db.prepare(`
|
|
348
|
+
SELECT id, project_id, session_id, agent_id, event_type, event_data, timestamp
|
|
349
|
+
FROM session_events
|
|
350
|
+
WHERE session_id = ?
|
|
351
|
+
ORDER BY timestamp ASC
|
|
352
|
+
`).all(sessionId);
|
|
353
|
+
return rows.map(row => ({
|
|
354
|
+
id: row.id,
|
|
355
|
+
projectId: row.project_id,
|
|
356
|
+
sessionId: row.session_id,
|
|
357
|
+
agentId: row.agent_id,
|
|
358
|
+
eventType: row.event_type,
|
|
359
|
+
eventData: JSON.parse(row.event_data),
|
|
360
|
+
timestamp: row.timestamp
|
|
361
|
+
}));
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* Get all sessions for this project (for UI display)
|
|
365
|
+
*/
|
|
366
|
+
getProjectSessions() {
|
|
367
|
+
const db = databaseManager.getConnection();
|
|
368
|
+
const rows = db.prepare(`
|
|
369
|
+
SELECT
|
|
370
|
+
session_id,
|
|
371
|
+
MAX(agent_id) as agent_id,
|
|
372
|
+
COUNT(*) as event_count,
|
|
373
|
+
MIN(timestamp) as first_event,
|
|
374
|
+
MAX(timestamp) as last_event
|
|
375
|
+
FROM session_events
|
|
376
|
+
WHERE project_id = ?
|
|
377
|
+
GROUP BY session_id
|
|
378
|
+
ORDER BY last_event DESC
|
|
379
|
+
`).all(this.projectId);
|
|
380
|
+
return rows.map(row => ({
|
|
381
|
+
sessionId: row.session_id,
|
|
382
|
+
agentId: row.agent_id,
|
|
383
|
+
eventCount: row.event_count,
|
|
384
|
+
firstEvent: row.first_event,
|
|
385
|
+
lastEvent: row.last_event
|
|
386
|
+
}));
|
|
387
|
+
}
|
|
388
|
+
// ========================================================================
|
|
389
|
+
// Messages (Agent Log)
|
|
390
|
+
// ========================================================================
|
|
391
|
+
/**
|
|
392
|
+
* Log a message
|
|
393
|
+
*/
|
|
394
|
+
logMessage(agentId, message) {
|
|
395
|
+
const db = databaseManager.getConnection();
|
|
396
|
+
const now = new Date().toISOString();
|
|
397
|
+
db.prepare(`
|
|
398
|
+
INSERT INTO messages (project_id, agent_id, message, timestamp)
|
|
399
|
+
VALUES (?, ?, ?, ?)
|
|
400
|
+
`).run(this.projectId, agentId, message, now);
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* Get recent messages
|
|
404
|
+
*/
|
|
405
|
+
getMessages(limit = 20) {
|
|
406
|
+
const db = databaseManager.getConnection();
|
|
407
|
+
const rows = db.prepare(`
|
|
408
|
+
SELECT id, project_id, agent_id, message, timestamp
|
|
409
|
+
FROM messages
|
|
410
|
+
WHERE project_id = ?
|
|
411
|
+
ORDER BY timestamp DESC
|
|
412
|
+
LIMIT ?
|
|
413
|
+
`).all(this.projectId, limit);
|
|
414
|
+
return rows.map(row => ({
|
|
415
|
+
id: row.id,
|
|
416
|
+
projectId: row.project_id,
|
|
417
|
+
agentId: row.agent_id,
|
|
418
|
+
message: row.message,
|
|
419
|
+
timestamp: row.timestamp
|
|
420
|
+
}));
|
|
421
|
+
}
|
|
422
|
+
// ========================================================================
|
|
423
|
+
// Export to Markdown (for compatibility/display)
|
|
424
|
+
// ========================================================================
|
|
425
|
+
/**
|
|
426
|
+
* Generate Markdown representation of the board (for get_board action)
|
|
427
|
+
*/
|
|
428
|
+
exportToMarkdown() {
|
|
429
|
+
const agents = this.getAllAgents();
|
|
430
|
+
const tasks = this.getPendingTasks();
|
|
431
|
+
const locks = this.getLocks();
|
|
432
|
+
const messages = this.getMessages(20);
|
|
433
|
+
const lines = [
|
|
434
|
+
'# Multi-Agent Board',
|
|
435
|
+
'',
|
|
436
|
+
'## Active Agents',
|
|
437
|
+
'| Agent ID | Status | Current Focus | Session ID | Last Heartbeat |',
|
|
438
|
+
'|---|---|---|---|---|',
|
|
439
|
+
...agents.map(a => `| ${a.id} | ${a.status} | ${a.focus} | ${a.sessionId} | ${a.lastHeartbeat} |`),
|
|
440
|
+
'',
|
|
441
|
+
'## Pending Tasks',
|
|
442
|
+
'| ID | Title | From | Status | Created At |',
|
|
443
|
+
'|---|---|---|---|---|',
|
|
444
|
+
...tasks.map(t => `| ${t.id} | ${t.title} | ${t.fromProject || t.fromAgent || '-'} | ${t.status} | ${t.createdAt} |`),
|
|
445
|
+
'',
|
|
446
|
+
'## File Locks',
|
|
447
|
+
'| File Pattern | Claimed By | Since |',
|
|
448
|
+
'|---|---|---|',
|
|
449
|
+
...locks.map(l => `| ${l.resource} | ${l.agentId} | ${l.acquiredAt} |`),
|
|
450
|
+
'',
|
|
451
|
+
'## Agent Messages',
|
|
452
|
+
...messages.reverse().map(m => `- [${m.timestamp.split('T')[1]?.split('.')[0] || m.timestamp}] **${m.agentId}**: ${m.message}`),
|
|
453
|
+
''
|
|
454
|
+
];
|
|
455
|
+
return lines.join('\n');
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
// ============================================================================
|
|
459
|
+
// Cleanup utilities
|
|
460
|
+
// ============================================================================
|
|
461
|
+
/**
|
|
462
|
+
* Cleanup stale agents (no heartbeat for specified minutes)
|
|
463
|
+
*/
|
|
464
|
+
export function cleanupStaleAgents(staleMinutes = 30) {
|
|
465
|
+
const db = databaseManager.getConnection();
|
|
466
|
+
const result = db.prepare(`
|
|
467
|
+
UPDATE agents
|
|
468
|
+
SET status = 'INACTIVE'
|
|
469
|
+
WHERE status = 'ACTIVE'
|
|
470
|
+
AND datetime(last_heartbeat) < datetime('now', '-' || ? || ' minutes')
|
|
471
|
+
`).run(staleMinutes);
|
|
472
|
+
return result.changes;
|
|
473
|
+
}
|
|
474
|
+
/**
|
|
475
|
+
* Cleanup all orphaned locks across all projects
|
|
476
|
+
*/
|
|
477
|
+
export function cleanupAllOrphanedLocks() {
|
|
478
|
+
const db = databaseManager.getConnection();
|
|
479
|
+
const result = db.prepare(`
|
|
480
|
+
DELETE FROM locks
|
|
481
|
+
WHERE agent_id NOT IN (
|
|
482
|
+
SELECT id FROM agents WHERE status = 'ACTIVE'
|
|
483
|
+
)
|
|
484
|
+
`).run();
|
|
485
|
+
return result.changes;
|
|
486
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview SQLite Database Manager for Memory Bank
|
|
3
|
+
* Centralized database for agent coordination, sessions, tasks, and locks.
|
|
4
|
+
* Located at ~/.memorybank/agentboard.db (same directory as global_registry.json)
|
|
5
|
+
*/
|
|
6
|
+
import Database from 'better-sqlite3';
|
|
7
|
+
import * as path from 'path';
|
|
8
|
+
import * as os from 'os';
|
|
9
|
+
import * as fs from 'fs';
|
|
10
|
+
const SCHEMA_VERSION = 1;
|
|
11
|
+
/**
|
|
12
|
+
* SQL Schema for Agent Board
|
|
13
|
+
*/
|
|
14
|
+
const SCHEMA_SQL = `
|
|
15
|
+
-- Agents: All agent sessions across all projects
|
|
16
|
+
-- Only one agent can be ACTIVE per project at a time
|
|
17
|
+
CREATE TABLE IF NOT EXISTS agents (
|
|
18
|
+
id TEXT NOT NULL, -- Full agent ID with hash suffix (e.g., 'Dev-VSCode-Gemini-abc12345')
|
|
19
|
+
project_id TEXT NOT NULL, -- Project this agent is working on
|
|
20
|
+
session_id TEXT NOT NULL, -- UUID for this session
|
|
21
|
+
status TEXT NOT NULL DEFAULT 'ACTIVE', -- ACTIVE, INACTIVE
|
|
22
|
+
focus TEXT DEFAULT '-', -- Current task/file being worked on
|
|
23
|
+
last_heartbeat TEXT NOT NULL, -- ISO timestamp of last activity
|
|
24
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
25
|
+
PRIMARY KEY (id, project_id)
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
CREATE INDEX IF NOT EXISTS idx_agents_project ON agents(project_id);
|
|
29
|
+
CREATE INDEX IF NOT EXISTS idx_agents_status ON agents(project_id, status);
|
|
30
|
+
CREATE INDEX IF NOT EXISTS idx_agents_session ON agents(session_id);
|
|
31
|
+
|
|
32
|
+
-- Tasks: Project-centric tasks (not assigned to specific agents)
|
|
33
|
+
-- The active agent of a project processes its pending tasks
|
|
34
|
+
CREATE TABLE IF NOT EXISTS tasks (
|
|
35
|
+
id TEXT PRIMARY KEY, -- e.g., 'TASK-123456'
|
|
36
|
+
project_id TEXT NOT NULL, -- Target project to handle this task
|
|
37
|
+
title TEXT NOT NULL,
|
|
38
|
+
description TEXT, -- Extended task description
|
|
39
|
+
from_project TEXT, -- Source project (for cross-project delegation)
|
|
40
|
+
from_agent TEXT, -- Agent that created the task
|
|
41
|
+
status TEXT NOT NULL DEFAULT 'PENDING', -- PENDING, IN_PROGRESS, COMPLETED, CANCELLED
|
|
42
|
+
claimed_by TEXT, -- Agent ID that claimed the task
|
|
43
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
44
|
+
claimed_at TEXT,
|
|
45
|
+
completed_at TEXT
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_project ON tasks(project_id);
|
|
49
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(project_id, status);
|
|
50
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_from ON tasks(from_project);
|
|
51
|
+
|
|
52
|
+
-- Locks: Resource/file locks held by agents
|
|
53
|
+
CREATE TABLE IF NOT EXISTS locks (
|
|
54
|
+
resource TEXT NOT NULL, -- Resource pattern (e.g., 'src/auth/')
|
|
55
|
+
project_id TEXT NOT NULL, -- Project scope
|
|
56
|
+
agent_id TEXT NOT NULL, -- Agent holding the lock
|
|
57
|
+
acquired_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
58
|
+
PRIMARY KEY (resource, project_id)
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
CREATE INDEX IF NOT EXISTS idx_locks_agent ON locks(agent_id);
|
|
62
|
+
CREATE INDEX IF NOT EXISTS idx_locks_project ON locks(project_id);
|
|
63
|
+
|
|
64
|
+
-- Session Events: All actions performed during agent sessions
|
|
65
|
+
-- Replaces JSONL files for better querying
|
|
66
|
+
CREATE TABLE IF NOT EXISTS session_events (
|
|
67
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
68
|
+
project_id TEXT NOT NULL,
|
|
69
|
+
session_id TEXT NOT NULL,
|
|
70
|
+
agent_id TEXT, -- Agent that performed the action
|
|
71
|
+
event_type TEXT NOT NULL, -- search, read_doc, read_file, index, decision, write_file, etc.
|
|
72
|
+
event_data TEXT NOT NULL, -- JSON blob with event details
|
|
73
|
+
timestamp TEXT NOT NULL DEFAULT (datetime('now'))
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
CREATE INDEX IF NOT EXISTS idx_events_session ON session_events(session_id);
|
|
77
|
+
CREATE INDEX IF NOT EXISTS idx_events_project ON session_events(project_id);
|
|
78
|
+
CREATE INDEX IF NOT EXISTS idx_events_type ON session_events(event_type);
|
|
79
|
+
CREATE INDEX IF NOT EXISTS idx_events_time ON session_events(timestamp);
|
|
80
|
+
|
|
81
|
+
-- Messages: Agent communication log
|
|
82
|
+
CREATE TABLE IF NOT EXISTS messages (
|
|
83
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
84
|
+
project_id TEXT NOT NULL,
|
|
85
|
+
agent_id TEXT NOT NULL,
|
|
86
|
+
message TEXT NOT NULL,
|
|
87
|
+
timestamp TEXT NOT NULL DEFAULT (datetime('now'))
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
CREATE INDEX IF NOT EXISTS idx_messages_project ON messages(project_id);
|
|
91
|
+
CREATE INDEX IF NOT EXISTS idx_messages_time ON messages(timestamp);
|
|
92
|
+
|
|
93
|
+
-- Schema versioning for future migrations
|
|
94
|
+
CREATE TABLE IF NOT EXISTS schema_version (
|
|
95
|
+
version INTEGER PRIMARY KEY,
|
|
96
|
+
applied_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
97
|
+
);
|
|
98
|
+
`;
|
|
99
|
+
/**
|
|
100
|
+
* Database Manager Singleton
|
|
101
|
+
* Manages a single SQLite connection for the entire MCP server
|
|
102
|
+
*/
|
|
103
|
+
class DatabaseManager {
|
|
104
|
+
static instance;
|
|
105
|
+
db = null;
|
|
106
|
+
dbPath;
|
|
107
|
+
constructor() {
|
|
108
|
+
// Same directory as global_registry.json
|
|
109
|
+
this.dbPath = path.join(os.homedir(), '.memorybank', 'agentboard.db');
|
|
110
|
+
}
|
|
111
|
+
static getInstance() {
|
|
112
|
+
if (!DatabaseManager.instance) {
|
|
113
|
+
DatabaseManager.instance = new DatabaseManager();
|
|
114
|
+
}
|
|
115
|
+
return DatabaseManager.instance;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Get or create the database connection
|
|
119
|
+
*/
|
|
120
|
+
getConnection() {
|
|
121
|
+
if (this.db) {
|
|
122
|
+
return this.db;
|
|
123
|
+
}
|
|
124
|
+
// Ensure directory exists
|
|
125
|
+
const dir = path.dirname(this.dbPath);
|
|
126
|
+
if (!fs.existsSync(dir)) {
|
|
127
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
128
|
+
}
|
|
129
|
+
// Open database with WAL mode for better concurrency
|
|
130
|
+
this.db = new Database(this.dbPath, {
|
|
131
|
+
timeout: 5000, // Wait up to 5s for locks
|
|
132
|
+
});
|
|
133
|
+
// Enable WAL mode for concurrent reads during writes
|
|
134
|
+
this.db.pragma('journal_mode = WAL');
|
|
135
|
+
// Foreign keys enforcement
|
|
136
|
+
this.db.pragma('foreign_keys = ON');
|
|
137
|
+
// Initialize schema
|
|
138
|
+
this.initializeSchema();
|
|
139
|
+
console.error(`[Database] Connected to ${this.dbPath}`);
|
|
140
|
+
return this.db;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Initialize database schema
|
|
144
|
+
*/
|
|
145
|
+
initializeSchema() {
|
|
146
|
+
if (!this.db)
|
|
147
|
+
return;
|
|
148
|
+
// Check current schema version
|
|
149
|
+
let currentVersion = 0;
|
|
150
|
+
try {
|
|
151
|
+
const row = this.db.prepare('SELECT MAX(version) as version FROM schema_version').get();
|
|
152
|
+
currentVersion = row?.version || 0;
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
// Table doesn't exist yet, version is 0
|
|
156
|
+
}
|
|
157
|
+
if (currentVersion < SCHEMA_VERSION) {
|
|
158
|
+
console.error(`[Database] Migrating schema from v${currentVersion} to v${SCHEMA_VERSION}`);
|
|
159
|
+
// Run schema creation (IF NOT EXISTS makes it safe)
|
|
160
|
+
this.db.exec(SCHEMA_SQL);
|
|
161
|
+
// Record schema version
|
|
162
|
+
if (currentVersion === 0) {
|
|
163
|
+
this.db.prepare('INSERT OR REPLACE INTO schema_version (version) VALUES (?)').run(SCHEMA_VERSION);
|
|
164
|
+
}
|
|
165
|
+
console.error(`[Database] Schema initialized at v${SCHEMA_VERSION}`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Close database connection
|
|
170
|
+
*/
|
|
171
|
+
close() {
|
|
172
|
+
if (this.db) {
|
|
173
|
+
this.db.close();
|
|
174
|
+
this.db = null;
|
|
175
|
+
console.error('[Database] Connection closed');
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Get the database file path
|
|
180
|
+
*/
|
|
181
|
+
getDbPath() {
|
|
182
|
+
return this.dbPath;
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Check if database file exists
|
|
186
|
+
*/
|
|
187
|
+
exists() {
|
|
188
|
+
return fs.existsSync(this.dbPath);
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Run a transaction with automatic rollback on error
|
|
192
|
+
*/
|
|
193
|
+
transaction(fn) {
|
|
194
|
+
const db = this.getConnection();
|
|
195
|
+
return db.transaction(fn)();
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
// Export singleton instance
|
|
199
|
+
export const databaseManager = DatabaseManager.getInstance();
|