mcp-coordinator 0.2.1 → 0.4.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 +846 -846
- package/dashboard/Dockerfile +19 -19
- package/dashboard/public/index.html +1178 -1178
- package/dist/cli/dashboard.js +9 -5
- package/dist/cli/server/backup.d.ts +7 -0
- package/dist/cli/server/backup.js +162 -0
- package/dist/cli/server/index.js +5 -0
- package/dist/cli/server/restore.d.ts +2 -0
- package/dist/cli/server/restore.js +117 -0
- package/dist/cli/server/start.js +24 -1
- package/dist/cli/server/status.js +16 -23
- package/dist/src/agent-activity.js +6 -6
- package/dist/src/agent-registry.js +6 -6
- package/dist/src/announce-workflow.d.ts +52 -0
- package/dist/src/announce-workflow.js +91 -0
- package/dist/src/consultation.d.ts +22 -0
- package/dist/src/consultation.js +118 -45
- package/dist/src/database.js +126 -126
- package/dist/src/db-adapter.d.ts +30 -0
- package/dist/src/db-adapter.js +32 -1
- package/dist/src/dependency-map.js +5 -5
- package/dist/src/file-tracker.d.ts +10 -0
- package/dist/src/file-tracker.js +40 -8
- package/dist/src/http/handle-health.d.ts +23 -0
- package/dist/src/http/handle-health.js +86 -0
- package/dist/src/http/handle-rest.d.ts +23 -0
- package/dist/src/http/handle-rest.js +374 -0
- package/dist/src/http/utils.d.ts +15 -0
- package/dist/src/http/utils.js +39 -0
- package/dist/src/impact-scorer.js +87 -50
- package/dist/src/introspection.js +1 -1
- package/dist/src/metrics.d.ts +83 -0
- package/dist/src/metrics.js +162 -0
- package/dist/src/mqtt-bridge.d.ts +21 -0
- package/dist/src/mqtt-bridge.js +55 -5
- package/dist/src/mqtt-broker.d.ts +16 -0
- package/dist/src/mqtt-broker.js +16 -1
- package/dist/src/path-guard.d.ts +14 -0
- package/dist/src/path-guard.js +44 -0
- package/dist/src/reset-guard.d.ts +16 -0
- package/dist/src/reset-guard.js +24 -0
- package/dist/src/serve-http.d.ts +31 -1
- package/dist/src/serve-http.js +189 -446
- package/dist/src/server-setup.d.ts +2 -0
- package/dist/src/server-setup.js +25 -366
- package/dist/src/sse-emitter.d.ts +6 -0
- package/dist/src/sse-emitter.js +50 -2
- package/dist/src/tools/agents-tools.d.ts +8 -0
- package/dist/src/tools/agents-tools.js +46 -0
- package/dist/src/tools/consultation-tools.d.ts +21 -0
- package/dist/src/tools/consultation-tools.js +170 -0
- package/dist/src/tools/dependencies-tools.d.ts +8 -0
- package/dist/src/tools/dependencies-tools.js +27 -0
- package/dist/src/tools/files-tools.d.ts +8 -0
- package/dist/src/tools/files-tools.js +28 -0
- package/dist/src/tools/mqtt-tools.d.ts +9 -0
- package/dist/src/tools/mqtt-tools.js +33 -0
- package/dist/src/tools/status-tools.d.ts +8 -0
- package/dist/src/tools/status-tools.js +63 -0
- package/package.json +83 -80
package/dist/src/consultation.js
CHANGED
|
@@ -4,12 +4,45 @@ import { silentLogger } from "./logger.js";
|
|
|
4
4
|
export class Consultation {
|
|
5
5
|
onResolveCallback = null;
|
|
6
6
|
log;
|
|
7
|
+
timeoutSweeperHandle = null;
|
|
7
8
|
constructor(logger) {
|
|
8
9
|
this.log = logger || silentLogger;
|
|
9
10
|
}
|
|
10
11
|
onResolve(callback) {
|
|
11
12
|
this.onResolveCallback = callback;
|
|
12
13
|
}
|
|
14
|
+
/**
|
|
15
|
+
* B2 fix: replace the side-effect-on-read timeout check with an explicit
|
|
16
|
+
* background sweeper. Each tick atomically claims and resolves any thread
|
|
17
|
+
* past its deadline, then emits resolution events outside the transaction.
|
|
18
|
+
*
|
|
19
|
+
* Default tick interval: 30 seconds. Tests can pass a shorter interval to
|
|
20
|
+
* exercise the sweeper, or call checkTimeouts() explicitly.
|
|
21
|
+
*
|
|
22
|
+
* Safe to call multiple times — second call is a no-op until the previous
|
|
23
|
+
* sweeper is stopped.
|
|
24
|
+
*/
|
|
25
|
+
startTimeoutSweeper(intervalMs = 30000) {
|
|
26
|
+
if (this.timeoutSweeperHandle)
|
|
27
|
+
return;
|
|
28
|
+
this.timeoutSweeperHandle = setInterval(() => {
|
|
29
|
+
try {
|
|
30
|
+
this.checkTimeouts();
|
|
31
|
+
}
|
|
32
|
+
catch (err) {
|
|
33
|
+
this.log.warn({ err }, "Timeout sweeper iteration failed");
|
|
34
|
+
}
|
|
35
|
+
}, intervalMs);
|
|
36
|
+
// Don't keep the event loop alive just for the sweeper.
|
|
37
|
+
if (typeof this.timeoutSweeperHandle.unref === "function")
|
|
38
|
+
this.timeoutSweeperHandle.unref();
|
|
39
|
+
}
|
|
40
|
+
stopTimeoutSweeper() {
|
|
41
|
+
if (this.timeoutSweeperHandle) {
|
|
42
|
+
clearInterval(this.timeoutSweeperHandle);
|
|
43
|
+
this.timeoutSweeperHandle = null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
13
46
|
emitResolution(threadId, type, approvedBy, approvedByName) {
|
|
14
47
|
const db = getDb();
|
|
15
48
|
const thread = this.getThread(threadId);
|
|
@@ -39,23 +72,31 @@ export class Consultation {
|
|
|
39
72
|
announceWork(params) {
|
|
40
73
|
const db = getDb();
|
|
41
74
|
const id = randomUUID();
|
|
42
|
-
//
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
75
|
+
// B1 fix: SELECT respondents + INSERT thread must be atomic w.r.t. agent
|
|
76
|
+
// registry mutations (registerAgent / setOffline). Without a transaction,
|
|
77
|
+
// a race between announce and a concurrent setOffline produces a thread
|
|
78
|
+
// whose expected_respondents contains an agent that just went away — the
|
|
79
|
+
// thread then stays open forever waiting for an absent voter.
|
|
80
|
+
const tx = db.transaction(() => {
|
|
81
|
+
const onlineAgents = db
|
|
82
|
+
.prepare("SELECT id, modules FROM agents WHERE status = 'online' AND id != ?")
|
|
83
|
+
.all(params.agent_id);
|
|
84
|
+
const respondents = onlineAgents.filter((agent) => {
|
|
85
|
+
const agentModules = JSON.parse(agent.modules);
|
|
86
|
+
return params.target_modules.some((tm) => agentModules.some((am) => am === tm || am.startsWith(tm + "/") || tm.startsWith(am + "/")));
|
|
87
|
+
});
|
|
88
|
+
const respondentIds = respondents.map((r) => r.id);
|
|
89
|
+
// Directed dispatch skips module-based auto-resolve: if the thread is
|
|
90
|
+
// explicitly aimed at an agent, we keep it open for them regardless of
|
|
91
|
+
// what the module scorer finds.
|
|
92
|
+
const assignedTo = params.assigned_to ?? null;
|
|
93
|
+
const keepOpen = params.keep_open || assignedTo !== null;
|
|
94
|
+
const autoResolve = respondentIds.length === 0 && !keepOpen;
|
|
95
|
+
db.prepare(`INSERT INTO threads (id, initiator_id, subject, plan, target_modules, target_files, status, expected_respondents, resolved_at, depends_on_files, exports_affected, timeout_seconds, assigned_to)
|
|
96
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(id, params.agent_id, params.subject, params.plan || null, JSON.stringify(params.target_modules), JSON.stringify(params.target_files), autoResolve ? "resolved" : "open", JSON.stringify(respondentIds), autoResolve ? new Date().toISOString() : null, JSON.stringify(params.depends_on_files || []), JSON.stringify(params.exports_affected || []), keepOpen ? 0 : 600, assignedTo);
|
|
97
|
+
return { autoResolve, respondentIds, assignedTo };
|
|
49
98
|
});
|
|
50
|
-
const respondentIds =
|
|
51
|
-
// Directed dispatch skips module-based auto-resolve: if the thread is
|
|
52
|
-
// explicitly aimed at an agent, we keep it open for them regardless of
|
|
53
|
-
// what the module scorer finds.
|
|
54
|
-
const assignedTo = params.assigned_to ?? null;
|
|
55
|
-
const keepOpen = params.keep_open || assignedTo !== null;
|
|
56
|
-
const autoResolve = respondentIds.length === 0 && !keepOpen;
|
|
57
|
-
db.prepare(`INSERT INTO threads (id, initiator_id, subject, plan, target_modules, target_files, status, expected_respondents, resolved_at, depends_on_files, exports_affected, timeout_seconds, assigned_to)
|
|
58
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(id, params.agent_id, params.subject, params.plan || null, JSON.stringify(params.target_modules), JSON.stringify(params.target_files), autoResolve ? "resolved" : "open", JSON.stringify(respondentIds), autoResolve ? new Date().toISOString() : null, JSON.stringify(params.depends_on_files || []), JSON.stringify(params.exports_affected || []), keepOpen ? 0 : 600, assignedTo);
|
|
99
|
+
const { autoResolve, respondentIds, assignedTo } = tx();
|
|
59
100
|
this.log.info({
|
|
60
101
|
thread_id: id,
|
|
61
102
|
agent_id: params.agent_id,
|
|
@@ -81,7 +122,7 @@ export class Consultation {
|
|
|
81
122
|
const id = randomUUID();
|
|
82
123
|
// Simple token estimate: ~4 chars per token for English/French
|
|
83
124
|
const tokenEstimate = Math.ceil(params.content.length / 4);
|
|
84
|
-
db.prepare(`INSERT INTO thread_messages (id, thread_id, agent_id, agent_name, type, content, context_snapshot, in_reply_to, round, token_estimate)
|
|
125
|
+
db.prepare(`INSERT INTO thread_messages (id, thread_id, agent_id, agent_name, type, content, context_snapshot, in_reply_to, round, token_estimate)
|
|
85
126
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(id, params.thread_id, params.agent_id, params.agent_name || null, params.type, params.content, params.context_snapshot || null, params.in_reply_to || null, thread.round, tokenEstimate);
|
|
86
127
|
this.log.debug({
|
|
87
128
|
thread_id: params.thread_id,
|
|
@@ -109,12 +150,23 @@ export class Consultation {
|
|
|
109
150
|
throw new Error(`Thread ${threadId} not found`);
|
|
110
151
|
if (thread.status !== "resolving")
|
|
111
152
|
throw new Error(`Thread is ${thread.status}, not resolving`);
|
|
112
|
-
//
|
|
113
|
-
|
|
153
|
+
// B1 fix: post + check + transition must be atomic, else two concurrent
|
|
154
|
+
// approvals each see "all approved" and fire emitResolution twice. Using
|
|
155
|
+
// a transaction + UPDATE ... WHERE status='resolving' (CAS) ensures only
|
|
156
|
+
// the first transaction wins the consensus race; the loser's UPDATE
|
|
157
|
+
// affects 0 rows and emit is suppressed.
|
|
158
|
+
const tx = db.transaction(() => {
|
|
159
|
+
this.postResolutionMessage(threadId, agentId, "approve", "Approved");
|
|
160
|
+
if (!this.allRespondentsApproved(threadId))
|
|
161
|
+
return false;
|
|
162
|
+
const res = db
|
|
163
|
+
.prepare("UPDATE threads SET status = 'resolved', resolved_at = ? WHERE id = ? AND status = 'resolving'")
|
|
164
|
+
.run(new Date().toISOString(), threadId);
|
|
165
|
+
return res.changes > 0;
|
|
166
|
+
});
|
|
167
|
+
const wonRace = tx();
|
|
114
168
|
this.log.debug({ thread_id: threadId, agent_id: agentId }, "Resolution approved");
|
|
115
|
-
|
|
116
|
-
if (this.allRespondentsApproved(threadId)) {
|
|
117
|
-
db.prepare("UPDATE threads SET status = 'resolved', resolved_at = ? WHERE id = ?").run(new Date().toISOString(), threadId);
|
|
169
|
+
if (wonRace) {
|
|
118
170
|
this.emitResolution(threadId, "consensus", agentId, agentName || agentId);
|
|
119
171
|
}
|
|
120
172
|
}
|
|
@@ -194,30 +246,43 @@ export class Consultation {
|
|
|
194
246
|
}
|
|
195
247
|
checkTimeouts() {
|
|
196
248
|
const db = getDb();
|
|
197
|
-
//
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
249
|
+
// B2 fix: SELECT-then-UPDATE wrapped in a transaction so two concurrent
|
|
250
|
+
// sweepers can't both claim the same thread. The UPDATE returns
|
|
251
|
+
// db.changes (rows affected) — we use the SELECT only to know which
|
|
252
|
+
// thread IDs to emit for. The transaction makes both observe the same
|
|
253
|
+
// snapshot.
|
|
254
|
+
const tx = db.transaction(() => {
|
|
255
|
+
const timedOut = db.prepare(`
|
|
256
|
+
SELECT id FROM threads
|
|
257
|
+
WHERE status IN ('open', 'resolving')
|
|
258
|
+
AND timeout_seconds > 0
|
|
259
|
+
AND datetime(created_at, '+' || (timeout_seconds * round) || ' seconds') < CURRENT_TIMESTAMP
|
|
260
|
+
`).all();
|
|
261
|
+
if (timedOut.length === 0)
|
|
262
|
+
return [];
|
|
263
|
+
db.prepare(`
|
|
264
|
+
UPDATE threads SET status = 'resolved',
|
|
265
|
+
resolution_summary = 'Résolu par timeout — pas de réponse dans le délai',
|
|
266
|
+
resolved_at = CURRENT_TIMESTAMP
|
|
267
|
+
WHERE status IN ('open', 'resolving')
|
|
268
|
+
AND timeout_seconds > 0
|
|
269
|
+
AND datetime(created_at, '+' || (timeout_seconds * round) || ' seconds') < CURRENT_TIMESTAMP
|
|
270
|
+
`).run();
|
|
271
|
+
return timedOut;
|
|
272
|
+
});
|
|
273
|
+
const timedOut = tx();
|
|
204
274
|
if (timedOut.length === 0)
|
|
205
275
|
return;
|
|
206
|
-
db.prepare(`
|
|
207
|
-
UPDATE threads SET status = 'resolved',
|
|
208
|
-
resolution_summary = 'Résolu par timeout — pas de réponse dans le délai',
|
|
209
|
-
resolved_at = CURRENT_TIMESTAMP
|
|
210
|
-
WHERE status IN ('open', 'resolving')
|
|
211
|
-
AND timeout_seconds > 0
|
|
212
|
-
AND datetime(created_at, '+' || (timeout_seconds * round) || ' seconds') < CURRENT_TIMESTAMP
|
|
213
|
-
`).run();
|
|
214
276
|
this.log.info({ count: timedOut.length, thread_ids: timedOut.map(t => t.id) }, "Threads timed out");
|
|
277
|
+
// Emit OUTSIDE the transaction so listeners can re-enter the DB safely.
|
|
215
278
|
for (const t of timedOut) {
|
|
216
279
|
this.emitResolution(t.id, "timeout");
|
|
217
280
|
}
|
|
218
281
|
}
|
|
219
282
|
getThread(threadId) {
|
|
220
|
-
|
|
283
|
+
// B2 fix: timeout sweeping moved to startTimeoutSweeper() background timer.
|
|
284
|
+
// Reads no longer mutate state. Tests that need synchronous timeout
|
|
285
|
+
// resolution should call checkTimeouts() explicitly.
|
|
221
286
|
const db = getDb();
|
|
222
287
|
return (db.prepare("SELECT * FROM threads WHERE id = ?").get(threadId) || null);
|
|
223
288
|
}
|
|
@@ -232,7 +297,7 @@ export class Consultation {
|
|
|
232
297
|
return { thread, messages };
|
|
233
298
|
}
|
|
234
299
|
listThreads(filters) {
|
|
235
|
-
|
|
300
|
+
// B2 fix: removed checkTimeouts() side-effect; sweeper handles it.
|
|
236
301
|
const db = getDb();
|
|
237
302
|
let sql = "SELECT * FROM threads WHERE 1=1";
|
|
238
303
|
const params = [];
|
|
@@ -255,14 +320,22 @@ export class Consultation {
|
|
|
255
320
|
sql += " AND (assigned_to IS NULL OR assigned_to = ?)";
|
|
256
321
|
params.push(filters.assigned_to_me);
|
|
257
322
|
}
|
|
323
|
+
if (typeof filters.since_minutes === "number") {
|
|
324
|
+
// For resolved threads, gate on resolved_at (the moment that matters
|
|
325
|
+
// for "recent enough to still influence scoring"). For open/resolving
|
|
326
|
+
// threads, gate on created_at since they have no resolved_at yet.
|
|
327
|
+
// COALESCE picks the right column per row.
|
|
328
|
+
sql += " AND COALESCE(resolved_at, created_at) > datetime('now', '-' || ? || ' minutes')";
|
|
329
|
+
params.push(filters.since_minutes);
|
|
330
|
+
}
|
|
258
331
|
sql += " ORDER BY created_at DESC";
|
|
259
332
|
return db.prepare(sql).all(...params);
|
|
260
333
|
}
|
|
261
334
|
getThreadUpdates(agentId, since) {
|
|
262
335
|
const db = getDb();
|
|
263
|
-
let sql = `SELECT tm.* FROM thread_messages tm
|
|
264
|
-
JOIN threads t ON tm.thread_id = t.id
|
|
265
|
-
WHERE t.status IN ('open', 'resolving')
|
|
336
|
+
let sql = `SELECT tm.* FROM thread_messages tm
|
|
337
|
+
JOIN threads t ON tm.thread_id = t.id
|
|
338
|
+
WHERE t.status IN ('open', 'resolving')
|
|
266
339
|
AND tm.agent_id != ?`;
|
|
267
340
|
const params = [agentId];
|
|
268
341
|
if (since) {
|
|
@@ -284,7 +357,7 @@ export class Consultation {
|
|
|
284
357
|
logActionSummary(params) {
|
|
285
358
|
const db = getDb();
|
|
286
359
|
const id = randomUUID();
|
|
287
|
-
db.prepare(`INSERT INTO action_summaries (id, session_id, agent_id, file_path, summary)
|
|
360
|
+
db.prepare(`INSERT INTO action_summaries (id, session_id, agent_id, file_path, summary)
|
|
288
361
|
VALUES (?, ?, ?, ?, ?)`).run(id, params.session_id, params.agent_id, params.file_path || null, params.summary);
|
|
289
362
|
return db.prepare("SELECT * FROM action_summaries WHERE id = ?").get(id);
|
|
290
363
|
}
|
|
@@ -310,7 +383,7 @@ export class Consultation {
|
|
|
310
383
|
const db = getDb();
|
|
311
384
|
const thread = this.getThread(threadId);
|
|
312
385
|
const id = randomUUID();
|
|
313
|
-
db.prepare(`INSERT INTO thread_messages (id, thread_id, agent_id, type, content, round)
|
|
386
|
+
db.prepare(`INSERT INTO thread_messages (id, thread_id, agent_id, type, content, round)
|
|
314
387
|
VALUES (?, ?, ?, ?, ?, ?)`).run(id, threadId, agentId, type, content, thread.round);
|
|
315
388
|
}
|
|
316
389
|
allRespondentsApproved(threadId) {
|
|
@@ -323,7 +396,7 @@ export class Consultation {
|
|
|
323
396
|
// increments the round, and prior-round approvals must be re-collected
|
|
324
397
|
// for the new proposal.
|
|
325
398
|
const approvals = db
|
|
326
|
-
.prepare(`SELECT DISTINCT agent_id FROM thread_messages
|
|
399
|
+
.prepare(`SELECT DISTINCT agent_id FROM thread_messages
|
|
327
400
|
WHERE thread_id = ? AND type = 'approve' AND round = ?`)
|
|
328
401
|
.all(threadId, thread.round);
|
|
329
402
|
const approvedIds = new Set(approvals.map((a) => a.agent_id));
|
package/dist/src/database.js
CHANGED
|
@@ -3,132 +3,132 @@ import { mkdirSync } from "fs";
|
|
|
3
3
|
import { createRequire } from "module";
|
|
4
4
|
const require = createRequire(import.meta.url);
|
|
5
5
|
let db;
|
|
6
|
-
const SCHEMA = `
|
|
7
|
-
CREATE TABLE IF NOT EXISTS agents (
|
|
8
|
-
id TEXT PRIMARY KEY,
|
|
9
|
-
name TEXT NOT NULL,
|
|
10
|
-
modules TEXT DEFAULT '[]',
|
|
11
|
-
status TEXT DEFAULT 'offline',
|
|
12
|
-
registered_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
|
13
|
-
last_seen_at TEXT DEFAULT CURRENT_TIMESTAMP
|
|
14
|
-
);
|
|
15
|
-
|
|
16
|
-
CREATE TABLE IF NOT EXISTS threads (
|
|
17
|
-
id TEXT PRIMARY KEY,
|
|
18
|
-
initiator_id TEXT NOT NULL,
|
|
19
|
-
subject TEXT NOT NULL,
|
|
20
|
-
plan TEXT,
|
|
21
|
-
target_modules TEXT DEFAULT '[]',
|
|
22
|
-
target_files TEXT DEFAULT '[]',
|
|
23
|
-
status TEXT DEFAULT 'open',
|
|
24
|
-
resolution_summary TEXT,
|
|
25
|
-
conflicts TEXT,
|
|
26
|
-
round INTEGER DEFAULT 1,
|
|
27
|
-
max_rounds INTEGER DEFAULT 4,
|
|
28
|
-
timeout_seconds INTEGER DEFAULT 600,
|
|
29
|
-
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
|
30
|
-
resolved_at TEXT,
|
|
31
|
-
expected_respondents TEXT,
|
|
32
|
-
depends_on_files TEXT,
|
|
33
|
-
exports_affected TEXT,
|
|
34
|
-
claimed_by TEXT,
|
|
35
|
-
claimed_at TEXT,
|
|
36
|
-
FOREIGN KEY (initiator_id) REFERENCES agents(id)
|
|
37
|
-
);
|
|
38
|
-
|
|
39
|
-
CREATE TABLE IF NOT EXISTS thread_messages (
|
|
40
|
-
id TEXT PRIMARY KEY,
|
|
41
|
-
thread_id TEXT NOT NULL,
|
|
42
|
-
agent_id TEXT NOT NULL,
|
|
43
|
-
agent_name TEXT,
|
|
44
|
-
type TEXT NOT NULL,
|
|
45
|
-
content TEXT NOT NULL,
|
|
46
|
-
context_snapshot TEXT,
|
|
47
|
-
in_reply_to TEXT,
|
|
48
|
-
round INTEGER NOT NULL,
|
|
49
|
-
token_estimate INTEGER DEFAULT 0,
|
|
50
|
-
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
|
51
|
-
FOREIGN KEY (thread_id) REFERENCES threads(id),
|
|
52
|
-
FOREIGN KEY (agent_id) REFERENCES agents(id)
|
|
53
|
-
);
|
|
54
|
-
|
|
55
|
-
CREATE TABLE IF NOT EXISTS action_summaries (
|
|
56
|
-
id TEXT PRIMARY KEY,
|
|
57
|
-
session_id TEXT NOT NULL,
|
|
58
|
-
agent_id TEXT NOT NULL,
|
|
59
|
-
file_path TEXT,
|
|
60
|
-
summary TEXT NOT NULL,
|
|
61
|
-
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
|
62
|
-
FOREIGN KEY (agent_id) REFERENCES agents(id)
|
|
63
|
-
);
|
|
64
|
-
|
|
65
|
-
CREATE TABLE IF NOT EXISTS events (
|
|
66
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
67
|
-
type TEXT NOT NULL,
|
|
68
|
-
payload TEXT NOT NULL,
|
|
69
|
-
created_at TEXT DEFAULT CURRENT_TIMESTAMP
|
|
70
|
-
);
|
|
71
|
-
|
|
72
|
-
CREATE TABLE IF NOT EXISTS dependency_map (
|
|
73
|
-
module_id TEXT PRIMARY KEY,
|
|
74
|
-
depends_on TEXT DEFAULT '[]',
|
|
75
|
-
exports TEXT DEFAULT '[]',
|
|
76
|
-
owners TEXT DEFAULT '[]'
|
|
77
|
-
);
|
|
78
|
-
|
|
79
|
-
CREATE TABLE IF NOT EXISTS file_activity (
|
|
80
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
81
|
-
session_id TEXT NOT NULL,
|
|
82
|
-
agent_id TEXT NOT NULL,
|
|
83
|
-
agent_name TEXT,
|
|
84
|
-
tool_name TEXT NOT NULL,
|
|
85
|
-
file_path TEXT NOT NULL,
|
|
86
|
-
module TEXT,
|
|
87
|
-
created_at TEXT DEFAULT CURRENT_TIMESTAMP
|
|
88
|
-
);
|
|
89
|
-
|
|
90
|
-
CREATE INDEX IF NOT EXISTS idx_threads_status ON threads(status);
|
|
91
|
-
CREATE INDEX IF NOT EXISTS idx_threads_initiator ON threads(initiator_id);
|
|
92
|
-
CREATE INDEX IF NOT EXISTS idx_messages_thread ON thread_messages(thread_id);
|
|
93
|
-
CREATE INDEX IF NOT EXISTS idx_messages_agent ON thread_messages(agent_id);
|
|
94
|
-
CREATE INDEX IF NOT EXISTS idx_summaries_agent ON action_summaries(agent_id);
|
|
95
|
-
CREATE INDEX IF NOT EXISTS idx_summaries_session ON action_summaries(session_id);
|
|
96
|
-
CREATE INDEX IF NOT EXISTS idx_events_type ON events(type);
|
|
97
|
-
CREATE INDEX IF NOT EXISTS idx_file_activity_agent ON file_activity(agent_id);
|
|
98
|
-
CREATE INDEX IF NOT EXISTS idx_file_activity_path ON file_activity(file_path);
|
|
99
|
-
|
|
100
|
-
CREATE TABLE IF NOT EXISTS introspections (
|
|
101
|
-
id TEXT PRIMARY KEY,
|
|
102
|
-
thread_id TEXT NOT NULL,
|
|
103
|
-
agent_id TEXT NOT NULL,
|
|
104
|
-
score INTEGER NOT NULL,
|
|
105
|
-
reasons TEXT,
|
|
106
|
-
status TEXT DEFAULT 'pending',
|
|
107
|
-
response TEXT,
|
|
108
|
-
concerned INTEGER DEFAULT 0,
|
|
109
|
-
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
|
110
|
-
responded_at TEXT,
|
|
111
|
-
FOREIGN KEY (thread_id) REFERENCES threads(id),
|
|
112
|
-
FOREIGN KEY (agent_id) REFERENCES agents(id)
|
|
113
|
-
);
|
|
114
|
-
|
|
115
|
-
CREATE INDEX IF NOT EXISTS idx_introspections_agent ON introspections(agent_id);
|
|
116
|
-
CREATE INDEX IF NOT EXISTS idx_introspections_status ON introspections(status);
|
|
117
|
-
|
|
118
|
-
CREATE TABLE IF NOT EXISTS agent_activity_status (
|
|
119
|
-
agent_id TEXT PRIMARY KEY,
|
|
120
|
-
activity_status TEXT DEFAULT 'idle',
|
|
121
|
-
current_file TEXT,
|
|
122
|
-
current_thread TEXT,
|
|
123
|
-
last_activity_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
|
124
|
-
FOREIGN KEY (agent_id) REFERENCES agents(id)
|
|
125
|
-
);
|
|
126
|
-
|
|
127
|
-
CREATE TABLE IF NOT EXISTS revoked_agents (
|
|
128
|
-
agent_id TEXT PRIMARY KEY,
|
|
129
|
-
revoked_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
130
|
-
revoked_by TEXT NOT NULL
|
|
131
|
-
);
|
|
6
|
+
const SCHEMA = `
|
|
7
|
+
CREATE TABLE IF NOT EXISTS agents (
|
|
8
|
+
id TEXT PRIMARY KEY,
|
|
9
|
+
name TEXT NOT NULL,
|
|
10
|
+
modules TEXT DEFAULT '[]',
|
|
11
|
+
status TEXT DEFAULT 'offline',
|
|
12
|
+
registered_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
|
13
|
+
last_seen_at TEXT DEFAULT CURRENT_TIMESTAMP
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
CREATE TABLE IF NOT EXISTS threads (
|
|
17
|
+
id TEXT PRIMARY KEY,
|
|
18
|
+
initiator_id TEXT NOT NULL,
|
|
19
|
+
subject TEXT NOT NULL,
|
|
20
|
+
plan TEXT,
|
|
21
|
+
target_modules TEXT DEFAULT '[]',
|
|
22
|
+
target_files TEXT DEFAULT '[]',
|
|
23
|
+
status TEXT DEFAULT 'open',
|
|
24
|
+
resolution_summary TEXT,
|
|
25
|
+
conflicts TEXT,
|
|
26
|
+
round INTEGER DEFAULT 1,
|
|
27
|
+
max_rounds INTEGER DEFAULT 4,
|
|
28
|
+
timeout_seconds INTEGER DEFAULT 600,
|
|
29
|
+
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
|
30
|
+
resolved_at TEXT,
|
|
31
|
+
expected_respondents TEXT,
|
|
32
|
+
depends_on_files TEXT,
|
|
33
|
+
exports_affected TEXT,
|
|
34
|
+
claimed_by TEXT,
|
|
35
|
+
claimed_at TEXT,
|
|
36
|
+
FOREIGN KEY (initiator_id) REFERENCES agents(id)
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
CREATE TABLE IF NOT EXISTS thread_messages (
|
|
40
|
+
id TEXT PRIMARY KEY,
|
|
41
|
+
thread_id TEXT NOT NULL,
|
|
42
|
+
agent_id TEXT NOT NULL,
|
|
43
|
+
agent_name TEXT,
|
|
44
|
+
type TEXT NOT NULL,
|
|
45
|
+
content TEXT NOT NULL,
|
|
46
|
+
context_snapshot TEXT,
|
|
47
|
+
in_reply_to TEXT,
|
|
48
|
+
round INTEGER NOT NULL,
|
|
49
|
+
token_estimate INTEGER DEFAULT 0,
|
|
50
|
+
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
|
51
|
+
FOREIGN KEY (thread_id) REFERENCES threads(id),
|
|
52
|
+
FOREIGN KEY (agent_id) REFERENCES agents(id)
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
CREATE TABLE IF NOT EXISTS action_summaries (
|
|
56
|
+
id TEXT PRIMARY KEY,
|
|
57
|
+
session_id TEXT NOT NULL,
|
|
58
|
+
agent_id TEXT NOT NULL,
|
|
59
|
+
file_path TEXT,
|
|
60
|
+
summary TEXT NOT NULL,
|
|
61
|
+
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
|
62
|
+
FOREIGN KEY (agent_id) REFERENCES agents(id)
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
CREATE TABLE IF NOT EXISTS events (
|
|
66
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
67
|
+
type TEXT NOT NULL,
|
|
68
|
+
payload TEXT NOT NULL,
|
|
69
|
+
created_at TEXT DEFAULT CURRENT_TIMESTAMP
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
CREATE TABLE IF NOT EXISTS dependency_map (
|
|
73
|
+
module_id TEXT PRIMARY KEY,
|
|
74
|
+
depends_on TEXT DEFAULT '[]',
|
|
75
|
+
exports TEXT DEFAULT '[]',
|
|
76
|
+
owners TEXT DEFAULT '[]'
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
CREATE TABLE IF NOT EXISTS file_activity (
|
|
80
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
81
|
+
session_id TEXT NOT NULL,
|
|
82
|
+
agent_id TEXT NOT NULL,
|
|
83
|
+
agent_name TEXT,
|
|
84
|
+
tool_name TEXT NOT NULL,
|
|
85
|
+
file_path TEXT NOT NULL,
|
|
86
|
+
module TEXT,
|
|
87
|
+
created_at TEXT DEFAULT CURRENT_TIMESTAMP
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
CREATE INDEX IF NOT EXISTS idx_threads_status ON threads(status);
|
|
91
|
+
CREATE INDEX IF NOT EXISTS idx_threads_initiator ON threads(initiator_id);
|
|
92
|
+
CREATE INDEX IF NOT EXISTS idx_messages_thread ON thread_messages(thread_id);
|
|
93
|
+
CREATE INDEX IF NOT EXISTS idx_messages_agent ON thread_messages(agent_id);
|
|
94
|
+
CREATE INDEX IF NOT EXISTS idx_summaries_agent ON action_summaries(agent_id);
|
|
95
|
+
CREATE INDEX IF NOT EXISTS idx_summaries_session ON action_summaries(session_id);
|
|
96
|
+
CREATE INDEX IF NOT EXISTS idx_events_type ON events(type);
|
|
97
|
+
CREATE INDEX IF NOT EXISTS idx_file_activity_agent ON file_activity(agent_id);
|
|
98
|
+
CREATE INDEX IF NOT EXISTS idx_file_activity_path ON file_activity(file_path);
|
|
99
|
+
|
|
100
|
+
CREATE TABLE IF NOT EXISTS introspections (
|
|
101
|
+
id TEXT PRIMARY KEY,
|
|
102
|
+
thread_id TEXT NOT NULL,
|
|
103
|
+
agent_id TEXT NOT NULL,
|
|
104
|
+
score INTEGER NOT NULL,
|
|
105
|
+
reasons TEXT,
|
|
106
|
+
status TEXT DEFAULT 'pending',
|
|
107
|
+
response TEXT,
|
|
108
|
+
concerned INTEGER DEFAULT 0,
|
|
109
|
+
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
|
110
|
+
responded_at TEXT,
|
|
111
|
+
FOREIGN KEY (thread_id) REFERENCES threads(id),
|
|
112
|
+
FOREIGN KEY (agent_id) REFERENCES agents(id)
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
CREATE INDEX IF NOT EXISTS idx_introspections_agent ON introspections(agent_id);
|
|
116
|
+
CREATE INDEX IF NOT EXISTS idx_introspections_status ON introspections(status);
|
|
117
|
+
|
|
118
|
+
CREATE TABLE IF NOT EXISTS agent_activity_status (
|
|
119
|
+
agent_id TEXT PRIMARY KEY,
|
|
120
|
+
activity_status TEXT DEFAULT 'idle',
|
|
121
|
+
current_file TEXT,
|
|
122
|
+
current_thread TEXT,
|
|
123
|
+
last_activity_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
|
124
|
+
FOREIGN KEY (agent_id) REFERENCES agents(id)
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
CREATE TABLE IF NOT EXISTS revoked_agents (
|
|
128
|
+
agent_id TEXT PRIMARY KEY,
|
|
129
|
+
revoked_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
130
|
+
revoked_by TEXT NOT NULL
|
|
131
|
+
);
|
|
132
132
|
`;
|
|
133
133
|
function createBetterSqlite3(dataDir) {
|
|
134
134
|
mkdirSync(dataDir, { recursive: true });
|
package/dist/src/db-adapter.d.ts
CHANGED
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Database adapter surface.
|
|
3
|
+
*
|
|
4
|
+
* Design intent: this file is the *contract* both `createBetterSqlite3` and
|
|
5
|
+
* `createBunSqlite` (in `database.ts`) implement. The interfaces are a strict
|
|
6
|
+
* subset of better-sqlite3's API that Bun:sqlite also satisfies, so callers
|
|
7
|
+
* stay portable across both runtimes.
|
|
8
|
+
*
|
|
9
|
+
* Helpers (e.g. `withTransaction`) live here so portable code paths can use
|
|
10
|
+
* one canonical entry point without each call site re-deriving the
|
|
11
|
+
* `db.transaction(fn)()` two-step pattern.
|
|
12
|
+
*/
|
|
1
13
|
export interface RunResult {
|
|
2
14
|
changes: number;
|
|
3
15
|
lastInsertRowid: number;
|
|
@@ -13,3 +25,21 @@ export interface DatabaseAdapter {
|
|
|
13
25
|
close(): void;
|
|
14
26
|
transaction<T>(fn: () => T): () => T;
|
|
15
27
|
}
|
|
28
|
+
/**
|
|
29
|
+
* Run `fn` inside a single SQLite transaction and return its result.
|
|
30
|
+
*
|
|
31
|
+
* Replaces the verbose two-step pattern:
|
|
32
|
+
*
|
|
33
|
+
* const tx = db.transaction(() => { ...; return value; });
|
|
34
|
+
* const value = tx();
|
|
35
|
+
*
|
|
36
|
+
* with:
|
|
37
|
+
*
|
|
38
|
+
* const value = withTransaction(db, () => { ...; return value; });
|
|
39
|
+
*
|
|
40
|
+
* Errors thrown inside `fn` propagate to the caller and the transaction is
|
|
41
|
+
* rolled back by the underlying driver (better-sqlite3 / bun:sqlite both do
|
|
42
|
+
* this). Use this for any read-modify-write block where multiple statements
|
|
43
|
+
* must be atomic.
|
|
44
|
+
*/
|
|
45
|
+
export declare function withTransaction<T>(db: DatabaseAdapter, fn: () => T): T;
|
package/dist/src/db-adapter.js
CHANGED
|
@@ -1 +1,32 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Database adapter surface.
|
|
3
|
+
*
|
|
4
|
+
* Design intent: this file is the *contract* both `createBetterSqlite3` and
|
|
5
|
+
* `createBunSqlite` (in `database.ts`) implement. The interfaces are a strict
|
|
6
|
+
* subset of better-sqlite3's API that Bun:sqlite also satisfies, so callers
|
|
7
|
+
* stay portable across both runtimes.
|
|
8
|
+
*
|
|
9
|
+
* Helpers (e.g. `withTransaction`) live here so portable code paths can use
|
|
10
|
+
* one canonical entry point without each call site re-deriving the
|
|
11
|
+
* `db.transaction(fn)()` two-step pattern.
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* Run `fn` inside a single SQLite transaction and return its result.
|
|
15
|
+
*
|
|
16
|
+
* Replaces the verbose two-step pattern:
|
|
17
|
+
*
|
|
18
|
+
* const tx = db.transaction(() => { ...; return value; });
|
|
19
|
+
* const value = tx();
|
|
20
|
+
*
|
|
21
|
+
* with:
|
|
22
|
+
*
|
|
23
|
+
* const value = withTransaction(db, () => { ...; return value; });
|
|
24
|
+
*
|
|
25
|
+
* Errors thrown inside `fn` propagate to the caller and the transaction is
|
|
26
|
+
* rolled back by the underlying driver (better-sqlite3 / bun:sqlite both do
|
|
27
|
+
* this). Use this for any read-modify-write block where multiple statements
|
|
28
|
+
* must be atomic.
|
|
29
|
+
*/
|
|
30
|
+
export function withTransaction(db, fn) {
|
|
31
|
+
return db.transaction(fn)();
|
|
32
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { getDb } from "./database.js";
|
|
2
|
+
import { withTransaction } from "./db-adapter.js";
|
|
2
3
|
export class DependencyMapper {
|
|
3
4
|
getMap() {
|
|
4
5
|
const db = getDb();
|
|
@@ -16,16 +17,15 @@ export class DependencyMapper {
|
|
|
16
17
|
}
|
|
17
18
|
setMap(map) {
|
|
18
19
|
const db = getDb();
|
|
19
|
-
const stmt = db.prepare(`INSERT INTO dependency_map (module_id, depends_on, exports, owners)
|
|
20
|
-
VALUES (?, ?, ?, ?)
|
|
21
|
-
ON CONFLICT(module_id) DO UPDATE SET
|
|
20
|
+
const stmt = db.prepare(`INSERT INTO dependency_map (module_id, depends_on, exports, owners)
|
|
21
|
+
VALUES (?, ?, ?, ?)
|
|
22
|
+
ON CONFLICT(module_id) DO UPDATE SET
|
|
22
23
|
depends_on = excluded.depends_on, exports = excluded.exports, owners = excluded.owners`);
|
|
23
|
-
|
|
24
|
+
withTransaction(db, () => {
|
|
24
25
|
for (const [id, info] of Object.entries(map)) {
|
|
25
26
|
stmt.run(id, JSON.stringify(info.depends_on), JSON.stringify(info.exports), JSON.stringify(info.owners));
|
|
26
27
|
}
|
|
27
28
|
});
|
|
28
|
-
tx();
|
|
29
29
|
}
|
|
30
30
|
getModuleInfo(moduleId) {
|
|
31
31
|
const db = getDb();
|
|
@@ -17,5 +17,15 @@ export declare class FileTracker {
|
|
|
17
17
|
conflict: boolean;
|
|
18
18
|
agents: string[];
|
|
19
19
|
};
|
|
20
|
+
/**
|
|
21
|
+
* P2 perf: batch lookup of recent file→agents activity. Replaces N
|
|
22
|
+
* `checkFileConflict` calls (one per file) with a single SQL query, then
|
|
23
|
+
* builds an in-memory reverse index. The impact scorer uses this so its
|
|
24
|
+
* per-file inner loop is O(1) Map.get() rather than O(F) SQL round-trips.
|
|
25
|
+
*
|
|
26
|
+
* Excludes the calling agent (so the scorer doesn't flag the announcer
|
|
27
|
+
* against themselves). Returns Map<file_path, Set<agent_id>>.
|
|
28
|
+
*/
|
|
29
|
+
getFileToAgentsIndex(filePaths: string[], excludeAgentId: string, withinMinutes?: number): Map<string, Set<string>>;
|
|
20
30
|
fileToModule(filePath: string): string;
|
|
21
31
|
}
|