code-graph-context 2.7.0 → 2.9.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 +12 -22
- package/dist/mcp/constants.js +40 -8
- package/dist/mcp/handlers/graph-generator.handler.js +3 -0
- package/dist/mcp/tools/index.js +6 -3
- package/dist/mcp/tools/session-bookmark.tool.js +31 -10
- package/dist/mcp/tools/session-cleanup.tool.js +139 -0
- package/dist/mcp/tools/swarm-claim-task.tool.js +35 -0
- package/dist/mcp/tools/swarm-cleanup.tool.js +55 -3
- package/dist/mcp/tools/swarm-constants.js +28 -0
- package/dist/mcp/tools/swarm-message.tool.js +362 -0
- package/dist/storage/neo4j/neo4j.service.js +4 -0
- package/package.json +1 -1
- package/dist/mcp/tools/swarm-orchestrate.tool.js +0 -471
package/README.md
CHANGED
|
@@ -345,7 +345,6 @@ Explicit task management with dependencies:
|
|
|
345
345
|
|
|
346
346
|
| Tool | Purpose |
|
|
347
347
|
|------|---------|
|
|
348
|
-
| `swarm_orchestrate` | Decompose a task and spawn worker agents |
|
|
349
348
|
| `swarm_post_task` | Add a task to the queue |
|
|
350
349
|
| `swarm_get_tasks` | Query tasks with filters |
|
|
351
350
|
| `swarm_claim_task` | Claim/start/release a task |
|
|
@@ -357,28 +356,20 @@ Explicit task management with dependencies:
|
|
|
357
356
|
### Example: Parallel Refactoring
|
|
358
357
|
|
|
359
358
|
```typescript
|
|
360
|
-
// Orchestrator decomposes and creates
|
|
361
|
-
|
|
359
|
+
// Orchestrator decomposes the task and creates individual work items
|
|
360
|
+
swarm_post_task({
|
|
362
361
|
projectId: "backend",
|
|
363
|
-
|
|
364
|
-
|
|
362
|
+
swarmId: "swarm_rename_user",
|
|
363
|
+
title: "Update UserService.findUserById",
|
|
364
|
+
description: "Rename getUserById to findUserById in UserService",
|
|
365
|
+
type: "refactor",
|
|
366
|
+
createdBy: "orchestrator"
|
|
365
367
|
})
|
|
366
368
|
|
|
367
|
-
//
|
|
368
|
-
{
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
totalTasks: 12,
|
|
372
|
-
parallelizable: 8,
|
|
373
|
-
sequential: 4, // These have dependencies
|
|
374
|
-
tasks: [
|
|
375
|
-
{ id: "task_1", title: "Update UserService.findUserById", status: "available" },
|
|
376
|
-
{ id: "task_2", title: "Update UserController references", status: "blocked", depends: ["task_1"] },
|
|
377
|
-
...
|
|
378
|
-
]
|
|
379
|
-
},
|
|
380
|
-
workerInstructions: "..." // Copy-paste to spawn workers
|
|
381
|
-
}
|
|
369
|
+
// Workers claim and execute tasks
|
|
370
|
+
swarm_claim_task({ projectId: "backend", swarmId: "swarm_rename_user", agentId: "worker_1" })
|
|
371
|
+
// ... do work ...
|
|
372
|
+
swarm_complete_task({ taskId: "task_1", agentId: "worker_1", action: "complete", summary: "Renamed method" })
|
|
382
373
|
```
|
|
383
374
|
|
|
384
375
|
### Install the Swarm Skill
|
|
@@ -428,7 +419,6 @@ See [`skills/swarm/SKILL.md`](skills/swarm/SKILL.md) for the full documentation.
|
|
|
428
419
|
| `stop_watch_project` | Stop file watching |
|
|
429
420
|
| `list_watchers` | List active file watchers |
|
|
430
421
|
| **Swarm** | |
|
|
431
|
-
| `swarm_orchestrate` | Plan and spawn parallel agents |
|
|
432
422
|
| `swarm_post_task` | Add task to the queue |
|
|
433
423
|
| `swarm_get_tasks` | Query tasks |
|
|
434
424
|
| `swarm_claim_task` | Claim/start/release tasks |
|
|
@@ -458,7 +448,7 @@ detect_dead_code → detect_duplicate_code → prioritize cleanup
|
|
|
458
448
|
|
|
459
449
|
**Pattern 4: Multi-Agent Work**
|
|
460
450
|
```
|
|
461
|
-
|
|
451
|
+
swarm_post_task → swarm_claim_task → swarm_complete_task → swarm_get_tasks(includeStats) → swarm_cleanup
|
|
462
452
|
```
|
|
463
453
|
|
|
464
454
|
### Multi-Project Support
|
package/dist/mcp/constants.js
CHANGED
|
@@ -38,11 +38,12 @@ export const TOOL_NAMES = {
|
|
|
38
38
|
swarmClaimTask: 'swarm_claim_task',
|
|
39
39
|
swarmCompleteTask: 'swarm_complete_task',
|
|
40
40
|
swarmGetTasks: 'swarm_get_tasks',
|
|
41
|
-
swarmOrchestrate: 'swarm_orchestrate',
|
|
42
41
|
saveSessionBookmark: 'save_session_bookmark',
|
|
43
42
|
restoreSessionBookmark: 'restore_session_bookmark',
|
|
44
43
|
saveSessionNote: 'save_session_note',
|
|
45
44
|
recallSessionNotes: 'recall_session_notes',
|
|
45
|
+
cleanupSession: 'cleanup_session',
|
|
46
|
+
swarmMessage: 'swarm_message',
|
|
46
47
|
};
|
|
47
48
|
// Tool Metadata
|
|
48
49
|
export const TOOL_METADATA = {
|
|
@@ -221,7 +222,7 @@ Returns pheromones with current intensity after decay. minIntensity default: 0.3
|
|
|
221
222
|
},
|
|
222
223
|
[TOOL_NAMES.swarmCleanup]: {
|
|
223
224
|
title: 'Swarm Cleanup',
|
|
224
|
-
description: `Bulk delete pheromones. Specify swarmId, agentId, or all:true. Warning pheromones preserved by default (override with keepTypes:[]). Use dryRun:true to preview.`,
|
|
225
|
+
description: `Bulk delete pheromones, tasks, and messages. Specify swarmId, agentId, or all:true. Warning pheromones preserved by default (override with keepTypes:[]). Messages and tasks are deleted when swarmId is provided. Use dryRun:true to preview.`,
|
|
225
226
|
},
|
|
226
227
|
[TOOL_NAMES.swarmPostTask]: {
|
|
227
228
|
title: 'Swarm Post Task',
|
|
@@ -258,12 +259,6 @@ Complete unblocks dependent tasks. Failed tasks can be retried if retryable=true
|
|
|
258
259
|
description: `Query tasks with filters. Use taskId for single task, or filter by swarmId, statuses, types, claimedBy, createdBy, minPriority.
|
|
259
260
|
|
|
260
261
|
Sort by: priority (default), created, updated. Add includeStats:true for aggregate counts.`,
|
|
261
|
-
},
|
|
262
|
-
[TOOL_NAMES.swarmOrchestrate]: {
|
|
263
|
-
title: 'Swarm Orchestrate',
|
|
264
|
-
description: `Coordinate multiple agents for complex multi-file tasks. Analyzes codebase, decomposes into atomic tasks, spawns workers, monitors progress.
|
|
265
|
-
|
|
266
|
-
Use dryRun:true to preview plan. maxAgents controls parallelism (default: 3). Failed tasks auto-retry via pheromone decay.`,
|
|
267
262
|
},
|
|
268
263
|
[TOOL_NAMES.saveSessionBookmark]: {
|
|
269
264
|
title: 'Save Session Bookmark',
|
|
@@ -334,6 +329,43 @@ Parameters:
|
|
|
334
329
|
|
|
335
330
|
Returns notes with topic, content, category, severity, relevance score (vector mode), and linked aboutNodes.`,
|
|
336
331
|
},
|
|
332
|
+
[TOOL_NAMES.cleanupSession]: {
|
|
333
|
+
title: 'Cleanup Session',
|
|
334
|
+
description: `Clean up expired session notes and old session bookmarks.
|
|
335
|
+
|
|
336
|
+
Removes:
|
|
337
|
+
- Expired SessionNote nodes (past expiresAt) and their edges
|
|
338
|
+
- Old SessionBookmark nodes, keeping only the most recent N per session (default: 3)
|
|
339
|
+
|
|
340
|
+
Parameters:
|
|
341
|
+
- projectId (required): Project ID, name, or path
|
|
342
|
+
- keepBookmarks (default: 3): Number of most recent bookmarks to keep per session
|
|
343
|
+
- dryRun (default: false): Preview what would be deleted without deleting
|
|
344
|
+
|
|
345
|
+
Returns counts of deleted notes, bookmarks, and edges.`,
|
|
346
|
+
},
|
|
347
|
+
[TOOL_NAMES.swarmMessage]: {
|
|
348
|
+
title: 'Swarm Message',
|
|
349
|
+
description: `Direct agent-to-agent messaging via Neo4j. Unlike pheromones (passive/decay-based), messages are explicit and delivered when agents claim tasks.
|
|
350
|
+
|
|
351
|
+
**Actions:**
|
|
352
|
+
- send: Post a message to a specific agent or broadcast to all agents in a swarm
|
|
353
|
+
- read: Retrieve unread messages (or all messages) for an agent
|
|
354
|
+
- acknowledge: Mark messages as read
|
|
355
|
+
|
|
356
|
+
**Categories:** blocked, conflict, finding, request, alert, handoff
|
|
357
|
+
|
|
358
|
+
**Key behavior:**
|
|
359
|
+
- Messages sent to a specific agent are delivered when that agent calls swarm_claim_task
|
|
360
|
+
- Broadcast messages (toAgentId omitted) are visible to all agents in the swarm
|
|
361
|
+
- Messages auto-expire after 4 hours (configurable via ttlMs)
|
|
362
|
+
- Use this for critical coordination signals that agents MUST see, not optional context
|
|
363
|
+
|
|
364
|
+
**Examples:**
|
|
365
|
+
- Agent finds a breaking type error: send alert to all
|
|
366
|
+
- Agent is blocked on a file another agent owns: send blocked to that agent
|
|
367
|
+
- Agent discovers context another agent needs: send finding to that agent`,
|
|
368
|
+
},
|
|
337
369
|
};
|
|
338
370
|
// Default Values
|
|
339
371
|
export const DEFAULTS = {
|
|
@@ -74,6 +74,9 @@ export class GraphGeneratorHandler {
|
|
|
74
74
|
await this.neo4jService.run(QUERIES.CREATE_PROJECT_ID_INDEX_EMBEDDED);
|
|
75
75
|
await this.neo4jService.run(QUERIES.CREATE_PROJECT_ID_INDEX_SOURCEFILE);
|
|
76
76
|
await this.neo4jService.run(QUERIES.CREATE_NORMALIZED_HASH_INDEX);
|
|
77
|
+
await this.neo4jService.run(QUERIES.CREATE_SESSION_BOOKMARK_INDEX);
|
|
78
|
+
await this.neo4jService.run(QUERIES.CREATE_SESSION_NOTE_INDEX);
|
|
79
|
+
await this.neo4jService.run(QUERIES.CREATE_SESSION_NOTE_CATEGORY_INDEX);
|
|
77
80
|
await debugLog('Project indexes created');
|
|
78
81
|
}
|
|
79
82
|
async importNodes(nodes, batchSize) {
|
package/dist/mcp/tools/index.js
CHANGED
|
@@ -13,6 +13,7 @@ import { createNaturalLanguageToCypherTool } from './natural-language-to-cypher.
|
|
|
13
13
|
import { createParseTypescriptProjectTool } from './parse-typescript-project.tool.js';
|
|
14
14
|
import { createSearchCodebaseTool } from './search-codebase.tool.js';
|
|
15
15
|
import { createRestoreSessionBookmarkTool, createSaveSessionBookmarkTool } from './session-bookmark.tool.js';
|
|
16
|
+
import { createCleanupSessionTool } from './session-cleanup.tool.js';
|
|
16
17
|
import { createRecallSessionNotesTool, createSaveSessionNoteTool } from './session-note.tool.js';
|
|
17
18
|
import { createStartWatchProjectTool } from './start-watch-project.tool.js';
|
|
18
19
|
import { createStopWatchProjectTool } from './stop-watch-project.tool.js';
|
|
@@ -20,7 +21,7 @@ import { createSwarmClaimTaskTool } from './swarm-claim-task.tool.js';
|
|
|
20
21
|
import { createSwarmCleanupTool } from './swarm-cleanup.tool.js';
|
|
21
22
|
import { createSwarmCompleteTaskTool } from './swarm-complete-task.tool.js';
|
|
22
23
|
import { createSwarmGetTasksTool } from './swarm-get-tasks.tool.js';
|
|
23
|
-
import {
|
|
24
|
+
import { createSwarmMessageTool } from './swarm-message.tool.js';
|
|
24
25
|
import { createSwarmPheromoneTool } from './swarm-pheromone.tool.js';
|
|
25
26
|
import { createSwarmPostTaskTool } from './swarm-post-task.tool.js';
|
|
26
27
|
import { createSwarmSenseTool } from './swarm-sense.tool.js';
|
|
@@ -73,12 +74,14 @@ export const registerAllTools = (server) => {
|
|
|
73
74
|
createSwarmClaimTaskTool(server);
|
|
74
75
|
createSwarmCompleteTaskTool(server);
|
|
75
76
|
createSwarmGetTasksTool(server);
|
|
76
|
-
// Register swarm
|
|
77
|
-
|
|
77
|
+
// Register swarm messaging tools (direct agent-to-agent communication)
|
|
78
|
+
createSwarmMessageTool(server);
|
|
78
79
|
// Register session bookmark tools (cross-session context continuity)
|
|
79
80
|
createSaveSessionBookmarkTool(server);
|
|
80
81
|
createRestoreSessionBookmarkTool(server);
|
|
81
82
|
// Register session note tools (durable observations and decisions)
|
|
82
83
|
createSaveSessionNoteTool(server);
|
|
83
84
|
createRecallSessionNotesTool(server);
|
|
85
|
+
// Register session cleanup tool
|
|
86
|
+
createCleanupSessionTool(server);
|
|
84
87
|
};
|
|
@@ -96,11 +96,24 @@ const GET_SESSION_NOTES_QUERY = `
|
|
|
96
96
|
MATCH (n:SessionNote)
|
|
97
97
|
WHERE n.projectId = $projectId
|
|
98
98
|
AND n.sessionId = $sessionId
|
|
99
|
+
AND (n.expiresAt IS NULL OR n.expiresAt > timestamp())
|
|
100
|
+
|
|
101
|
+
OPTIONAL MATCH (n)-[:ABOUT]->(codeNode)
|
|
102
|
+
WHERE NOT codeNode:SessionNote
|
|
103
|
+
AND NOT codeNode:SessionBookmark
|
|
104
|
+
AND NOT codeNode:Pheromone
|
|
105
|
+
AND NOT codeNode:SwarmTask
|
|
106
|
+
|
|
99
107
|
RETURN n.id AS id,
|
|
100
|
-
n.
|
|
101
|
-
n.agentId AS agentId,
|
|
108
|
+
n.topic AS topic,
|
|
102
109
|
n.content AS content,
|
|
103
|
-
n.
|
|
110
|
+
n.category AS category,
|
|
111
|
+
n.severity AS severity,
|
|
112
|
+
n.agentId AS agentId,
|
|
113
|
+
n.sessionId AS sessionId,
|
|
114
|
+
n.createdAt AS createdAt,
|
|
115
|
+
n.expiresAt AS expiresAt,
|
|
116
|
+
collect(DISTINCT {id: codeNode.id, name: codeNode.name, filePath: codeNode.filePath}) AS aboutNodes
|
|
104
117
|
ORDER BY n.createdAt ASC
|
|
105
118
|
LIMIT 50
|
|
106
119
|
`;
|
|
@@ -266,13 +279,21 @@ export const createRestoreSessionBookmarkTool = (server) => {
|
|
|
266
279
|
}
|
|
267
280
|
return node;
|
|
268
281
|
});
|
|
269
|
-
const notes = noteRows.map((n) =>
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
282
|
+
const notes = noteRows.map((n) => {
|
|
283
|
+
const aboutNodes = (n.aboutNodes ?? []).filter((node) => node?.id != null);
|
|
284
|
+
return {
|
|
285
|
+
id: n.id,
|
|
286
|
+
topic: n.topic,
|
|
287
|
+
content: n.content,
|
|
288
|
+
category: n.category,
|
|
289
|
+
severity: n.severity,
|
|
290
|
+
agentId: n.agentId,
|
|
291
|
+
sessionId: n.sessionId,
|
|
292
|
+
createdAt: typeof n.createdAt === 'object' && n.createdAt?.toNumber ? n.createdAt.toNumber() : n.createdAt,
|
|
293
|
+
expiresAt: typeof n.expiresAt === 'object' && n.expiresAt?.toNumber ? n.expiresAt.toNumber() : n.expiresAt,
|
|
294
|
+
aboutNodes,
|
|
295
|
+
};
|
|
296
|
+
});
|
|
276
297
|
// Identify working set nodes not found in the graph (stale IDs after re-parse)
|
|
277
298
|
const foundIds = new Set(workingSetRows.map((r) => r.id));
|
|
278
299
|
const storedIds = Array.isArray(bm.workingSetNodeIds) ? bm.workingSetNodeIds : [];
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session Cleanup Tool
|
|
3
|
+
* Remove expired notes and prune old bookmarks
|
|
4
|
+
*/
|
|
5
|
+
import { z } from 'zod';
|
|
6
|
+
import { Neo4jService } from '../../storage/neo4j/neo4j.service.js';
|
|
7
|
+
import { TOOL_NAMES, TOOL_METADATA } from '../constants.js';
|
|
8
|
+
import { createErrorResponse, createSuccessResponse, resolveProjectIdOrError, debugLog } from '../utils.js';
|
|
9
|
+
/**
|
|
10
|
+
* Count expired notes (for dry run)
|
|
11
|
+
*/
|
|
12
|
+
const COUNT_EXPIRED_NOTES_QUERY = `
|
|
13
|
+
MATCH (n:SessionNote)
|
|
14
|
+
WHERE n.projectId = $projectId
|
|
15
|
+
AND n.expiresAt IS NOT NULL
|
|
16
|
+
AND n.expiresAt <= timestamp()
|
|
17
|
+
RETURN count(n) AS count
|
|
18
|
+
`;
|
|
19
|
+
/**
|
|
20
|
+
* Delete expired notes and their edges
|
|
21
|
+
*/
|
|
22
|
+
const DELETE_EXPIRED_NOTES_QUERY = `
|
|
23
|
+
MATCH (n:SessionNote)
|
|
24
|
+
WHERE n.projectId = $projectId
|
|
25
|
+
AND n.expiresAt IS NOT NULL
|
|
26
|
+
AND n.expiresAt <= timestamp()
|
|
27
|
+
WITH collect(n) AS toDelete
|
|
28
|
+
WITH size(toDelete) AS cnt, toDelete
|
|
29
|
+
FOREACH (n IN toDelete | DETACH DELETE n)
|
|
30
|
+
RETURN cnt AS deleted
|
|
31
|
+
`;
|
|
32
|
+
/**
|
|
33
|
+
* Find old bookmarks to prune (keeping N most recent per session)
|
|
34
|
+
*/
|
|
35
|
+
const COUNT_OLD_BOOKMARKS_QUERY = `
|
|
36
|
+
MATCH (b:SessionBookmark)
|
|
37
|
+
WHERE b.projectId = $projectId
|
|
38
|
+
WITH b.sessionId AS sessionId, b
|
|
39
|
+
ORDER BY b.createdAt DESC
|
|
40
|
+
WITH sessionId, collect(b) AS bookmarks
|
|
41
|
+
WHERE size(bookmarks) > $keepBookmarks
|
|
42
|
+
UNWIND bookmarks[$keepBookmarks..] AS old
|
|
43
|
+
RETURN count(old) AS count
|
|
44
|
+
`;
|
|
45
|
+
/**
|
|
46
|
+
* Delete old bookmarks (keeping N most recent per session)
|
|
47
|
+
*/
|
|
48
|
+
const DELETE_OLD_BOOKMARKS_QUERY = `
|
|
49
|
+
MATCH (b:SessionBookmark)
|
|
50
|
+
WHERE b.projectId = $projectId
|
|
51
|
+
WITH b.sessionId AS sessionId, b
|
|
52
|
+
ORDER BY b.createdAt DESC
|
|
53
|
+
WITH sessionId, collect(b) AS bookmarks
|
|
54
|
+
WHERE size(bookmarks) > $keepBookmarks
|
|
55
|
+
WITH reduce(all = [], bs IN collect(bookmarks[$keepBookmarks..]) | all + bs) AS toDelete
|
|
56
|
+
WITH size(toDelete) AS cnt, toDelete
|
|
57
|
+
FOREACH (b IN toDelete | DETACH DELETE b)
|
|
58
|
+
RETURN cnt AS deleted
|
|
59
|
+
`;
|
|
60
|
+
export const createCleanupSessionTool = (server) => {
|
|
61
|
+
server.registerTool(TOOL_NAMES.cleanupSession, {
|
|
62
|
+
title: TOOL_METADATA[TOOL_NAMES.cleanupSession].title,
|
|
63
|
+
description: TOOL_METADATA[TOOL_NAMES.cleanupSession].description,
|
|
64
|
+
inputSchema: {
|
|
65
|
+
projectId: z.string().describe('Project ID, name, or path (e.g., "backend" or "proj_a1b2c3d4e5f6")'),
|
|
66
|
+
keepBookmarks: z
|
|
67
|
+
.number()
|
|
68
|
+
.int()
|
|
69
|
+
.min(1)
|
|
70
|
+
.max(50)
|
|
71
|
+
.optional()
|
|
72
|
+
.default(3)
|
|
73
|
+
.describe('Number of most recent bookmarks to keep per session (default: 3)'),
|
|
74
|
+
dryRun: z
|
|
75
|
+
.boolean()
|
|
76
|
+
.optional()
|
|
77
|
+
.default(false)
|
|
78
|
+
.describe('Preview what would be deleted without deleting (default: false)'),
|
|
79
|
+
},
|
|
80
|
+
}, async ({ projectId, keepBookmarks = 3, dryRun = false }) => {
|
|
81
|
+
const neo4jService = new Neo4jService();
|
|
82
|
+
const projectResult = await resolveProjectIdOrError(projectId, neo4jService);
|
|
83
|
+
if (!projectResult.success) {
|
|
84
|
+
await neo4jService.close();
|
|
85
|
+
return projectResult.error;
|
|
86
|
+
}
|
|
87
|
+
const resolvedProjectId = projectResult.projectId;
|
|
88
|
+
try {
|
|
89
|
+
const params = { projectId: resolvedProjectId, keepBookmarks };
|
|
90
|
+
if (dryRun) {
|
|
91
|
+
const [noteCount, bookmarkCount] = await Promise.all([
|
|
92
|
+
neo4jService.run(COUNT_EXPIRED_NOTES_QUERY, params),
|
|
93
|
+
neo4jService.run(COUNT_OLD_BOOKMARKS_QUERY, params),
|
|
94
|
+
]);
|
|
95
|
+
const expiredNotes = noteCount[0]?.count ?? 0;
|
|
96
|
+
const oldBookmarks = bookmarkCount[0]?.count ?? 0;
|
|
97
|
+
const toNumber = (v) => (typeof v === 'object' && v?.toNumber ? v.toNumber() : v);
|
|
98
|
+
return createSuccessResponse(JSON.stringify({
|
|
99
|
+
dryRun: true,
|
|
100
|
+
projectId: resolvedProjectId,
|
|
101
|
+
wouldDelete: {
|
|
102
|
+
expiredNotes: toNumber(expiredNotes),
|
|
103
|
+
oldBookmarks: toNumber(oldBookmarks),
|
|
104
|
+
},
|
|
105
|
+
keepBookmarks,
|
|
106
|
+
message: toNumber(expiredNotes) === 0 && toNumber(oldBookmarks) === 0
|
|
107
|
+
? 'Nothing to clean up.'
|
|
108
|
+
: `Would delete ${toNumber(expiredNotes)} expired notes and ${toNumber(oldBookmarks)} old bookmarks.`,
|
|
109
|
+
}));
|
|
110
|
+
}
|
|
111
|
+
const [noteResult, bookmarkResult] = await Promise.all([
|
|
112
|
+
neo4jService.run(DELETE_EXPIRED_NOTES_QUERY, params),
|
|
113
|
+
neo4jService.run(DELETE_OLD_BOOKMARKS_QUERY, params),
|
|
114
|
+
]);
|
|
115
|
+
const toNumber = (v) => (typeof v === 'object' && v?.toNumber ? v.toNumber() : v);
|
|
116
|
+
const deletedNotes = toNumber(noteResult[0]?.deleted ?? 0);
|
|
117
|
+
const deletedBookmarks = toNumber(bookmarkResult[0]?.deleted ?? 0);
|
|
118
|
+
return createSuccessResponse(JSON.stringify({
|
|
119
|
+
success: true,
|
|
120
|
+
projectId: resolvedProjectId,
|
|
121
|
+
deleted: {
|
|
122
|
+
expiredNotes: deletedNotes,
|
|
123
|
+
oldBookmarks: deletedBookmarks,
|
|
124
|
+
},
|
|
125
|
+
keepBookmarks,
|
|
126
|
+
message: deletedNotes === 0 && deletedBookmarks === 0
|
|
127
|
+
? 'Nothing to clean up.'
|
|
128
|
+
: `Deleted ${deletedNotes} expired notes and ${deletedBookmarks} old bookmarks.`,
|
|
129
|
+
}));
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
await debugLog('Cleanup session error', { error: String(error) });
|
|
133
|
+
return createErrorResponse(error instanceof Error ? error : String(error));
|
|
134
|
+
}
|
|
135
|
+
finally {
|
|
136
|
+
await neo4jService.close();
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
};
|
|
@@ -12,6 +12,7 @@ import { Neo4jService } from '../../storage/neo4j/neo4j.service.js';
|
|
|
12
12
|
import { TOOL_NAMES, TOOL_METADATA } from '../constants.js';
|
|
13
13
|
import { createErrorResponse, createSuccessResponse, resolveProjectIdOrError, debugLog } from '../utils.js';
|
|
14
14
|
import { TASK_TYPES, TASK_PRIORITIES } from './swarm-constants.js';
|
|
15
|
+
import { PENDING_MESSAGES_FOR_AGENT_QUERY, AUTO_ACKNOWLEDGE_QUERY } from './swarm-message.tool.js';
|
|
15
16
|
/** Maximum retries when racing for a task */
|
|
16
17
|
const MAX_CLAIM_RETRIES = 3;
|
|
17
18
|
/** Delay between retries (ms) */
|
|
@@ -435,6 +436,39 @@ export const createSwarmClaimTaskTool = (server) => {
|
|
|
435
436
|
name: t.name,
|
|
436
437
|
filePath: t.filePath,
|
|
437
438
|
}));
|
|
439
|
+
// Fetch pending messages for this agent (direct delivery on claim)
|
|
440
|
+
let pendingMessages = [];
|
|
441
|
+
try {
|
|
442
|
+
const msgResult = await neo4jService.run(PENDING_MESSAGES_FOR_AGENT_QUERY, {
|
|
443
|
+
projectId: resolvedProjectId,
|
|
444
|
+
swarmId,
|
|
445
|
+
agentId,
|
|
446
|
+
});
|
|
447
|
+
if (msgResult.length > 0) {
|
|
448
|
+
pendingMessages = msgResult.map((m) => {
|
|
449
|
+
const ts = typeof m.timestamp === 'object' && m.timestamp?.toNumber ? m.timestamp.toNumber() : m.timestamp;
|
|
450
|
+
return {
|
|
451
|
+
id: m.id,
|
|
452
|
+
from: m.fromAgentId,
|
|
453
|
+
category: m.category,
|
|
454
|
+
content: m.content,
|
|
455
|
+
taskId: m.taskId ?? undefined,
|
|
456
|
+
filePaths: m.filePaths?.length > 0 ? m.filePaths : undefined,
|
|
457
|
+
age: ts ? `${Math.round((Date.now() - ts) / 1000)}s ago` : null,
|
|
458
|
+
};
|
|
459
|
+
});
|
|
460
|
+
// Auto-acknowledge delivered messages
|
|
461
|
+
const deliveredIds = pendingMessages.map((m) => m.id);
|
|
462
|
+
await neo4jService.run(AUTO_ACKNOWLEDGE_QUERY, {
|
|
463
|
+
messageIds: deliveredIds,
|
|
464
|
+
agentId,
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
catch (msgError) {
|
|
469
|
+
// Non-fatal: message delivery failure shouldn't block task claim
|
|
470
|
+
await debugLog('Swarm claim task: message delivery failed (non-fatal)', { error: String(msgError) });
|
|
471
|
+
}
|
|
438
472
|
// Slim response - only essential fields for agent to do work
|
|
439
473
|
return createSuccessResponse(JSON.stringify({
|
|
440
474
|
action: actionLabel,
|
|
@@ -450,6 +484,7 @@ export const createSwarmClaimTaskTool = (server) => {
|
|
|
450
484
|
targetFilePaths: task.targetFilePaths,
|
|
451
485
|
...(task.dependencies?.length > 0 && { dependencies: task.dependencies }),
|
|
452
486
|
},
|
|
487
|
+
...(pendingMessages.length > 0 && { messages: pendingMessages }),
|
|
453
488
|
...(retryCount > 0 && { retryAttempts: retryCount }),
|
|
454
489
|
}));
|
|
455
490
|
}
|
|
@@ -75,6 +75,22 @@ const COUNT_ALL_QUERY = `
|
|
|
75
75
|
WHERE p.projectId = $projectId AND NOT p.type IN $keepTypes
|
|
76
76
|
RETURN count(p) as count, collect(DISTINCT p.agentId) as agents, collect(DISTINCT p.swarmId) as swarms, collect(DISTINCT p.type) as types
|
|
77
77
|
`;
|
|
78
|
+
/**
|
|
79
|
+
* Neo4j query to delete SwarmMessage nodes by swarm ID
|
|
80
|
+
*/
|
|
81
|
+
const CLEANUP_MESSAGES_BY_SWARM_QUERY = `
|
|
82
|
+
MATCH (m:SwarmMessage)
|
|
83
|
+
WHERE m.projectId = $projectId
|
|
84
|
+
AND m.swarmId = $swarmId
|
|
85
|
+
WITH m, m.category as category
|
|
86
|
+
DELETE m
|
|
87
|
+
RETURN count(m) as deleted, collect(DISTINCT category) as categories
|
|
88
|
+
`;
|
|
89
|
+
const COUNT_MESSAGES_BY_SWARM_QUERY = `
|
|
90
|
+
MATCH (m:SwarmMessage)
|
|
91
|
+
WHERE m.projectId = $projectId AND m.swarmId = $swarmId
|
|
92
|
+
RETURN count(m) as count, collect(DISTINCT m.category) as categories
|
|
93
|
+
`;
|
|
78
94
|
export const createSwarmCleanupTool = (server) => {
|
|
79
95
|
server.registerTool(TOOL_NAMES.swarmCleanup, {
|
|
80
96
|
title: TOOL_METADATA[TOOL_NAMES.swarmCleanup].title,
|
|
@@ -143,6 +159,15 @@ export const createSwarmCleanupTool = (server) => {
|
|
|
143
159
|
typeof taskCount === 'object' && 'toNumber' in taskCount ? taskCount.toNumber() : taskCount;
|
|
144
160
|
taskStatuses = taskResult[0]?.statuses ?? [];
|
|
145
161
|
}
|
|
162
|
+
let messageCount = 0;
|
|
163
|
+
let messageCategories = [];
|
|
164
|
+
if (swarmId) {
|
|
165
|
+
const msgResult = await neo4jService.run(COUNT_MESSAGES_BY_SWARM_QUERY, params);
|
|
166
|
+
messageCount = msgResult[0]?.count ?? 0;
|
|
167
|
+
messageCount =
|
|
168
|
+
typeof messageCount === 'object' && 'toNumber' in messageCount ? messageCount.toNumber() : messageCount;
|
|
169
|
+
messageCategories = msgResult[0]?.categories ?? [];
|
|
170
|
+
}
|
|
146
171
|
return createSuccessResponse(JSON.stringify({
|
|
147
172
|
success: true,
|
|
148
173
|
dryRun: true,
|
|
@@ -160,6 +185,12 @@ export const createSwarmCleanupTool = (server) => {
|
|
|
160
185
|
statuses: taskStatuses,
|
|
161
186
|
}
|
|
162
187
|
: null,
|
|
188
|
+
messages: swarmId
|
|
189
|
+
? {
|
|
190
|
+
wouldDelete: messageCount,
|
|
191
|
+
categories: messageCategories,
|
|
192
|
+
}
|
|
193
|
+
: null,
|
|
163
194
|
keepTypes,
|
|
164
195
|
projectId: resolvedProjectId,
|
|
165
196
|
}));
|
|
@@ -179,6 +210,23 @@ export const createSwarmCleanupTool = (server) => {
|
|
|
179
210
|
: tasksDeleted;
|
|
180
211
|
taskStatuses = taskResult[0]?.statuses ?? [];
|
|
181
212
|
}
|
|
213
|
+
// Delete messages if swarmId provided
|
|
214
|
+
let messagesDeleted = 0;
|
|
215
|
+
let messageCategories = [];
|
|
216
|
+
if (swarmId) {
|
|
217
|
+
const msgResult = await neo4jService.run(CLEANUP_MESSAGES_BY_SWARM_QUERY, params);
|
|
218
|
+
messagesDeleted = msgResult[0]?.deleted ?? 0;
|
|
219
|
+
messagesDeleted =
|
|
220
|
+
typeof messagesDeleted === 'object' && 'toNumber' in messagesDeleted
|
|
221
|
+
? messagesDeleted.toNumber()
|
|
222
|
+
: messagesDeleted;
|
|
223
|
+
messageCategories = msgResult[0]?.categories ?? [];
|
|
224
|
+
}
|
|
225
|
+
const parts = [`${pheromonesDeleted} pheromones`];
|
|
226
|
+
if (swarmId && includeTasks)
|
|
227
|
+
parts.push(`${tasksDeleted} tasks`);
|
|
228
|
+
if (swarmId)
|
|
229
|
+
parts.push(`${messagesDeleted} messages`);
|
|
182
230
|
return createSuccessResponse(JSON.stringify({
|
|
183
231
|
success: true,
|
|
184
232
|
mode,
|
|
@@ -195,11 +243,15 @@ export const createSwarmCleanupTool = (server) => {
|
|
|
195
243
|
statuses: taskStatuses,
|
|
196
244
|
}
|
|
197
245
|
: null,
|
|
246
|
+
messages: swarmId
|
|
247
|
+
? {
|
|
248
|
+
deleted: messagesDeleted,
|
|
249
|
+
categories: messageCategories,
|
|
250
|
+
}
|
|
251
|
+
: null,
|
|
198
252
|
keepTypes,
|
|
199
253
|
projectId: resolvedProjectId,
|
|
200
|
-
message:
|
|
201
|
-
? `Cleaned up ${pheromonesDeleted} pheromones and ${tasksDeleted} tasks`
|
|
202
|
-
: `Cleaned up ${pheromonesDeleted} pheromones`,
|
|
254
|
+
message: `Cleaned up ${parts.join(', ')}`,
|
|
203
255
|
}));
|
|
204
256
|
}
|
|
205
257
|
catch (error) {
|
|
@@ -79,6 +79,34 @@ export const WORKFLOW_STATES = ['exploring', 'claiming', 'modifying', 'completed
|
|
|
79
79
|
*/
|
|
80
80
|
export const FLAG_TYPES = ['warning', 'proposal', 'needs_review', 'session_context'];
|
|
81
81
|
// ============================================================================
|
|
82
|
+
// SWARM MESSAGING CONSTANTS
|
|
83
|
+
// ============================================================================
|
|
84
|
+
/**
|
|
85
|
+
* Message categories for direct agent-to-agent communication.
|
|
86
|
+
* Unlike pheromones (passive, decay-based), messages are explicit and persistent until read.
|
|
87
|
+
*/
|
|
88
|
+
export const MESSAGE_CATEGORIES = {
|
|
89
|
+
blocked: 'Agent is blocked and needs help',
|
|
90
|
+
conflict: 'File or resource conflict detected',
|
|
91
|
+
finding: 'Important discovery another agent should know',
|
|
92
|
+
request: 'Direct request to another agent',
|
|
93
|
+
alert: 'Urgent notification (e.g., breaking change, test failure)',
|
|
94
|
+
handoff: 'Context handoff between agents (e.g., partial work)',
|
|
95
|
+
};
|
|
96
|
+
export const MESSAGE_CATEGORY_KEYS = Object.keys(MESSAGE_CATEGORIES);
|
|
97
|
+
/**
|
|
98
|
+
* Default TTL for messages (4 hours). Messages older than this are auto-cleaned.
|
|
99
|
+
*/
|
|
100
|
+
export const MESSAGE_DEFAULT_TTL_MS = 4 * 60 * 60 * 1000;
|
|
101
|
+
/**
|
|
102
|
+
* Generate a unique message ID
|
|
103
|
+
*/
|
|
104
|
+
export const generateMessageId = () => {
|
|
105
|
+
const timestamp = Date.now().toString(36);
|
|
106
|
+
const random = Math.random().toString(36).substring(2, 8);
|
|
107
|
+
return `msg_${timestamp}_${random}`;
|
|
108
|
+
};
|
|
109
|
+
// ============================================================================
|
|
82
110
|
// ORCHESTRATOR CONSTANTS
|
|
83
111
|
// ============================================================================
|
|
84
112
|
/**
|