mcp-coordinator 0.4.0 → 0.6.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 +938 -846
- package/dashboard/Dockerfile +19 -19
- package/dashboard/public/index.html +1201 -1178
- package/dist/cli/server/start.js +33 -0
- package/dist/src/agent-activity.js +6 -6
- package/dist/src/agent-registry.js +6 -6
- package/dist/src/announce-workflow.d.ts +1 -0
- package/dist/src/announce-workflow.js +28 -0
- package/dist/src/consultation.js +20 -20
- package/dist/src/database.js +191 -126
- package/dist/src/dependency-map.js +3 -3
- package/dist/src/file-tracker.d.ts +2 -0
- package/dist/src/file-tracker.js +13 -12
- package/dist/src/git-cochange-builder.d.ts +32 -0
- package/dist/src/git-cochange-builder.js +238 -0
- package/dist/src/http/handle-health.d.ts +1 -1
- package/dist/src/http/handle-health.js +26 -0
- package/dist/src/http/handle-rest.js +98 -2
- package/dist/src/http/utils.d.ts +0 -4
- package/dist/src/http/utils.js +16 -2
- package/dist/src/impact-scorer.d.ts +5 -1
- package/dist/src/impact-scorer.js +98 -8
- package/dist/src/introspection.js +1 -1
- package/dist/src/metrics.d.ts +5 -0
- package/dist/src/metrics.js +33 -0
- package/dist/src/path-normalize.d.ts +17 -0
- package/dist/src/path-normalize.js +38 -0
- package/dist/src/serve-http.js +41 -2
- package/dist/src/server-setup.d.ts +6 -0
- package/dist/src/server-setup.js +23 -3
- package/dist/src/tools/consultation-tools.js +4 -2
- package/dist/src/tree-sitter-extractor.d.ts +36 -0
- package/dist/src/tree-sitter-extractor.js +354 -0
- package/dist/src/working-files-tracker.d.ts +42 -0
- package/dist/src/working-files-tracker.js +111 -0
- package/package.json +100 -83
package/dist/cli/server/start.js
CHANGED
|
@@ -8,7 +8,32 @@ export function createServerStartCommand() {
|
|
|
8
8
|
.option("--port <port>", "Server port")
|
|
9
9
|
.option("--data-dir <path>", "Data directory")
|
|
10
10
|
.option("--daemon", "Run as background daemon")
|
|
11
|
+
.option("--repo-root <path>", "Project repo root (enables Layer 4 + FS fallback). Default env COORDINATOR_REPO_ROOT.")
|
|
12
|
+
.option("--max-body-bytes <bytes>", "Max HTTP request body in bytes. Default 1048576.")
|
|
13
|
+
.option("--working-files-ttl-min <minutes>", "TTL for working_files claims. Default 30.")
|
|
14
|
+
.option("--working-files-sweep-ms <ms>", "TTL sweeper tick interval. Default 60000.")
|
|
15
|
+
.option("--layer4-since-days <days>", "git log --since window. Default 7.")
|
|
16
|
+
.option("--layer4-max-commits <count>", "git log --max-count. Default 2000.")
|
|
17
|
+
.option("--layer4-refresh-ms <ms>", "Layer 4 successful-build refresh interval. Default 1800000.")
|
|
18
|
+
.option("--layer4-retry-ms <ms>", "Layer 4 retry interval on timeout. Default 300000.")
|
|
11
19
|
.action(async (opts) => {
|
|
20
|
+
// Wire CLI flags to env vars (CLI takes precedence; rest of codebase reads from env)
|
|
21
|
+
if (opts.repoRoot)
|
|
22
|
+
process.env.COORDINATOR_REPO_ROOT = opts.repoRoot;
|
|
23
|
+
if (opts.maxBodyBytes)
|
|
24
|
+
process.env.COORDINATOR_MAX_BODY_BYTES = opts.maxBodyBytes;
|
|
25
|
+
if (opts.workingFilesTtlMin)
|
|
26
|
+
process.env.COORDINATOR_WORKING_FILES_TTL_MIN = opts.workingFilesTtlMin;
|
|
27
|
+
if (opts.workingFilesSweepMs)
|
|
28
|
+
process.env.COORDINATOR_WORKING_FILES_SWEEP_INTERVAL_MS = opts.workingFilesSweepMs;
|
|
29
|
+
if (opts.layer4SinceDays)
|
|
30
|
+
process.env.COORDINATOR_LAYER4_SINCE_DAYS = opts.layer4SinceDays;
|
|
31
|
+
if (opts.layer4MaxCommits)
|
|
32
|
+
process.env.COORDINATOR_LAYER4_MAX_COMMITS = opts.layer4MaxCommits;
|
|
33
|
+
if (opts.layer4RefreshMs)
|
|
34
|
+
process.env.COORDINATOR_LAYER4_REFRESH_INTERVAL_MS = opts.layer4RefreshMs;
|
|
35
|
+
if (opts.layer4RetryMs)
|
|
36
|
+
process.env.COORDINATOR_LAYER4_RETRY_MS = opts.layer4RetryMs;
|
|
12
37
|
const config = loadConfig();
|
|
13
38
|
const port = parseInt(opts.port ?? process.env.PORT ?? String(config.server.port), 10);
|
|
14
39
|
const dataDir = opts.dataDir ?? process.env.COORDINATOR_DATA_DIR ?? config.server.data_dir;
|
|
@@ -47,6 +72,14 @@ export function createServerStartCommand() {
|
|
|
47
72
|
fwd("COORDINATOR_ADMIN_SECRET", process.env.COORDINATOR_ADMIN_SECRET);
|
|
48
73
|
fwd("COORDINATOR_MQTT_TCP_PORT", process.env.COORDINATOR_MQTT_TCP_PORT);
|
|
49
74
|
fwd("COORDINATOR_MQTT_WS_PATH", process.env.COORDINATOR_MQTT_WS_PATH);
|
|
75
|
+
fwd("COORDINATOR_REPO_ROOT", process.env.COORDINATOR_REPO_ROOT);
|
|
76
|
+
fwd("COORDINATOR_MAX_BODY_BYTES", process.env.COORDINATOR_MAX_BODY_BYTES);
|
|
77
|
+
fwd("COORDINATOR_WORKING_FILES_TTL_MIN", process.env.COORDINATOR_WORKING_FILES_TTL_MIN);
|
|
78
|
+
fwd("COORDINATOR_WORKING_FILES_SWEEP_INTERVAL_MS", process.env.COORDINATOR_WORKING_FILES_SWEEP_INTERVAL_MS);
|
|
79
|
+
fwd("COORDINATOR_LAYER4_SINCE_DAYS", process.env.COORDINATOR_LAYER4_SINCE_DAYS);
|
|
80
|
+
fwd("COORDINATOR_LAYER4_MAX_COMMITS", process.env.COORDINATOR_LAYER4_MAX_COMMITS);
|
|
81
|
+
fwd("COORDINATOR_LAYER4_REFRESH_INTERVAL_MS", process.env.COORDINATOR_LAYER4_REFRESH_INTERVAL_MS);
|
|
82
|
+
fwd("COORDINATOR_LAYER4_RETRY_MS", process.env.COORDINATOR_LAYER4_RETRY_MS);
|
|
50
83
|
const child = spawn(cmd, args, {
|
|
51
84
|
detached: true,
|
|
52
85
|
stdio: ["ignore", logFd, logFd],
|
|
@@ -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
|
}
|
|
@@ -15,7 +15,16 @@ export function runCommonAnnounceFlow(services, threadId, params) {
|
|
|
15
15
|
target_files: params.target_files,
|
|
16
16
|
depends_on_files: params.depends_on_files,
|
|
17
17
|
exports_affected: params.exports_affected,
|
|
18
|
+
target_symbols: params.target_symbols,
|
|
18
19
|
});
|
|
20
|
+
// Layer firing log: one row per concerned/gray-zone scored agent.
|
|
21
|
+
// Used by /api/scoring-stats and the dashboard "Conflict signals" panel.
|
|
22
|
+
const dbForFirings = getDb();
|
|
23
|
+
const insertFiring = dbForFirings.prepare("INSERT INTO layer_firings (thread_id, layer, score, agent_id) VALUES (?, ?, ?, ?)");
|
|
24
|
+
for (const s of [...categorized.concerned, ...categorized.gray_zone]) {
|
|
25
|
+
const layer = inferLayerFromReasons(s.reasons);
|
|
26
|
+
insertFiring.run(threadId, layer, s.score, s.agent_id);
|
|
27
|
+
}
|
|
19
28
|
// 2. Override expected_respondents on the thread with the scored set.
|
|
20
29
|
// Auto-resolve only when truly alone — if peers are online but not concerned
|
|
21
30
|
// (e.g., they haven't announced yet), keep the thread open so a subsequent
|
|
@@ -72,6 +81,25 @@ export function runCommonAnnounceFlow(services, threadId, params) {
|
|
|
72
81
|
const respondents = JSON.parse(updated.expected_respondents || "[]");
|
|
73
82
|
return { updated, categorized, respondents, planQuality };
|
|
74
83
|
}
|
|
84
|
+
function inferLayerFromReasons(reasons) {
|
|
85
|
+
for (const r of reasons) {
|
|
86
|
+
if (r.includes("disjoint symbols"))
|
|
87
|
+
return "L0.5";
|
|
88
|
+
if (r.includes("announced same file") || r.includes("modifies my dependency") || r.includes("they depend on my target"))
|
|
89
|
+
return "L0";
|
|
90
|
+
if (r.includes("same file (in flight)"))
|
|
91
|
+
return "L1";
|
|
92
|
+
if (r.includes("same file"))
|
|
93
|
+
return "L1";
|
|
94
|
+
if (r.includes("co-change"))
|
|
95
|
+
return "L4";
|
|
96
|
+
if (r.includes("depends on"))
|
|
97
|
+
return "L2";
|
|
98
|
+
if (r.includes("module overlap"))
|
|
99
|
+
return "L3";
|
|
100
|
+
}
|
|
101
|
+
return "L1";
|
|
102
|
+
}
|
|
75
103
|
function scoredCategory(s) {
|
|
76
104
|
if (s.score >= 90)
|
|
77
105
|
return "concerned";
|
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
|
@@ -3,132 +3,172 @@ import { mkdirSync } from "fs";
|
|
|
3
3
|
import { createRequire } from "module";
|
|
4
4
|
const require = createRequire(import.meta.url);
|
|
5
5
|
let db;
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
FOREIGN KEY (
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
CREATE INDEX IF NOT EXISTS
|
|
92
|
-
CREATE INDEX IF NOT EXISTS
|
|
93
|
-
CREATE INDEX IF NOT EXISTS
|
|
94
|
-
CREATE INDEX IF NOT EXISTS
|
|
95
|
-
CREATE INDEX IF NOT EXISTS
|
|
96
|
-
CREATE INDEX IF NOT EXISTS
|
|
97
|
-
CREATE INDEX IF NOT EXISTS
|
|
98
|
-
CREATE INDEX IF NOT EXISTS
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
FOREIGN KEY (
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
CREATE INDEX IF NOT EXISTS
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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);
|
|
132
172
|
`;
|
|
133
173
|
function createBetterSqlite3(dataDir) {
|
|
134
174
|
mkdirSync(dataDir, { recursive: true });
|
|
@@ -157,6 +197,20 @@ export function initDatabase(dataDir) {
|
|
|
157
197
|
else {
|
|
158
198
|
db = createBetterSqlite3(dataDir);
|
|
159
199
|
}
|
|
200
|
+
// Check for downgrade: refuse if DB was written by a newer binary
|
|
201
|
+
let foundVersion = 0;
|
|
202
|
+
try {
|
|
203
|
+
const v = db
|
|
204
|
+
.prepare("PRAGMA user_version")
|
|
205
|
+
.get();
|
|
206
|
+
foundVersion = v?.user_version ?? 0;
|
|
207
|
+
}
|
|
208
|
+
catch {
|
|
209
|
+
foundVersion = 0;
|
|
210
|
+
}
|
|
211
|
+
if (foundVersion > CURRENT_USER_VERSION) {
|
|
212
|
+
throw new Error(`Database schema is from a newer version (${foundVersion}) than this binary supports (${CURRENT_USER_VERSION}). Downgrade not supported.`);
|
|
213
|
+
}
|
|
160
214
|
db.exec(SCHEMA);
|
|
161
215
|
// Migrations for existing databases — columns may already exist
|
|
162
216
|
try {
|
|
@@ -182,6 +236,17 @@ export function initDatabase(dataDir) {
|
|
|
182
236
|
db.exec("ALTER TABLE threads ADD COLUMN assigned_to TEXT");
|
|
183
237
|
}
|
|
184
238
|
catch { /* already exists */ }
|
|
239
|
+
// v0.6: per-edit symbol metadata on file_activity
|
|
240
|
+
try {
|
|
241
|
+
db.exec("ALTER TABLE file_activity ADD COLUMN symbols_touched TEXT");
|
|
242
|
+
}
|
|
243
|
+
catch { /* already exists */ }
|
|
244
|
+
try {
|
|
245
|
+
db.exec("ALTER TABLE file_activity ADD COLUMN content_hash TEXT");
|
|
246
|
+
}
|
|
247
|
+
catch { /* already exists */ }
|
|
248
|
+
// v0.6: schema version marker. Used by cli/server/restore.ts to refuse downgrades.
|
|
249
|
+
db.exec("PRAGMA user_version = 6");
|
|
185
250
|
}
|
|
186
251
|
export function getDb() {
|
|
187
252
|
if (!db)
|
|
@@ -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)) {
|
|
@@ -6,6 +6,8 @@ export declare class FileTracker {
|
|
|
6
6
|
agent_name?: string;
|
|
7
7
|
tool_name: string;
|
|
8
8
|
file_path: string;
|
|
9
|
+
content_hash?: string | null;
|
|
10
|
+
symbols_touched?: string[] | null;
|
|
9
11
|
}): void;
|
|
10
12
|
getBySession(sessionId: string): FileActivity[];
|
|
11
13
|
getHotFiles(sinceMinutes?: number): {
|
package/dist/src/file-tracker.js
CHANGED
|
@@ -3,8 +3,9 @@ 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
|
-
|
|
6
|
+
db.prepare(`INSERT INTO file_activity
|
|
7
|
+
(session_id, agent_id, agent_name, tool_name, file_path, module, content_hash, symbols_touched)
|
|
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);
|
|
8
9
|
}
|
|
9
10
|
getBySession(sessionId) {
|
|
10
11
|
const db = getDb();
|
|
@@ -12,11 +13,11 @@ export class FileTracker {
|
|
|
12
13
|
}
|
|
13
14
|
getHotFiles(sinceMinutes = 30) {
|
|
14
15
|
const db = getDb();
|
|
15
|
-
const rows = db.prepare(`SELECT file_path, COUNT(DISTINCT agent_id) as agent_count, GROUP_CONCAT(DISTINCT agent_id) as agents
|
|
16
|
-
FROM file_activity
|
|
17
|
-
WHERE created_at > datetime('now', '-' || ? || ' minutes')
|
|
18
|
-
GROUP BY file_path
|
|
19
|
-
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
|
|
20
21
|
ORDER BY agent_count DESC`).all(sinceMinutes);
|
|
21
22
|
return rows.map((r) => ({
|
|
22
23
|
file_path: r.file_path,
|
|
@@ -26,8 +27,8 @@ export class FileTracker {
|
|
|
26
27
|
}
|
|
27
28
|
checkFileConflict(filePath, agentId, withinMinutes = 30) {
|
|
28
29
|
const db = getDb();
|
|
29
|
-
const rows = db.prepare(`SELECT DISTINCT agent_id FROM file_activity
|
|
30
|
-
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 != ?
|
|
31
32
|
AND created_at > datetime('now', '-' || ? || ' minutes')`).all(filePath, agentId, withinMinutes);
|
|
32
33
|
return { conflict: rows.length > 0, agents: rows.map((r) => r.agent_id) };
|
|
33
34
|
}
|
|
@@ -49,9 +50,9 @@ export class FileTracker {
|
|
|
49
50
|
// the impact scorer only passes target_files + depends_on_files (typically
|
|
50
51
|
// a handful of files per announce_work call).
|
|
51
52
|
const placeholders = filePaths.map(() => "?").join(",");
|
|
52
|
-
const rows = db.prepare(`SELECT DISTINCT file_path, agent_id FROM file_activity
|
|
53
|
-
WHERE file_path IN (${placeholders})
|
|
54
|
-
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 != ?
|
|
55
56
|
AND created_at > datetime('now', '-' || ? || ' minutes')`).all(...filePaths, excludeAgentId, withinMinutes);
|
|
56
57
|
for (const r of rows) {
|
|
57
58
|
let set = index.get(r.file_path);
|