mcp-coordinator 0.5.0 → 0.6.1
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 +938 -860
- package/dashboard/Dockerfile +19 -19
- package/dashboard/public/index.html +1201 -1201
- package/dist/src/agent-activity.js +6 -6
- package/dist/src/agent-registry.js +6 -6
- package/dist/src/consultation.js +20 -20
- package/dist/src/database.js +165 -165
- package/dist/src/dependency-map.js +3 -3
- package/dist/src/file-tracker.js +12 -12
- package/dist/src/http/handle-rest.js +56 -13
- package/dist/src/impact-scorer.js +15 -15
- package/dist/src/introspection.js +1 -1
- package/dist/src/tools/consultation-tools.js +4 -4
- package/dist/src/tools/files-tools.js +1 -1
- package/dist/src/working-files-tracker.js +7 -7
- package/package.json +103 -100
|
@@ -59,12 +59,12 @@ export class AgentActivityTracker {
|
|
|
59
59
|
// ── Private ──
|
|
60
60
|
upsert(agentId, status, file, thread) {
|
|
61
61
|
const db = getDb();
|
|
62
|
-
db.prepare(`INSERT INTO agent_activity_status (agent_id, activity_status, current_file, current_thread, last_activity_at)
|
|
63
|
-
VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP)
|
|
64
|
-
ON CONFLICT(agent_id) DO UPDATE SET
|
|
65
|
-
activity_status = excluded.activity_status,
|
|
66
|
-
current_file = excluded.current_file,
|
|
67
|
-
current_thread = excluded.current_thread,
|
|
62
|
+
db.prepare(`INSERT INTO agent_activity_status (agent_id, activity_status, current_file, current_thread, last_activity_at)
|
|
63
|
+
VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP)
|
|
64
|
+
ON CONFLICT(agent_id) DO UPDATE SET
|
|
65
|
+
activity_status = excluded.activity_status,
|
|
66
|
+
current_file = excluded.current_file,
|
|
67
|
+
current_thread = excluded.current_thread,
|
|
68
68
|
last_activity_at = CURRENT_TIMESTAMP`).run(agentId, status, file, thread);
|
|
69
69
|
}
|
|
70
70
|
}
|
|
@@ -2,12 +2,12 @@ import { getDb } from "./database.js";
|
|
|
2
2
|
export class AgentRegistry {
|
|
3
3
|
register(agentId, name, modules) {
|
|
4
4
|
const db = getDb();
|
|
5
|
-
db.prepare(`INSERT INTO agents (id, name, modules, status, registered_at, last_seen_at)
|
|
6
|
-
VALUES (?, ?, ?, 'online', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
|
7
|
-
ON CONFLICT(id) DO UPDATE SET
|
|
8
|
-
name = excluded.name,
|
|
9
|
-
modules = excluded.modules,
|
|
10
|
-
status = 'online',
|
|
5
|
+
db.prepare(`INSERT INTO agents (id, name, modules, status, registered_at, last_seen_at)
|
|
6
|
+
VALUES (?, ?, ?, 'online', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
|
7
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
8
|
+
name = excluded.name,
|
|
9
|
+
modules = excluded.modules,
|
|
10
|
+
status = 'online',
|
|
11
11
|
last_seen_at = CURRENT_TIMESTAMP`).run(agentId, name, JSON.stringify(modules));
|
|
12
12
|
return this.get(agentId);
|
|
13
13
|
}
|
package/dist/src/consultation.js
CHANGED
|
@@ -92,7 +92,7 @@ export class Consultation {
|
|
|
92
92
|
const assignedTo = params.assigned_to ?? null;
|
|
93
93
|
const keepOpen = params.keep_open || assignedTo !== null;
|
|
94
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)
|
|
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
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
97
|
return { autoResolve, respondentIds, assignedTo };
|
|
98
98
|
});
|
|
@@ -122,7 +122,7 @@ export class Consultation {
|
|
|
122
122
|
const id = randomUUID();
|
|
123
123
|
// Simple token estimate: ~4 chars per token for English/French
|
|
124
124
|
const tokenEstimate = Math.ceil(params.content.length / 4);
|
|
125
|
-
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)
|
|
126
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);
|
|
127
127
|
this.log.debug({
|
|
128
128
|
thread_id: params.thread_id,
|
|
@@ -252,21 +252,21 @@ export class Consultation {
|
|
|
252
252
|
// thread IDs to emit for. The transaction makes both observe the same
|
|
253
253
|
// snapshot.
|
|
254
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
|
|
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
260
|
`).all();
|
|
261
261
|
if (timedOut.length === 0)
|
|
262
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
|
|
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
270
|
`).run();
|
|
271
271
|
return timedOut;
|
|
272
272
|
});
|
|
@@ -333,9 +333,9 @@ export class Consultation {
|
|
|
333
333
|
}
|
|
334
334
|
getThreadUpdates(agentId, since) {
|
|
335
335
|
const db = getDb();
|
|
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')
|
|
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')
|
|
339
339
|
AND tm.agent_id != ?`;
|
|
340
340
|
const params = [agentId];
|
|
341
341
|
if (since) {
|
|
@@ -357,7 +357,7 @@ export class Consultation {
|
|
|
357
357
|
logActionSummary(params) {
|
|
358
358
|
const db = getDb();
|
|
359
359
|
const id = randomUUID();
|
|
360
|
-
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)
|
|
361
361
|
VALUES (?, ?, ?, ?, ?)`).run(id, params.session_id, params.agent_id, params.file_path || null, params.summary);
|
|
362
362
|
return db.prepare("SELECT * FROM action_summaries WHERE id = ?").get(id);
|
|
363
363
|
}
|
|
@@ -383,7 +383,7 @@ export class Consultation {
|
|
|
383
383
|
const db = getDb();
|
|
384
384
|
const thread = this.getThread(threadId);
|
|
385
385
|
const id = randomUUID();
|
|
386
|
-
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)
|
|
387
387
|
VALUES (?, ?, ?, ?, ?, ?)`).run(id, threadId, agentId, type, content, thread.round);
|
|
388
388
|
}
|
|
389
389
|
allRespondentsApproved(threadId) {
|
|
@@ -396,7 +396,7 @@ export class Consultation {
|
|
|
396
396
|
// increments the round, and prior-round approvals must be re-collected
|
|
397
397
|
// for the new proposal.
|
|
398
398
|
const approvals = db
|
|
399
|
-
.prepare(`SELECT DISTINCT agent_id FROM thread_messages
|
|
399
|
+
.prepare(`SELECT DISTINCT agent_id FROM thread_messages
|
|
400
400
|
WHERE thread_id = ? AND type = 'approve' AND round = ?`)
|
|
401
401
|
.all(threadId, thread.round);
|
|
402
402
|
const approvedIds = new Set(approvals.map((a) => a.agent_id));
|
package/dist/src/database.js
CHANGED
|
@@ -4,171 +4,171 @@ import { createRequire } from "module";
|
|
|
4
4
|
const require = createRequire(import.meta.url);
|
|
5
5
|
let db;
|
|
6
6
|
const CURRENT_USER_VERSION = 6;
|
|
7
|
-
const SCHEMA = `
|
|
8
|
-
CREATE TABLE IF NOT EXISTS agents (
|
|
9
|
-
id TEXT PRIMARY KEY,
|
|
10
|
-
name TEXT NOT NULL,
|
|
11
|
-
modules TEXT DEFAULT '[]',
|
|
12
|
-
status TEXT DEFAULT 'offline',
|
|
13
|
-
registered_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
|
14
|
-
last_seen_at TEXT DEFAULT CURRENT_TIMESTAMP
|
|
15
|
-
);
|
|
16
|
-
|
|
17
|
-
CREATE TABLE IF NOT EXISTS threads (
|
|
18
|
-
id TEXT PRIMARY KEY,
|
|
19
|
-
initiator_id TEXT NOT NULL,
|
|
20
|
-
subject TEXT NOT NULL,
|
|
21
|
-
plan TEXT,
|
|
22
|
-
target_modules TEXT DEFAULT '[]',
|
|
23
|
-
target_files TEXT DEFAULT '[]',
|
|
24
|
-
status TEXT DEFAULT 'open',
|
|
25
|
-
resolution_summary TEXT,
|
|
26
|
-
conflicts TEXT,
|
|
27
|
-
round INTEGER DEFAULT 1,
|
|
28
|
-
max_rounds INTEGER DEFAULT 4,
|
|
29
|
-
timeout_seconds INTEGER DEFAULT 600,
|
|
30
|
-
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
|
31
|
-
resolved_at TEXT,
|
|
32
|
-
expected_respondents TEXT,
|
|
33
|
-
depends_on_files TEXT,
|
|
34
|
-
exports_affected TEXT,
|
|
35
|
-
claimed_by TEXT,
|
|
36
|
-
claimed_at TEXT,
|
|
37
|
-
FOREIGN KEY (initiator_id) REFERENCES agents(id)
|
|
38
|
-
);
|
|
39
|
-
|
|
40
|
-
CREATE TABLE IF NOT EXISTS thread_messages (
|
|
41
|
-
id TEXT PRIMARY KEY,
|
|
42
|
-
thread_id TEXT NOT NULL,
|
|
43
|
-
agent_id TEXT NOT NULL,
|
|
44
|
-
agent_name TEXT,
|
|
45
|
-
type TEXT NOT NULL,
|
|
46
|
-
content TEXT NOT NULL,
|
|
47
|
-
context_snapshot TEXT,
|
|
48
|
-
in_reply_to TEXT,
|
|
49
|
-
round INTEGER NOT NULL,
|
|
50
|
-
token_estimate INTEGER DEFAULT 0,
|
|
51
|
-
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
|
52
|
-
FOREIGN KEY (thread_id) REFERENCES threads(id),
|
|
53
|
-
FOREIGN KEY (agent_id) REFERENCES agents(id)
|
|
54
|
-
);
|
|
55
|
-
|
|
56
|
-
CREATE TABLE IF NOT EXISTS action_summaries (
|
|
57
|
-
id TEXT PRIMARY KEY,
|
|
58
|
-
session_id TEXT NOT NULL,
|
|
59
|
-
agent_id TEXT NOT NULL,
|
|
60
|
-
file_path TEXT,
|
|
61
|
-
summary TEXT NOT NULL,
|
|
62
|
-
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
|
63
|
-
FOREIGN KEY (agent_id) REFERENCES agents(id)
|
|
64
|
-
);
|
|
65
|
-
|
|
66
|
-
CREATE TABLE IF NOT EXISTS events (
|
|
67
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
68
|
-
type TEXT NOT NULL,
|
|
69
|
-
payload TEXT NOT NULL,
|
|
70
|
-
created_at TEXT DEFAULT CURRENT_TIMESTAMP
|
|
71
|
-
);
|
|
72
|
-
|
|
73
|
-
CREATE TABLE IF NOT EXISTS dependency_map (
|
|
74
|
-
module_id TEXT PRIMARY KEY,
|
|
75
|
-
depends_on TEXT DEFAULT '[]',
|
|
76
|
-
exports TEXT DEFAULT '[]',
|
|
77
|
-
owners TEXT DEFAULT '[]'
|
|
78
|
-
);
|
|
79
|
-
|
|
80
|
-
CREATE TABLE IF NOT EXISTS file_activity (
|
|
81
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
82
|
-
session_id TEXT NOT NULL,
|
|
83
|
-
agent_id TEXT NOT NULL,
|
|
84
|
-
agent_name TEXT,
|
|
85
|
-
tool_name TEXT NOT NULL,
|
|
86
|
-
file_path TEXT NOT NULL,
|
|
87
|
-
module TEXT,
|
|
88
|
-
created_at TEXT DEFAULT CURRENT_TIMESTAMP
|
|
89
|
-
);
|
|
90
|
-
|
|
91
|
-
CREATE INDEX IF NOT EXISTS idx_threads_status ON threads(status);
|
|
92
|
-
CREATE INDEX IF NOT EXISTS idx_threads_initiator ON threads(initiator_id);
|
|
93
|
-
CREATE INDEX IF NOT EXISTS idx_messages_thread ON thread_messages(thread_id);
|
|
94
|
-
CREATE INDEX IF NOT EXISTS idx_messages_agent ON thread_messages(agent_id);
|
|
95
|
-
CREATE INDEX IF NOT EXISTS idx_summaries_agent ON action_summaries(agent_id);
|
|
96
|
-
CREATE INDEX IF NOT EXISTS idx_summaries_session ON action_summaries(session_id);
|
|
97
|
-
CREATE INDEX IF NOT EXISTS idx_events_type ON events(type);
|
|
98
|
-
CREATE INDEX IF NOT EXISTS idx_file_activity_agent ON file_activity(agent_id);
|
|
99
|
-
CREATE INDEX IF NOT EXISTS idx_file_activity_path ON file_activity(file_path);
|
|
100
|
-
|
|
101
|
-
CREATE TABLE IF NOT EXISTS introspections (
|
|
102
|
-
id TEXT PRIMARY KEY,
|
|
103
|
-
thread_id TEXT NOT NULL,
|
|
104
|
-
agent_id TEXT NOT NULL,
|
|
105
|
-
score INTEGER NOT NULL,
|
|
106
|
-
reasons TEXT,
|
|
107
|
-
status TEXT DEFAULT 'pending',
|
|
108
|
-
response TEXT,
|
|
109
|
-
concerned INTEGER DEFAULT 0,
|
|
110
|
-
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
|
111
|
-
responded_at TEXT,
|
|
112
|
-
FOREIGN KEY (thread_id) REFERENCES threads(id),
|
|
113
|
-
FOREIGN KEY (agent_id) REFERENCES agents(id)
|
|
114
|
-
);
|
|
115
|
-
|
|
116
|
-
CREATE INDEX IF NOT EXISTS idx_introspections_agent ON introspections(agent_id);
|
|
117
|
-
CREATE INDEX IF NOT EXISTS idx_introspections_status ON introspections(status);
|
|
118
|
-
|
|
119
|
-
CREATE TABLE IF NOT EXISTS agent_activity_status (
|
|
120
|
-
agent_id TEXT PRIMARY KEY,
|
|
121
|
-
activity_status TEXT DEFAULT 'idle',
|
|
122
|
-
current_file TEXT,
|
|
123
|
-
current_thread TEXT,
|
|
124
|
-
last_activity_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
|
125
|
-
FOREIGN KEY (agent_id) REFERENCES agents(id)
|
|
126
|
-
);
|
|
127
|
-
|
|
128
|
-
CREATE TABLE IF NOT EXISTS revoked_agents (
|
|
129
|
-
agent_id TEXT PRIMARY KEY,
|
|
130
|
-
revoked_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
131
|
-
revoked_by TEXT NOT NULL
|
|
132
|
-
);
|
|
133
|
-
|
|
134
|
-
CREATE TABLE IF NOT EXISTS working_files (
|
|
135
|
-
agent_id TEXT NOT NULL,
|
|
136
|
-
file_path TEXT NOT NULL,
|
|
137
|
-
started_at TEXT NOT NULL,
|
|
138
|
-
last_activity_at TEXT NOT NULL,
|
|
139
|
-
claim_until TEXT NOT NULL,
|
|
140
|
-
PRIMARY KEY (agent_id, file_path)
|
|
141
|
-
);
|
|
142
|
-
CREATE INDEX IF NOT EXISTS idx_working_files_path ON working_files(file_path);
|
|
143
|
-
CREATE INDEX IF NOT EXISTS idx_working_files_until ON working_files(claim_until);
|
|
144
|
-
|
|
145
|
-
CREATE TABLE IF NOT EXISTS git_cochange (
|
|
146
|
-
file_a TEXT NOT NULL,
|
|
147
|
-
file_b TEXT NOT NULL,
|
|
148
|
-
count INTEGER NOT NULL,
|
|
149
|
-
total_commits INTEGER NOT NULL,
|
|
150
|
-
computed_at TEXT NOT NULL,
|
|
151
|
-
PRIMARY KEY (file_a, file_b),
|
|
152
|
-
CHECK (file_a < file_b)
|
|
153
|
-
);
|
|
154
|
-
CREATE INDEX IF NOT EXISTS idx_cochange_a ON git_cochange(file_a);
|
|
155
|
-
CREATE INDEX IF NOT EXISTS idx_cochange_b ON git_cochange(file_b);
|
|
156
|
-
|
|
157
|
-
CREATE TABLE IF NOT EXISTS git_cochange_meta (
|
|
158
|
-
k TEXT PRIMARY KEY,
|
|
159
|
-
v TEXT
|
|
160
|
-
);
|
|
161
|
-
|
|
162
|
-
CREATE TABLE IF NOT EXISTS layer_firings (
|
|
163
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
164
|
-
thread_id TEXT,
|
|
165
|
-
layer TEXT NOT NULL,
|
|
166
|
-
score INTEGER NOT NULL,
|
|
167
|
-
agent_id TEXT,
|
|
168
|
-
fired_at TEXT DEFAULT CURRENT_TIMESTAMP
|
|
169
|
-
);
|
|
170
|
-
CREATE INDEX IF NOT EXISTS idx_firings_layer ON layer_firings(layer, fired_at);
|
|
171
|
-
CREATE INDEX IF NOT EXISTS idx_firings_thread ON layer_firings(thread_id);
|
|
7
|
+
const SCHEMA = `
|
|
8
|
+
CREATE TABLE IF NOT EXISTS agents (
|
|
9
|
+
id TEXT PRIMARY KEY,
|
|
10
|
+
name TEXT NOT NULL,
|
|
11
|
+
modules TEXT DEFAULT '[]',
|
|
12
|
+
status TEXT DEFAULT 'offline',
|
|
13
|
+
registered_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
|
14
|
+
last_seen_at TEXT DEFAULT CURRENT_TIMESTAMP
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
CREATE TABLE IF NOT EXISTS threads (
|
|
18
|
+
id TEXT PRIMARY KEY,
|
|
19
|
+
initiator_id TEXT NOT NULL,
|
|
20
|
+
subject TEXT NOT NULL,
|
|
21
|
+
plan TEXT,
|
|
22
|
+
target_modules TEXT DEFAULT '[]',
|
|
23
|
+
target_files TEXT DEFAULT '[]',
|
|
24
|
+
status TEXT DEFAULT 'open',
|
|
25
|
+
resolution_summary TEXT,
|
|
26
|
+
conflicts TEXT,
|
|
27
|
+
round INTEGER DEFAULT 1,
|
|
28
|
+
max_rounds INTEGER DEFAULT 4,
|
|
29
|
+
timeout_seconds INTEGER DEFAULT 600,
|
|
30
|
+
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
|
31
|
+
resolved_at TEXT,
|
|
32
|
+
expected_respondents TEXT,
|
|
33
|
+
depends_on_files TEXT,
|
|
34
|
+
exports_affected TEXT,
|
|
35
|
+
claimed_by TEXT,
|
|
36
|
+
claimed_at TEXT,
|
|
37
|
+
FOREIGN KEY (initiator_id) REFERENCES agents(id)
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
CREATE TABLE IF NOT EXISTS thread_messages (
|
|
41
|
+
id TEXT PRIMARY KEY,
|
|
42
|
+
thread_id TEXT NOT NULL,
|
|
43
|
+
agent_id TEXT NOT NULL,
|
|
44
|
+
agent_name TEXT,
|
|
45
|
+
type TEXT NOT NULL,
|
|
46
|
+
content TEXT NOT NULL,
|
|
47
|
+
context_snapshot TEXT,
|
|
48
|
+
in_reply_to TEXT,
|
|
49
|
+
round INTEGER NOT NULL,
|
|
50
|
+
token_estimate INTEGER DEFAULT 0,
|
|
51
|
+
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
|
52
|
+
FOREIGN KEY (thread_id) REFERENCES threads(id),
|
|
53
|
+
FOREIGN KEY (agent_id) REFERENCES agents(id)
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
CREATE TABLE IF NOT EXISTS action_summaries (
|
|
57
|
+
id TEXT PRIMARY KEY,
|
|
58
|
+
session_id TEXT NOT NULL,
|
|
59
|
+
agent_id TEXT NOT NULL,
|
|
60
|
+
file_path TEXT,
|
|
61
|
+
summary TEXT NOT NULL,
|
|
62
|
+
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
|
63
|
+
FOREIGN KEY (agent_id) REFERENCES agents(id)
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
CREATE TABLE IF NOT EXISTS events (
|
|
67
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
68
|
+
type TEXT NOT NULL,
|
|
69
|
+
payload TEXT NOT NULL,
|
|
70
|
+
created_at TEXT DEFAULT CURRENT_TIMESTAMP
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
CREATE TABLE IF NOT EXISTS dependency_map (
|
|
74
|
+
module_id TEXT PRIMARY KEY,
|
|
75
|
+
depends_on TEXT DEFAULT '[]',
|
|
76
|
+
exports TEXT DEFAULT '[]',
|
|
77
|
+
owners TEXT DEFAULT '[]'
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
CREATE TABLE IF NOT EXISTS file_activity (
|
|
81
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
82
|
+
session_id TEXT NOT NULL,
|
|
83
|
+
agent_id TEXT NOT NULL,
|
|
84
|
+
agent_name TEXT,
|
|
85
|
+
tool_name TEXT NOT NULL,
|
|
86
|
+
file_path TEXT NOT NULL,
|
|
87
|
+
module TEXT,
|
|
88
|
+
created_at TEXT DEFAULT CURRENT_TIMESTAMP
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
CREATE INDEX IF NOT EXISTS idx_threads_status ON threads(status);
|
|
92
|
+
CREATE INDEX IF NOT EXISTS idx_threads_initiator ON threads(initiator_id);
|
|
93
|
+
CREATE INDEX IF NOT EXISTS idx_messages_thread ON thread_messages(thread_id);
|
|
94
|
+
CREATE INDEX IF NOT EXISTS idx_messages_agent ON thread_messages(agent_id);
|
|
95
|
+
CREATE INDEX IF NOT EXISTS idx_summaries_agent ON action_summaries(agent_id);
|
|
96
|
+
CREATE INDEX IF NOT EXISTS idx_summaries_session ON action_summaries(session_id);
|
|
97
|
+
CREATE INDEX IF NOT EXISTS idx_events_type ON events(type);
|
|
98
|
+
CREATE INDEX IF NOT EXISTS idx_file_activity_agent ON file_activity(agent_id);
|
|
99
|
+
CREATE INDEX IF NOT EXISTS idx_file_activity_path ON file_activity(file_path);
|
|
100
|
+
|
|
101
|
+
CREATE TABLE IF NOT EXISTS introspections (
|
|
102
|
+
id TEXT PRIMARY KEY,
|
|
103
|
+
thread_id TEXT NOT NULL,
|
|
104
|
+
agent_id TEXT NOT NULL,
|
|
105
|
+
score INTEGER NOT NULL,
|
|
106
|
+
reasons TEXT,
|
|
107
|
+
status TEXT DEFAULT 'pending',
|
|
108
|
+
response TEXT,
|
|
109
|
+
concerned INTEGER DEFAULT 0,
|
|
110
|
+
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
|
111
|
+
responded_at TEXT,
|
|
112
|
+
FOREIGN KEY (thread_id) REFERENCES threads(id),
|
|
113
|
+
FOREIGN KEY (agent_id) REFERENCES agents(id)
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
CREATE INDEX IF NOT EXISTS idx_introspections_agent ON introspections(agent_id);
|
|
117
|
+
CREATE INDEX IF NOT EXISTS idx_introspections_status ON introspections(status);
|
|
118
|
+
|
|
119
|
+
CREATE TABLE IF NOT EXISTS agent_activity_status (
|
|
120
|
+
agent_id TEXT PRIMARY KEY,
|
|
121
|
+
activity_status TEXT DEFAULT 'idle',
|
|
122
|
+
current_file TEXT,
|
|
123
|
+
current_thread TEXT,
|
|
124
|
+
last_activity_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
|
125
|
+
FOREIGN KEY (agent_id) REFERENCES agents(id)
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
CREATE TABLE IF NOT EXISTS revoked_agents (
|
|
129
|
+
agent_id TEXT PRIMARY KEY,
|
|
130
|
+
revoked_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
131
|
+
revoked_by TEXT NOT NULL
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
CREATE TABLE IF NOT EXISTS working_files (
|
|
135
|
+
agent_id TEXT NOT NULL,
|
|
136
|
+
file_path TEXT NOT NULL,
|
|
137
|
+
started_at TEXT NOT NULL,
|
|
138
|
+
last_activity_at TEXT NOT NULL,
|
|
139
|
+
claim_until TEXT NOT NULL,
|
|
140
|
+
PRIMARY KEY (agent_id, file_path)
|
|
141
|
+
);
|
|
142
|
+
CREATE INDEX IF NOT EXISTS idx_working_files_path ON working_files(file_path);
|
|
143
|
+
CREATE INDEX IF NOT EXISTS idx_working_files_until ON working_files(claim_until);
|
|
144
|
+
|
|
145
|
+
CREATE TABLE IF NOT EXISTS git_cochange (
|
|
146
|
+
file_a TEXT NOT NULL,
|
|
147
|
+
file_b TEXT NOT NULL,
|
|
148
|
+
count INTEGER NOT NULL,
|
|
149
|
+
total_commits INTEGER NOT NULL,
|
|
150
|
+
computed_at TEXT NOT NULL,
|
|
151
|
+
PRIMARY KEY (file_a, file_b),
|
|
152
|
+
CHECK (file_a < file_b)
|
|
153
|
+
);
|
|
154
|
+
CREATE INDEX IF NOT EXISTS idx_cochange_a ON git_cochange(file_a);
|
|
155
|
+
CREATE INDEX IF NOT EXISTS idx_cochange_b ON git_cochange(file_b);
|
|
156
|
+
|
|
157
|
+
CREATE TABLE IF NOT EXISTS git_cochange_meta (
|
|
158
|
+
k TEXT PRIMARY KEY,
|
|
159
|
+
v TEXT
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
CREATE TABLE IF NOT EXISTS layer_firings (
|
|
163
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
164
|
+
thread_id TEXT,
|
|
165
|
+
layer TEXT NOT NULL,
|
|
166
|
+
score INTEGER NOT NULL,
|
|
167
|
+
agent_id TEXT,
|
|
168
|
+
fired_at TEXT DEFAULT CURRENT_TIMESTAMP
|
|
169
|
+
);
|
|
170
|
+
CREATE INDEX IF NOT EXISTS idx_firings_layer ON layer_firings(layer, fired_at);
|
|
171
|
+
CREATE INDEX IF NOT EXISTS idx_firings_thread ON layer_firings(thread_id);
|
|
172
172
|
`;
|
|
173
173
|
function createBetterSqlite3(dataDir) {
|
|
174
174
|
mkdirSync(dataDir, { recursive: true });
|
|
@@ -17,9 +17,9 @@ export class DependencyMapper {
|
|
|
17
17
|
}
|
|
18
18
|
setMap(map) {
|
|
19
19
|
const db = getDb();
|
|
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
|
|
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
|
|
23
23
|
depends_on = excluded.depends_on, exports = excluded.exports, owners = excluded.owners`);
|
|
24
24
|
withTransaction(db, () => {
|
|
25
25
|
for (const [id, info] of Object.entries(map)) {
|
package/dist/src/file-tracker.js
CHANGED
|
@@ -3,8 +3,8 @@ export class FileTracker {
|
|
|
3
3
|
log(params) {
|
|
4
4
|
const db = getDb();
|
|
5
5
|
const module = this.fileToModule(params.file_path);
|
|
6
|
-
db.prepare(`INSERT INTO file_activity
|
|
7
|
-
(session_id, agent_id, agent_name, tool_name, file_path, module, content_hash, symbols_touched)
|
|
6
|
+
db.prepare(`INSERT INTO file_activity
|
|
7
|
+
(session_id, agent_id, agent_name, tool_name, file_path, module, content_hash, symbols_touched)
|
|
8
8
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`).run(params.session_id, params.agent_id, params.agent_name || null, params.tool_name, params.file_path, module, params.content_hash || null, params.symbols_touched ? JSON.stringify(params.symbols_touched) : null);
|
|
9
9
|
}
|
|
10
10
|
getBySession(sessionId) {
|
|
@@ -13,11 +13,11 @@ export class FileTracker {
|
|
|
13
13
|
}
|
|
14
14
|
getHotFiles(sinceMinutes = 30) {
|
|
15
15
|
const db = getDb();
|
|
16
|
-
const rows = db.prepare(`SELECT file_path, COUNT(DISTINCT agent_id) as agent_count, GROUP_CONCAT(DISTINCT agent_id) as agents
|
|
17
|
-
FROM file_activity
|
|
18
|
-
WHERE created_at > datetime('now', '-' || ? || ' minutes')
|
|
19
|
-
GROUP BY file_path
|
|
20
|
-
HAVING COUNT(DISTINCT agent_id) > 1
|
|
16
|
+
const rows = db.prepare(`SELECT file_path, COUNT(DISTINCT agent_id) as agent_count, GROUP_CONCAT(DISTINCT agent_id) as agents
|
|
17
|
+
FROM file_activity
|
|
18
|
+
WHERE created_at > datetime('now', '-' || ? || ' minutes')
|
|
19
|
+
GROUP BY file_path
|
|
20
|
+
HAVING COUNT(DISTINCT agent_id) > 1
|
|
21
21
|
ORDER BY agent_count DESC`).all(sinceMinutes);
|
|
22
22
|
return rows.map((r) => ({
|
|
23
23
|
file_path: r.file_path,
|
|
@@ -27,8 +27,8 @@ export class FileTracker {
|
|
|
27
27
|
}
|
|
28
28
|
checkFileConflict(filePath, agentId, withinMinutes = 30) {
|
|
29
29
|
const db = getDb();
|
|
30
|
-
const rows = db.prepare(`SELECT DISTINCT agent_id FROM file_activity
|
|
31
|
-
WHERE file_path = ? AND agent_id != ?
|
|
30
|
+
const rows = db.prepare(`SELECT DISTINCT agent_id FROM file_activity
|
|
31
|
+
WHERE file_path = ? AND agent_id != ?
|
|
32
32
|
AND created_at > datetime('now', '-' || ? || ' minutes')`).all(filePath, agentId, withinMinutes);
|
|
33
33
|
return { conflict: rows.length > 0, agents: rows.map((r) => r.agent_id) };
|
|
34
34
|
}
|
|
@@ -50,9 +50,9 @@ export class FileTracker {
|
|
|
50
50
|
// the impact scorer only passes target_files + depends_on_files (typically
|
|
51
51
|
// a handful of files per announce_work call).
|
|
52
52
|
const placeholders = filePaths.map(() => "?").join(",");
|
|
53
|
-
const rows = db.prepare(`SELECT DISTINCT file_path, agent_id FROM file_activity
|
|
54
|
-
WHERE file_path IN (${placeholders})
|
|
55
|
-
AND agent_id != ?
|
|
53
|
+
const rows = db.prepare(`SELECT DISTINCT file_path, agent_id FROM file_activity
|
|
54
|
+
WHERE file_path IN (${placeholders})
|
|
55
|
+
AND agent_id != ?
|
|
56
56
|
AND created_at > datetime('now', '-' || ? || ' minutes')`).all(...filePaths, excludeAgentId, withinMinutes);
|
|
57
57
|
for (const r of rows) {
|
|
58
58
|
let set = index.get(r.file_path);
|
|
@@ -3,6 +3,7 @@ import { getDb } from "../database.js";
|
|
|
3
3
|
import { runCommonAnnounceFlow } from "../announce-workflow.js";
|
|
4
4
|
import { canResetDb } from "../reset-guard.js";
|
|
5
5
|
import { parseBody, json } from "./utils.js";
|
|
6
|
+
import { normalizePath } from "../path-normalize.js";
|
|
6
7
|
export async function handleRest(req, res, ctx) {
|
|
7
8
|
const { services, httpLog, authEnabled, getRunConfig, setRunConfig } = ctx;
|
|
8
9
|
const url = req.url || "";
|
|
@@ -378,6 +379,15 @@ export async function handleRest(req, res, ctx) {
|
|
|
378
379
|
json(res, { error: "agent_name must be string when present" }, 400);
|
|
379
380
|
return;
|
|
380
381
|
}
|
|
382
|
+
const repoRoot = process.env.COORDINATOR_REPO_ROOT || null;
|
|
383
|
+
let filePath;
|
|
384
|
+
try {
|
|
385
|
+
filePath = normalizePath(repoRoot, body.file_path);
|
|
386
|
+
}
|
|
387
|
+
catch (err) {
|
|
388
|
+
json(res, { error: `invalid file_path: ${err.message}` }, 400);
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
381
391
|
const MAX_CONTENT = 262144;
|
|
382
392
|
let symbols = null;
|
|
383
393
|
let contentHash = null;
|
|
@@ -387,14 +397,14 @@ export async function handleRest(req, res, ctx) {
|
|
|
387
397
|
return;
|
|
388
398
|
}
|
|
389
399
|
contentHash = createHash("sha256").update(body.content).digest("hex");
|
|
390
|
-
symbols = ctx.services.treeSitter.extract(
|
|
400
|
+
symbols = ctx.services.treeSitter.extract(filePath, body.content, null);
|
|
391
401
|
}
|
|
392
402
|
ctx.services.fileTracker.log({
|
|
393
403
|
session_id: body.session_id,
|
|
394
404
|
agent_id: body.agent_id,
|
|
395
405
|
agent_name: body.agent_name,
|
|
396
406
|
tool_name: body.tool_name,
|
|
397
|
-
file_path:
|
|
407
|
+
file_path: filePath,
|
|
398
408
|
content_hash: contentHash,
|
|
399
409
|
symbols_touched: symbols,
|
|
400
410
|
});
|
|
@@ -405,8 +415,17 @@ export async function handleRest(req, res, ctx) {
|
|
|
405
415
|
json(res, { error: "agent_id and file_path required" }, 400);
|
|
406
416
|
return;
|
|
407
417
|
}
|
|
418
|
+
const repoRoot = process.env.COORDINATOR_REPO_ROOT || null;
|
|
419
|
+
let filePath;
|
|
420
|
+
try {
|
|
421
|
+
filePath = normalizePath(repoRoot, body.file_path);
|
|
422
|
+
}
|
|
423
|
+
catch (err) {
|
|
424
|
+
json(res, { error: `invalid file_path: ${err.message}` }, 400);
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
408
427
|
const ttl = parseInt(process.env.COORDINATOR_WORKING_FILES_TTL_MIN || "30", 10);
|
|
409
|
-
services.workingFiles.start(body.agent_id,
|
|
428
|
+
services.workingFiles.start(body.agent_id, filePath, ttl);
|
|
410
429
|
json(res, { ok: true });
|
|
411
430
|
}
|
|
412
431
|
else if (url === "/api/working-files/stop" && req.method === "POST") {
|
|
@@ -414,7 +433,16 @@ export async function handleRest(req, res, ctx) {
|
|
|
414
433
|
json(res, { error: "agent_id and file_path required" }, 400);
|
|
415
434
|
return;
|
|
416
435
|
}
|
|
417
|
-
|
|
436
|
+
const repoRoot = process.env.COORDINATOR_REPO_ROOT || null;
|
|
437
|
+
let filePath;
|
|
438
|
+
try {
|
|
439
|
+
filePath = normalizePath(repoRoot, body.file_path);
|
|
440
|
+
}
|
|
441
|
+
catch (err) {
|
|
442
|
+
json(res, { error: `invalid file_path: ${err.message}` }, 400);
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
services.workingFiles.stop(body.agent_id, filePath);
|
|
418
446
|
json(res, { ok: true });
|
|
419
447
|
}
|
|
420
448
|
else if (url?.startsWith("/api/scoring-stats") && req.method === "GET") {
|
|
@@ -424,18 +452,33 @@ export async function handleRest(req, res, ctx) {
|
|
|
424
452
|
: sinceParam.endsWith("d") ? parseInt(sinceParam) * 60 * 24
|
|
425
453
|
: 60 * 24;
|
|
426
454
|
const db = getDb();
|
|
427
|
-
const
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
455
|
+
const rows = db.prepare(`SELECT
|
|
456
|
+
lf.layer,
|
|
457
|
+
COUNT(*) AS fire_count,
|
|
458
|
+
AVG(lf.score) AS avg_score,
|
|
459
|
+
SUM(CASE WHEN json_extract(e.payload, '$.resolution_type') = 'auto_resolved' THEN 1 ELSE 0 END) AS auto_resolved,
|
|
460
|
+
SUM(CASE WHEN json_extract(e.payload, '$.resolution_type') = 'consensus' THEN 1 ELSE 0 END) AS consensus,
|
|
461
|
+
SUM(CASE WHEN json_extract(e.payload, '$.resolution_type') = 'timeout' THEN 1 ELSE 0 END) AS timeout_count,
|
|
462
|
+
SUM(CASE WHEN json_extract(e.payload, '$.resolution_type') IN ('agent_departure','closed') THEN 1 ELSE 0 END) AS cancelled
|
|
463
|
+
FROM layer_firings lf
|
|
464
|
+
LEFT JOIN events e
|
|
465
|
+
ON e.type = 'thread_resolved'
|
|
466
|
+
AND json_extract(e.payload, '$.thread_id') = lf.thread_id
|
|
467
|
+
WHERE lf.fired_at > datetime('now', '-' || ? || ' minutes')
|
|
468
|
+
GROUP BY lf.layer
|
|
431
469
|
ORDER BY fire_count DESC`).all(sinceMin);
|
|
432
470
|
json(res, {
|
|
433
471
|
window: { since: sinceParam, now: new Date().toISOString() },
|
|
434
|
-
layers:
|
|
435
|
-
layer:
|
|
436
|
-
fire_count:
|
|
437
|
-
avg_score:
|
|
438
|
-
outcomes: {
|
|
472
|
+
layers: rows.map(r => ({
|
|
473
|
+
layer: r.layer,
|
|
474
|
+
fire_count: r.fire_count,
|
|
475
|
+
avg_score: r.avg_score,
|
|
476
|
+
outcomes: {
|
|
477
|
+
auto_resolved: r.auto_resolved,
|
|
478
|
+
consensus: r.consensus,
|
|
479
|
+
timeout: r.timeout_count,
|
|
480
|
+
cancelled: r.cancelled,
|
|
481
|
+
},
|
|
439
482
|
})),
|
|
440
483
|
});
|
|
441
484
|
}
|