code-graph-context 2.5.3 → 2.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/dist/core/embeddings/natural-language-to-cypher.service.js +1 -1
- package/dist/mcp/constants.js +70 -557
- package/dist/mcp/tools/detect-dead-code.tool.js +0 -16
- package/dist/mcp/tools/detect-duplicate-code.tool.js +0 -27
- package/dist/mcp/tools/impact-analysis.tool.js +0 -13
- package/dist/mcp/tools/index.js +4 -15
- package/dist/mcp/tools/list-projects.tool.js +0 -2
- package/dist/mcp/tools/list-watchers.tool.js +0 -2
- package/dist/mcp/tools/natural-language-to-cypher.tool.js +0 -6
- package/dist/mcp/tools/search-codebase.tool.js +0 -19
- package/dist/mcp/tools/swarm-claim-task.tool.js +220 -93
- package/dist/mcp/tools/swarm-cleanup.tool.js +76 -32
- package/dist/mcp/tools/swarm-complete-task.tool.js +79 -88
- package/dist/mcp/tools/swarm-get-tasks.tool.js +0 -9
- package/dist/mcp/tools/swarm-orchestrate.tool.js +55 -92
- package/dist/mcp/tools/swarm-post-task.tool.js +32 -11
- package/dist/mcp/tools/traverse-from-node.tool.js +0 -15
- package/package.json +1 -1
|
@@ -1,12 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Swarm Claim Task Tool
|
|
3
3
|
* Allow an agent to claim an available task from the blackboard
|
|
4
|
+
*
|
|
5
|
+
* Phase 1 improvements:
|
|
6
|
+
* - Atomic claim_and_start action (eliminates race window)
|
|
7
|
+
* - Retry logic on race loss
|
|
8
|
+
* - Recovery actions (abandon, force_start)
|
|
4
9
|
*/
|
|
5
10
|
import { z } from 'zod';
|
|
6
11
|
import { Neo4jService } from '../../storage/neo4j/neo4j.service.js';
|
|
7
12
|
import { TOOL_NAMES, TOOL_METADATA } from '../constants.js';
|
|
8
13
|
import { createErrorResponse, createSuccessResponse, resolveProjectIdOrError, debugLog } from '../utils.js';
|
|
9
14
|
import { TASK_TYPES, TASK_PRIORITIES } from './swarm-constants.js';
|
|
15
|
+
/** Maximum retries when racing for a task */
|
|
16
|
+
const MAX_CLAIM_RETRIES = 3;
|
|
17
|
+
/** Delay between retries (ms) */
|
|
18
|
+
const RETRY_DELAY_BASE_MS = 50;
|
|
10
19
|
/**
|
|
11
20
|
* Query to claim a specific task by ID
|
|
12
21
|
* Uses atomic update to prevent race conditions
|
|
@@ -30,9 +39,10 @@ const CLAIM_TASK_BY_ID_QUERY = `
|
|
|
30
39
|
WITH t WHERE t.status IN ['available', 'blocked']
|
|
31
40
|
|
|
32
41
|
// Atomic claim
|
|
33
|
-
SET t.status =
|
|
42
|
+
SET t.status = $targetStatus,
|
|
34
43
|
t.claimedBy = $agentId,
|
|
35
44
|
t.claimedAt = timestamp(),
|
|
45
|
+
t.startedAt = CASE WHEN $targetStatus = 'in_progress' THEN timestamp() ELSE null END,
|
|
36
46
|
t.updatedAt = timestamp()
|
|
37
47
|
|
|
38
48
|
// Return task details with target info
|
|
@@ -52,6 +62,7 @@ const CLAIM_TASK_BY_ID_QUERY = `
|
|
|
52
62
|
t.dependencies as dependencies,
|
|
53
63
|
t.claimedBy as claimedBy,
|
|
54
64
|
t.claimedAt as claimedAt,
|
|
65
|
+
t.startedAt as startedAt,
|
|
55
66
|
t.createdBy as createdBy,
|
|
56
67
|
t.metadata as metadata,
|
|
57
68
|
collect(DISTINCT {
|
|
@@ -64,6 +75,7 @@ const CLAIM_TASK_BY_ID_QUERY = `
|
|
|
64
75
|
/**
|
|
65
76
|
* Query to claim the highest priority available task matching criteria
|
|
66
77
|
* Uses APOC locking to prevent race conditions between parallel workers
|
|
78
|
+
* Supports both 'claimed' and 'in_progress' target states for atomic claim_and_start
|
|
67
79
|
*/
|
|
68
80
|
const CLAIM_NEXT_TASK_QUERY = `
|
|
69
81
|
// Find available or blocked tasks (blocked tasks may have deps completed now)
|
|
@@ -78,7 +90,8 @@ const CLAIM_NEXT_TASK_QUERY = `
|
|
|
78
90
|
WITH t, count(dep) as incompleteDeps
|
|
79
91
|
WHERE incompleteDeps = 0
|
|
80
92
|
|
|
81
|
-
//
|
|
93
|
+
// Re-establish context for ordering (required by Cypher syntax)
|
|
94
|
+
WITH t
|
|
82
95
|
ORDER BY t.priorityScore DESC, t.createdAt ASC
|
|
83
96
|
LIMIT 1
|
|
84
97
|
|
|
@@ -88,10 +101,11 @@ const CLAIM_NEXT_TASK_QUERY = `
|
|
|
88
101
|
// Double-check status after acquiring lock (another worker may have claimed it)
|
|
89
102
|
WITH t WHERE t.status IN ['available', 'blocked']
|
|
90
103
|
|
|
91
|
-
// Atomic claim
|
|
92
|
-
SET t.status =
|
|
104
|
+
// Atomic claim - supports both claim and claim_and_start via $targetStatus
|
|
105
|
+
SET t.status = $targetStatus,
|
|
93
106
|
t.claimedBy = $agentId,
|
|
94
107
|
t.claimedAt = timestamp(),
|
|
108
|
+
t.startedAt = CASE WHEN $targetStatus = 'in_progress' THEN timestamp() ELSE null END,
|
|
95
109
|
t.updatedAt = timestamp()
|
|
96
110
|
|
|
97
111
|
// Return task details with target info
|
|
@@ -111,6 +125,7 @@ const CLAIM_NEXT_TASK_QUERY = `
|
|
|
111
125
|
t.dependencies as dependencies,
|
|
112
126
|
t.claimedBy as claimedBy,
|
|
113
127
|
t.claimedAt as claimedAt,
|
|
128
|
+
t.startedAt as startedAt,
|
|
114
129
|
t.createdBy as createdBy,
|
|
115
130
|
t.metadata as metadata,
|
|
116
131
|
collect(DISTINCT {
|
|
@@ -154,6 +169,71 @@ const RELEASE_TASK_QUERY = `
|
|
|
154
169
|
t.title as title,
|
|
155
170
|
t.status as status
|
|
156
171
|
`;
|
|
172
|
+
/**
|
|
173
|
+
* Query to abandon a task - releases it with tracking for debugging
|
|
174
|
+
* More explicit than release, tracks abandon history
|
|
175
|
+
*/
|
|
176
|
+
const ABANDON_TASK_QUERY = `
|
|
177
|
+
MATCH (t:SwarmTask {id: $taskId, projectId: $projectId})
|
|
178
|
+
WHERE t.claimedBy = $agentId
|
|
179
|
+
AND t.status IN ['claimed', 'in_progress']
|
|
180
|
+
|
|
181
|
+
// Track abandon history
|
|
182
|
+
SET t.status = 'available',
|
|
183
|
+
t.previousClaimedBy = t.claimedBy,
|
|
184
|
+
t.claimedBy = null,
|
|
185
|
+
t.claimedAt = null,
|
|
186
|
+
t.startedAt = null,
|
|
187
|
+
t.updatedAt = timestamp(),
|
|
188
|
+
t.abandonedBy = $agentId,
|
|
189
|
+
t.abandonedAt = timestamp(),
|
|
190
|
+
t.abandonReason = $reason,
|
|
191
|
+
t.abandonCount = COALESCE(t.abandonCount, 0) + 1
|
|
192
|
+
|
|
193
|
+
RETURN t.id as id,
|
|
194
|
+
t.title as title,
|
|
195
|
+
t.status as status,
|
|
196
|
+
t.abandonCount as abandonCount,
|
|
197
|
+
t.abandonReason as abandonReason
|
|
198
|
+
`;
|
|
199
|
+
/**
|
|
200
|
+
* Query to force-start a task that's stuck in claimed state
|
|
201
|
+
* Allows recovery when the normal start action fails
|
|
202
|
+
*/
|
|
203
|
+
const FORCE_START_QUERY = `
|
|
204
|
+
MATCH (t:SwarmTask {id: $taskId, projectId: $projectId})
|
|
205
|
+
WHERE t.claimedBy = $agentId
|
|
206
|
+
AND t.status IN ['claimed', 'available']
|
|
207
|
+
|
|
208
|
+
SET t.status = 'in_progress',
|
|
209
|
+
t.claimedBy = $agentId,
|
|
210
|
+
t.claimedAt = COALESCE(t.claimedAt, timestamp()),
|
|
211
|
+
t.startedAt = timestamp(),
|
|
212
|
+
t.updatedAt = timestamp(),
|
|
213
|
+
t.forceStarted = true,
|
|
214
|
+
t.forceStartReason = $reason
|
|
215
|
+
|
|
216
|
+
RETURN t.id as id,
|
|
217
|
+
t.title as title,
|
|
218
|
+
t.status as status,
|
|
219
|
+
t.claimedBy as claimedBy,
|
|
220
|
+
t.startedAt as startedAt,
|
|
221
|
+
t.forceStarted as forceStarted
|
|
222
|
+
`;
|
|
223
|
+
/**
|
|
224
|
+
* Query to get current task state for better error messages
|
|
225
|
+
*/
|
|
226
|
+
const GET_TASK_STATE_QUERY = `
|
|
227
|
+
MATCH (t:SwarmTask {id: $taskId, projectId: $projectId})
|
|
228
|
+
RETURN t.id as id,
|
|
229
|
+
t.title as title,
|
|
230
|
+
t.status as status,
|
|
231
|
+
t.claimedBy as claimedBy,
|
|
232
|
+
t.claimedAt as claimedAt,
|
|
233
|
+
t.startedAt as startedAt,
|
|
234
|
+
t.abandonCount as abandonCount,
|
|
235
|
+
t.previousClaimedBy as previousClaimedBy
|
|
236
|
+
`;
|
|
157
237
|
export const createSwarmClaimTaskTool = (server) => {
|
|
158
238
|
server.registerTool(TOOL_NAMES.swarmClaimTask, {
|
|
159
239
|
title: TOOL_METADATA[TOOL_NAMES.swarmClaimTask].title,
|
|
@@ -175,16 +255,18 @@ export const createSwarmClaimTaskTool = (server) => {
|
|
|
175
255
|
.optional()
|
|
176
256
|
.describe('Minimum priority level when auto-selecting'),
|
|
177
257
|
action: z
|
|
178
|
-
.enum(['claim', 'start', 'release'])
|
|
258
|
+
.enum(['claim', 'claim_and_start', 'start', 'release', 'abandon', 'force_start'])
|
|
179
259
|
.optional()
|
|
180
|
-
.default('
|
|
181
|
-
.describe('Action:
|
|
260
|
+
.default('claim_and_start')
|
|
261
|
+
.describe('Action: claim_and_start (RECOMMENDED: atomic claim+start), claim (reserve only), ' +
|
|
262
|
+
'start (begin work on claimed task), release (give up task), ' +
|
|
263
|
+
'abandon (release with tracking), force_start (recover from stuck claimed state)'),
|
|
182
264
|
releaseReason: z
|
|
183
265
|
.string()
|
|
184
266
|
.optional()
|
|
185
|
-
.describe('Reason for releasing the task
|
|
267
|
+
.describe('Reason for releasing/abandoning the task'),
|
|
186
268
|
},
|
|
187
|
-
}, async ({ projectId, swarmId, agentId, taskId, types, minPriority, action = '
|
|
269
|
+
}, async ({ projectId, swarmId, agentId, taskId, types, minPriority, action = 'claim_and_start', releaseReason, }) => {
|
|
188
270
|
const neo4jService = new Neo4jService();
|
|
189
271
|
// Resolve project ID
|
|
190
272
|
const projectResult = await resolveProjectIdOrError(projectId, neo4jService);
|
|
@@ -194,15 +276,6 @@ export const createSwarmClaimTaskTool = (server) => {
|
|
|
194
276
|
}
|
|
195
277
|
const resolvedProjectId = projectResult.projectId;
|
|
196
278
|
try {
|
|
197
|
-
await debugLog('Swarm claim task', {
|
|
198
|
-
action,
|
|
199
|
-
projectId: resolvedProjectId,
|
|
200
|
-
swarmId,
|
|
201
|
-
agentId,
|
|
202
|
-
taskId,
|
|
203
|
-
types,
|
|
204
|
-
minPriority,
|
|
205
|
-
});
|
|
206
279
|
// Handle release action
|
|
207
280
|
if (action === 'release') {
|
|
208
281
|
if (!taskId) {
|
|
@@ -215,18 +288,70 @@ export const createSwarmClaimTaskTool = (server) => {
|
|
|
215
288
|
reason: releaseReason || 'No reason provided',
|
|
216
289
|
});
|
|
217
290
|
if (result.length === 0) {
|
|
218
|
-
|
|
291
|
+
// Get current state for better error message
|
|
292
|
+
const stateResult = await neo4jService.run(GET_TASK_STATE_QUERY, {
|
|
293
|
+
taskId,
|
|
294
|
+
projectId: resolvedProjectId,
|
|
295
|
+
});
|
|
296
|
+
const currentState = stateResult[0];
|
|
297
|
+
return createErrorResponse(`Cannot release task ${taskId}. ` +
|
|
298
|
+
(currentState
|
|
299
|
+
? `Current state: ${currentState.status}, claimedBy: ${currentState.claimedBy || 'none'}`
|
|
300
|
+
: 'Task not found.'));
|
|
301
|
+
}
|
|
302
|
+
return createSuccessResponse(JSON.stringify({ action: 'released', taskId: result[0].id }));
|
|
303
|
+
}
|
|
304
|
+
// Handle abandon action (release with tracking)
|
|
305
|
+
if (action === 'abandon') {
|
|
306
|
+
if (!taskId) {
|
|
307
|
+
return createErrorResponse('taskId is required for abandon action');
|
|
308
|
+
}
|
|
309
|
+
const result = await neo4jService.run(ABANDON_TASK_QUERY, {
|
|
310
|
+
taskId,
|
|
311
|
+
projectId: resolvedProjectId,
|
|
312
|
+
agentId,
|
|
313
|
+
reason: releaseReason || 'No reason provided',
|
|
314
|
+
});
|
|
315
|
+
if (result.length === 0) {
|
|
316
|
+
const stateResult = await neo4jService.run(GET_TASK_STATE_QUERY, {
|
|
317
|
+
taskId,
|
|
318
|
+
projectId: resolvedProjectId,
|
|
319
|
+
});
|
|
320
|
+
const currentState = stateResult[0];
|
|
321
|
+
return createErrorResponse(`Cannot abandon task ${taskId}. ` +
|
|
322
|
+
(currentState
|
|
323
|
+
? `Current state: ${currentState.status}, claimedBy: ${currentState.claimedBy || 'none'}`
|
|
324
|
+
: 'Task not found.'));
|
|
325
|
+
}
|
|
326
|
+
const abandonCount = typeof result[0].abandonCount === 'object'
|
|
327
|
+
? result[0].abandonCount.toNumber()
|
|
328
|
+
: result[0].abandonCount;
|
|
329
|
+
return createSuccessResponse(JSON.stringify({ action: 'abandoned', taskId: result[0].id, abandonCount }));
|
|
330
|
+
}
|
|
331
|
+
// Handle force_start action (recovery from stuck claimed state)
|
|
332
|
+
if (action === 'force_start') {
|
|
333
|
+
if (!taskId) {
|
|
334
|
+
return createErrorResponse('taskId is required for force_start action');
|
|
335
|
+
}
|
|
336
|
+
const result = await neo4jService.run(FORCE_START_QUERY, {
|
|
337
|
+
taskId,
|
|
338
|
+
projectId: resolvedProjectId,
|
|
339
|
+
agentId,
|
|
340
|
+
reason: releaseReason || 'Recovering from stuck state',
|
|
341
|
+
});
|
|
342
|
+
if (result.length === 0) {
|
|
343
|
+
const stateResult = await neo4jService.run(GET_TASK_STATE_QUERY, {
|
|
344
|
+
taskId,
|
|
345
|
+
projectId: resolvedProjectId,
|
|
346
|
+
});
|
|
347
|
+
const currentState = stateResult[0];
|
|
348
|
+
return createErrorResponse(`Cannot force_start task ${taskId}. ` +
|
|
349
|
+
(currentState
|
|
350
|
+
? `Current state: ${currentState.status}, claimedBy: ${currentState.claimedBy || 'none'}. ` +
|
|
351
|
+
`force_start requires status=claimed|available and you must be the claimant.`
|
|
352
|
+
: 'Task not found.'));
|
|
219
353
|
}
|
|
220
|
-
return createSuccessResponse(JSON.stringify({
|
|
221
|
-
success: true,
|
|
222
|
-
action: 'released',
|
|
223
|
-
task: {
|
|
224
|
-
id: result[0].id,
|
|
225
|
-
title: result[0].title,
|
|
226
|
-
status: result[0].status,
|
|
227
|
-
},
|
|
228
|
-
message: `Task released and now available for other agents`,
|
|
229
|
-
}));
|
|
354
|
+
return createSuccessResponse(JSON.stringify({ action: 'force_started', taskId: result[0].id, status: 'in_progress' }));
|
|
230
355
|
}
|
|
231
356
|
// Handle start action
|
|
232
357
|
if (action === 'start') {
|
|
@@ -239,98 +364,100 @@ export const createSwarmClaimTaskTool = (server) => {
|
|
|
239
364
|
agentId,
|
|
240
365
|
});
|
|
241
366
|
if (result.length === 0) {
|
|
242
|
-
|
|
367
|
+
// Get current state for better error message
|
|
368
|
+
const stateResult = await neo4jService.run(GET_TASK_STATE_QUERY, {
|
|
369
|
+
taskId,
|
|
370
|
+
projectId: resolvedProjectId,
|
|
371
|
+
});
|
|
372
|
+
const currentState = stateResult[0];
|
|
373
|
+
return createErrorResponse(`Cannot start task ${taskId}. ` +
|
|
374
|
+
(currentState
|
|
375
|
+
? `Current state: ${currentState.status}, claimedBy: ${currentState.claimedBy || 'none'}. ` +
|
|
376
|
+
`Tip: Use action="force_start" to recover from stuck claimed state, ` +
|
|
377
|
+
`or action="abandon" to release the task.`
|
|
378
|
+
: 'Task not found.'));
|
|
243
379
|
}
|
|
244
|
-
return createSuccessResponse(JSON.stringify({
|
|
245
|
-
success: true,
|
|
246
|
-
action: 'started',
|
|
247
|
-
task: {
|
|
248
|
-
id: result[0].id,
|
|
249
|
-
status: result[0].status,
|
|
250
|
-
claimedBy: result[0].claimedBy,
|
|
251
|
-
startedAt: typeof result[0].startedAt === 'object'
|
|
252
|
-
? result[0].startedAt.toNumber()
|
|
253
|
-
: result[0].startedAt,
|
|
254
|
-
},
|
|
255
|
-
message: 'Task is now in progress',
|
|
256
|
-
}));
|
|
380
|
+
return createSuccessResponse(JSON.stringify({ action: 'started', taskId: result[0].id, status: 'in_progress' }));
|
|
257
381
|
}
|
|
258
|
-
// Handle claim
|
|
382
|
+
// Handle claim and claim_and_start actions
|
|
383
|
+
// Determine target status based on action
|
|
384
|
+
const targetStatus = action === 'claim_and_start' ? 'in_progress' : 'claimed';
|
|
259
385
|
let result;
|
|
386
|
+
let retryCount = 0;
|
|
260
387
|
if (taskId) {
|
|
261
388
|
// Claim specific task
|
|
262
389
|
result = await neo4jService.run(CLAIM_TASK_BY_ID_QUERY, {
|
|
263
390
|
taskId,
|
|
264
391
|
projectId: resolvedProjectId,
|
|
265
392
|
agentId,
|
|
393
|
+
targetStatus,
|
|
266
394
|
});
|
|
267
395
|
if (result.length === 0) {
|
|
268
|
-
|
|
396
|
+
const stateResult = await neo4jService.run(GET_TASK_STATE_QUERY, {
|
|
397
|
+
taskId,
|
|
398
|
+
projectId: resolvedProjectId,
|
|
399
|
+
});
|
|
400
|
+
const currentState = stateResult[0];
|
|
401
|
+
return createErrorResponse(`Cannot claim task ${taskId}. ` +
|
|
402
|
+
(currentState
|
|
403
|
+
? `Current state: ${currentState.status}, claimedBy: ${currentState.claimedBy || 'none'}`
|
|
404
|
+
: 'Task not found or has incomplete dependencies.'));
|
|
269
405
|
}
|
|
270
406
|
}
|
|
271
407
|
else {
|
|
272
|
-
// Auto-select highest priority available task
|
|
408
|
+
// Auto-select highest priority available task with retry logic
|
|
273
409
|
const minPriorityScore = minPriority
|
|
274
410
|
? TASK_PRIORITIES[minPriority]
|
|
275
411
|
: null;
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
const task = result[0];
|
|
297
|
-
// Parse metadata if present
|
|
298
|
-
let metadata = null;
|
|
299
|
-
if (task.metadata) {
|
|
300
|
-
try {
|
|
301
|
-
metadata = JSON.parse(task.metadata);
|
|
412
|
+
// Retry loop to handle race conditions
|
|
413
|
+
while (retryCount < MAX_CLAIM_RETRIES) {
|
|
414
|
+
result = await neo4jService.run(CLAIM_NEXT_TASK_QUERY, {
|
|
415
|
+
projectId: resolvedProjectId,
|
|
416
|
+
swarmId,
|
|
417
|
+
agentId,
|
|
418
|
+
types: types || null,
|
|
419
|
+
minPriority: minPriorityScore,
|
|
420
|
+
targetStatus,
|
|
421
|
+
});
|
|
422
|
+
if (result.length > 0) {
|
|
423
|
+
break; // Successfully claimed a task
|
|
424
|
+
}
|
|
425
|
+
retryCount++;
|
|
426
|
+
if (retryCount < MAX_CLAIM_RETRIES) {
|
|
427
|
+
// Wait before retry with exponential backoff
|
|
428
|
+
await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY_BASE_MS * Math.pow(2, retryCount - 1)));
|
|
429
|
+
}
|
|
302
430
|
}
|
|
303
|
-
|
|
304
|
-
|
|
431
|
+
if (!result || result.length === 0) {
|
|
432
|
+
return createSuccessResponse(JSON.stringify({ action: 'no_tasks', retryAttempts: retryCount }));
|
|
305
433
|
}
|
|
306
434
|
}
|
|
307
|
-
|
|
308
|
-
const
|
|
435
|
+
const task = result[0];
|
|
436
|
+
const actionLabel = action === 'claim_and_start' ? 'claimed_and_started' : 'claimed';
|
|
437
|
+
// Extract valid targets (resolved via :TARGETS relationship)
|
|
438
|
+
const resolvedTargets = (task.targets || [])
|
|
439
|
+
.filter((t) => t?.id)
|
|
440
|
+
.map((t) => ({
|
|
441
|
+
nodeId: t.id,
|
|
442
|
+
name: t.name,
|
|
443
|
+
filePath: t.filePath,
|
|
444
|
+
}));
|
|
445
|
+
// Slim response - only essential fields for agent to do work
|
|
309
446
|
return createSuccessResponse(JSON.stringify({
|
|
310
|
-
|
|
311
|
-
action: 'claimed',
|
|
447
|
+
action: actionLabel,
|
|
312
448
|
task: {
|
|
313
449
|
id: task.id,
|
|
314
|
-
projectId: task.projectId,
|
|
315
|
-
swarmId: task.swarmId,
|
|
316
450
|
title: task.title,
|
|
317
451
|
description: task.description,
|
|
318
|
-
type: task.type,
|
|
319
|
-
priority: task.priority,
|
|
320
|
-
priorityScore: task.priorityScore,
|
|
321
452
|
status: task.status,
|
|
322
|
-
|
|
453
|
+
type: task.type,
|
|
454
|
+
// Prefer resolved targets over stored nodeIds (resolved targets are from graph relationships)
|
|
455
|
+
targets: resolvedTargets.length > 0 ? resolvedTargets : undefined,
|
|
456
|
+
targetNodeIds: task.targetNodeIds?.length > 0 ? task.targetNodeIds : undefined,
|
|
323
457
|
targetFilePaths: task.targetFilePaths,
|
|
324
|
-
dependencies: task.dependencies,
|
|
325
|
-
claimedBy: task.claimedBy,
|
|
326
|
-
claimedAt: typeof task.claimedAt === 'object'
|
|
327
|
-
? task.claimedAt.toNumber()
|
|
328
|
-
: task.claimedAt,
|
|
329
|
-
createdBy: task.createdBy,
|
|
330
|
-
metadata,
|
|
331
|
-
targets,
|
|
458
|
+
...(task.dependencies?.length > 0 && { dependencies: task.dependencies }),
|
|
332
459
|
},
|
|
333
|
-
|
|
460
|
+
...(retryCount > 0 && { retryAttempts: retryCount }),
|
|
334
461
|
}));
|
|
335
462
|
}
|
|
336
463
|
catch (error) {
|
|
@@ -9,7 +9,7 @@ import { createErrorResponse, createSuccessResponse, resolveProjectIdOrError, de
|
|
|
9
9
|
/**
|
|
10
10
|
* Neo4j query to delete pheromones by swarm ID
|
|
11
11
|
*/
|
|
12
|
-
const
|
|
12
|
+
const CLEANUP_PHEROMONES_BY_SWARM_QUERY = `
|
|
13
13
|
MATCH (p:Pheromone)
|
|
14
14
|
WHERE p.projectId = $projectId
|
|
15
15
|
AND p.swarmId = $swarmId
|
|
@@ -18,6 +18,17 @@ const CLEANUP_BY_SWARM_QUERY = `
|
|
|
18
18
|
DETACH DELETE p
|
|
19
19
|
RETURN count(p) as deleted, collect(DISTINCT agentId) as agents, collect(DISTINCT type) as types
|
|
20
20
|
`;
|
|
21
|
+
/**
|
|
22
|
+
* Neo4j query to delete SwarmTask nodes by swarm ID
|
|
23
|
+
*/
|
|
24
|
+
const CLEANUP_TASKS_BY_SWARM_QUERY = `
|
|
25
|
+
MATCH (t:SwarmTask)
|
|
26
|
+
WHERE t.projectId = $projectId
|
|
27
|
+
AND t.swarmId = $swarmId
|
|
28
|
+
WITH t, t.status as status
|
|
29
|
+
DETACH DELETE t
|
|
30
|
+
RETURN count(t) as deleted, collect(DISTINCT status) as statuses
|
|
31
|
+
`;
|
|
21
32
|
/**
|
|
22
33
|
* Neo4j query to delete pheromones by agent ID
|
|
23
34
|
*/
|
|
@@ -44,11 +55,16 @@ const CLEANUP_ALL_QUERY = `
|
|
|
44
55
|
/**
|
|
45
56
|
* Count queries for dry run
|
|
46
57
|
*/
|
|
47
|
-
const
|
|
58
|
+
const COUNT_PHEROMONES_BY_SWARM_QUERY = `
|
|
48
59
|
MATCH (p:Pheromone)
|
|
49
60
|
WHERE p.projectId = $projectId AND p.swarmId = $swarmId AND NOT p.type IN $keepTypes
|
|
50
61
|
RETURN count(p) as count, collect(DISTINCT p.agentId) as agents, collect(DISTINCT p.type) as types
|
|
51
62
|
`;
|
|
63
|
+
const COUNT_TASKS_BY_SWARM_QUERY = `
|
|
64
|
+
MATCH (t:SwarmTask)
|
|
65
|
+
WHERE t.projectId = $projectId AND t.swarmId = $swarmId
|
|
66
|
+
RETURN count(t) as count, collect(DISTINCT t.status) as statuses
|
|
67
|
+
`;
|
|
52
68
|
const COUNT_BY_AGENT_QUERY = `
|
|
53
69
|
MATCH (p:Pheromone)
|
|
54
70
|
WHERE p.projectId = $projectId AND p.agentId = $agentId AND NOT p.type IN $keepTypes
|
|
@@ -65,9 +81,14 @@ export const createSwarmCleanupTool = (server) => {
|
|
|
65
81
|
description: TOOL_METADATA[TOOL_NAMES.swarmCleanup].description,
|
|
66
82
|
inputSchema: {
|
|
67
83
|
projectId: z.string().describe('Project ID, name, or path'),
|
|
68
|
-
swarmId: z.string().optional().describe('Delete all pheromones from this swarm'),
|
|
84
|
+
swarmId: z.string().optional().describe('Delete all pheromones and tasks from this swarm'),
|
|
69
85
|
agentId: z.string().optional().describe('Delete all pheromones from this agent'),
|
|
70
86
|
all: z.boolean().optional().default(false).describe('Delete ALL pheromones in project (use with caution)'),
|
|
87
|
+
includeTasks: z
|
|
88
|
+
.boolean()
|
|
89
|
+
.optional()
|
|
90
|
+
.default(true)
|
|
91
|
+
.describe('Also delete SwarmTask nodes (default: true, only applies when swarmId is provided)'),
|
|
71
92
|
keepTypes: z
|
|
72
93
|
.array(z.string())
|
|
73
94
|
.optional()
|
|
@@ -75,7 +96,7 @@ export const createSwarmCleanupTool = (server) => {
|
|
|
75
96
|
.describe('Pheromone types to preserve (default: ["warning"])'),
|
|
76
97
|
dryRun: z.boolean().optional().default(false).describe('Preview what would be deleted without deleting'),
|
|
77
98
|
},
|
|
78
|
-
}, async ({ projectId, swarmId, agentId, all = false, keepTypes = ['warning'], dryRun = false }) => {
|
|
99
|
+
}, async ({ projectId, swarmId, agentId, all = false, includeTasks = true, keepTypes = ['warning'], dryRun = false }) => {
|
|
79
100
|
const neo4jService = new Neo4jService();
|
|
80
101
|
// Resolve project ID
|
|
81
102
|
const projectResult = await resolveProjectIdOrError(projectId, neo4jService);
|
|
@@ -89,61 +110,84 @@ export const createSwarmCleanupTool = (server) => {
|
|
|
89
110
|
if (!swarmId && !agentId && !all) {
|
|
90
111
|
return createErrorResponse('Must specify one of: swarmId, agentId, or all=true. Use dryRun=true to preview.');
|
|
91
112
|
}
|
|
92
|
-
await debugLog('Swarm cleanup operation', {
|
|
93
|
-
projectId: resolvedProjectId,
|
|
94
|
-
swarmId,
|
|
95
|
-
agentId,
|
|
96
|
-
all,
|
|
97
|
-
keepTypes,
|
|
98
|
-
dryRun,
|
|
99
|
-
});
|
|
100
113
|
const params = { projectId: resolvedProjectId, keepTypes };
|
|
101
|
-
let
|
|
102
|
-
let
|
|
114
|
+
let pheromoneDeleteQuery;
|
|
115
|
+
let pheromoneCountQuery;
|
|
103
116
|
let mode;
|
|
104
117
|
if (swarmId) {
|
|
105
118
|
params.swarmId = swarmId;
|
|
106
|
-
|
|
107
|
-
|
|
119
|
+
pheromoneDeleteQuery = CLEANUP_PHEROMONES_BY_SWARM_QUERY;
|
|
120
|
+
pheromoneCountQuery = COUNT_PHEROMONES_BY_SWARM_QUERY;
|
|
108
121
|
mode = 'swarm';
|
|
109
122
|
}
|
|
110
123
|
else if (agentId) {
|
|
111
124
|
params.agentId = agentId;
|
|
112
|
-
|
|
113
|
-
|
|
125
|
+
pheromoneDeleteQuery = CLEANUP_BY_AGENT_QUERY;
|
|
126
|
+
pheromoneCountQuery = COUNT_BY_AGENT_QUERY;
|
|
114
127
|
mode = 'agent';
|
|
115
128
|
}
|
|
116
129
|
else {
|
|
117
|
-
|
|
118
|
-
|
|
130
|
+
pheromoneDeleteQuery = CLEANUP_ALL_QUERY;
|
|
131
|
+
pheromoneCountQuery = COUNT_ALL_QUERY;
|
|
119
132
|
mode = 'all';
|
|
120
133
|
}
|
|
121
134
|
if (dryRun) {
|
|
122
|
-
const
|
|
123
|
-
const
|
|
135
|
+
const pheromoneResult = await neo4jService.run(pheromoneCountQuery, params);
|
|
136
|
+
const pheromoneCount = pheromoneResult[0]?.count ?? 0;
|
|
137
|
+
let taskCount = 0;
|
|
138
|
+
let taskStatuses = [];
|
|
139
|
+
if (swarmId && includeTasks) {
|
|
140
|
+
const taskResult = await neo4jService.run(COUNT_TASKS_BY_SWARM_QUERY, params);
|
|
141
|
+
taskCount = taskResult[0]?.count ?? 0;
|
|
142
|
+
taskCount = typeof taskCount === 'object' && 'toNumber' in taskCount ? taskCount.toNumber() : taskCount;
|
|
143
|
+
taskStatuses = taskResult[0]?.statuses ?? [];
|
|
144
|
+
}
|
|
124
145
|
return createSuccessResponse(JSON.stringify({
|
|
125
146
|
success: true,
|
|
126
147
|
dryRun: true,
|
|
127
148
|
mode,
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
149
|
+
pheromones: {
|
|
150
|
+
wouldDelete: typeof pheromoneCount === 'object' && 'toNumber' in pheromoneCount ? pheromoneCount.toNumber() : pheromoneCount,
|
|
151
|
+
agents: pheromoneResult[0]?.agents ?? [],
|
|
152
|
+
types: pheromoneResult[0]?.types ?? [],
|
|
153
|
+
},
|
|
154
|
+
tasks: swarmId && includeTasks ? {
|
|
155
|
+
wouldDelete: taskCount,
|
|
156
|
+
statuses: taskStatuses,
|
|
157
|
+
} : null,
|
|
132
158
|
keepTypes,
|
|
133
159
|
projectId: resolvedProjectId,
|
|
134
160
|
}));
|
|
135
161
|
}
|
|
136
|
-
|
|
137
|
-
const
|
|
162
|
+
// Delete pheromones
|
|
163
|
+
const pheromoneResult = await neo4jService.run(pheromoneDeleteQuery, params);
|
|
164
|
+
const pheromonesDeleted = pheromoneResult[0]?.deleted ?? 0;
|
|
165
|
+
// Delete tasks if swarmId provided and includeTasks is true
|
|
166
|
+
let tasksDeleted = 0;
|
|
167
|
+
let taskStatuses = [];
|
|
168
|
+
if (swarmId && includeTasks) {
|
|
169
|
+
const taskResult = await neo4jService.run(CLEANUP_TASKS_BY_SWARM_QUERY, params);
|
|
170
|
+
tasksDeleted = taskResult[0]?.deleted ?? 0;
|
|
171
|
+
tasksDeleted = typeof tasksDeleted === 'object' && 'toNumber' in tasksDeleted ? tasksDeleted.toNumber() : tasksDeleted;
|
|
172
|
+
taskStatuses = taskResult[0]?.statuses ?? [];
|
|
173
|
+
}
|
|
138
174
|
return createSuccessResponse(JSON.stringify({
|
|
139
175
|
success: true,
|
|
140
176
|
mode,
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
177
|
+
pheromones: {
|
|
178
|
+
deleted: typeof pheromonesDeleted === 'object' && 'toNumber' in pheromonesDeleted ? pheromonesDeleted.toNumber() : pheromonesDeleted,
|
|
179
|
+
agents: pheromoneResult[0]?.agents ?? [],
|
|
180
|
+
types: pheromoneResult[0]?.types ?? [],
|
|
181
|
+
},
|
|
182
|
+
tasks: swarmId && includeTasks ? {
|
|
183
|
+
deleted: tasksDeleted,
|
|
184
|
+
statuses: taskStatuses,
|
|
185
|
+
} : null,
|
|
145
186
|
keepTypes,
|
|
146
187
|
projectId: resolvedProjectId,
|
|
188
|
+
message: swarmId && includeTasks
|
|
189
|
+
? `Cleaned up ${pheromonesDeleted} pheromones and ${tasksDeleted} tasks`
|
|
190
|
+
: `Cleaned up ${pheromonesDeleted} pheromones`,
|
|
147
191
|
}));
|
|
148
192
|
}
|
|
149
193
|
catch (error) {
|