code-graph-context 2.14.1 → 3.0.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 +2 -2
- package/dist/core/embeddings/natural-language-to-cypher.service.js +2 -4
- package/dist/mcp/constants.js +56 -228
- package/dist/mcp/handlers/swarm/abandon.handler.js +61 -0
- package/dist/mcp/handlers/swarm/advance.handler.js +78 -0
- package/dist/mcp/handlers/swarm/claim.handler.js +61 -0
- package/dist/mcp/handlers/swarm/index.js +5 -0
- package/dist/mcp/handlers/swarm/queries.js +140 -0
- package/dist/mcp/handlers/swarm/release.handler.js +41 -0
- package/dist/mcp/handlers/swarm-worker.handler.js +2 -13
- package/dist/mcp/tools/detect-dead-code.tool.js +33 -65
- package/dist/mcp/tools/detect-duplicate-code.tool.js +44 -53
- package/dist/mcp/tools/impact-analysis.tool.js +1 -1
- package/dist/mcp/tools/index.js +9 -9
- package/dist/mcp/tools/list-projects.tool.js +2 -2
- package/dist/mcp/tools/list-watchers.tool.js +2 -5
- package/dist/mcp/tools/natural-language-to-cypher.tool.js +2 -2
- package/dist/mcp/tools/parse-typescript-project.tool.js +7 -17
- package/dist/mcp/tools/search-codebase.tool.js +11 -26
- package/dist/mcp/tools/session-bookmark.tool.js +7 -11
- package/dist/mcp/tools/session-cleanup.tool.js +2 -6
- package/dist/mcp/tools/session-note.tool.js +6 -21
- package/dist/mcp/tools/session-recall.tool.js +293 -0
- package/dist/mcp/tools/session-save.tool.js +280 -0
- package/dist/mcp/tools/start-watch-project.tool.js +1 -1
- package/dist/mcp/tools/swarm-advance-task.tool.js +56 -0
- package/dist/mcp/tools/swarm-claim-task.tool.js +24 -388
- package/dist/mcp/tools/swarm-cleanup.tool.js +3 -7
- package/dist/mcp/tools/swarm-complete-task.tool.js +14 -17
- package/dist/mcp/tools/swarm-get-tasks.tool.js +8 -26
- package/dist/mcp/tools/swarm-message.tool.js +10 -25
- package/dist/mcp/tools/swarm-pheromone.tool.js +7 -25
- package/dist/mcp/tools/swarm-post-task.tool.js +7 -19
- package/dist/mcp/tools/swarm-release-task.tool.js +53 -0
- package/dist/mcp/tools/swarm-sense.tool.js +10 -30
- package/dist/mcp/tools/traverse-from-node.tool.js +19 -41
- package/dist/mcp/utils.js +41 -1
- package/package.json +3 -3
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session Save Tool
|
|
3
|
+
* Unified tool that merges save_session_bookmark and save_session_note into one call.
|
|
4
|
+
* Auto-detects bookmark vs note based on input fields provided.
|
|
5
|
+
*/
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
import { EmbeddingsService, getEmbeddingDimensions } from '../../core/embeddings/embeddings.service.js';
|
|
8
|
+
import { Neo4jService, QUERIES } from '../../storage/neo4j/neo4j.service.js';
|
|
9
|
+
import { TOOL_NAMES, TOOL_METADATA } from '../constants.js';
|
|
10
|
+
import { createErrorResponse, createSuccessResponse, resolveProjectIdOrError, debugLog } from '../utils.js';
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// Cypher queries (copied from their respective source tools)
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
const CREATE_BOOKMARK_QUERY = `
|
|
15
|
+
CREATE (b:SessionBookmark {
|
|
16
|
+
id: $bookmarkId,
|
|
17
|
+
projectId: $projectId,
|
|
18
|
+
sessionId: $sessionId,
|
|
19
|
+
agentId: $agentId,
|
|
20
|
+
summary: $summary,
|
|
21
|
+
workingSetNodeIds: $workingSetNodeIds,
|
|
22
|
+
taskContext: $taskContext,
|
|
23
|
+
findings: $findings,
|
|
24
|
+
nextSteps: $nextSteps,
|
|
25
|
+
metadata: $metadata,
|
|
26
|
+
createdAt: timestamp(),
|
|
27
|
+
updatedAt: timestamp()
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
// Link to referenced code nodes (exclude coordination nodes)
|
|
31
|
+
WITH b
|
|
32
|
+
OPTIONAL MATCH (target)
|
|
33
|
+
WHERE target.id IN $workingSetNodeIds
|
|
34
|
+
AND target.projectId = $projectId
|
|
35
|
+
AND NOT target:Pheromone
|
|
36
|
+
AND NOT target:SwarmTask
|
|
37
|
+
AND NOT target:SessionBookmark
|
|
38
|
+
AND NOT target:SessionNote
|
|
39
|
+
WITH b, collect(DISTINCT target) AS targets
|
|
40
|
+
FOREACH (t IN targets | MERGE (b)-[:REFERENCES]->(t))
|
|
41
|
+
|
|
42
|
+
RETURN b.id AS id,
|
|
43
|
+
b.sessionId AS sessionId,
|
|
44
|
+
b.agentId AS agentId,
|
|
45
|
+
b.summary AS summary,
|
|
46
|
+
b.taskContext AS taskContext,
|
|
47
|
+
b.createdAt AS createdAt,
|
|
48
|
+
size(targets) AS linkedNodes
|
|
49
|
+
`;
|
|
50
|
+
const CREATE_SESSION_NOTE_QUERY = `
|
|
51
|
+
// Create the SessionNote node
|
|
52
|
+
CREATE (n:SessionNote {
|
|
53
|
+
id: $noteId,
|
|
54
|
+
projectId: $projectId,
|
|
55
|
+
sessionId: $sessionId,
|
|
56
|
+
agentId: $agentId,
|
|
57
|
+
topic: $topic,
|
|
58
|
+
content: $content,
|
|
59
|
+
category: $category,
|
|
60
|
+
severity: $severity,
|
|
61
|
+
createdAt: timestamp(),
|
|
62
|
+
expiresAt: $expiresAt
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
// Link to referenced code nodes (filter out internal coordination nodes)
|
|
66
|
+
WITH n
|
|
67
|
+
UNWIND CASE WHEN size($aboutNodeIds) = 0 THEN [null] ELSE $aboutNodeIds END AS aboutNodeId
|
|
68
|
+
OPTIONAL MATCH (target)
|
|
69
|
+
WHERE aboutNodeId IS NOT NULL
|
|
70
|
+
AND target.id = aboutNodeId
|
|
71
|
+
AND target.projectId = $projectId
|
|
72
|
+
AND NOT target:SessionNote
|
|
73
|
+
AND NOT target:SessionBookmark
|
|
74
|
+
AND NOT target:Pheromone
|
|
75
|
+
AND NOT target:SwarmTask
|
|
76
|
+
WITH n, collect(target) AS targets
|
|
77
|
+
FOREACH (t IN [x IN targets WHERE x IS NOT NULL] | MERGE (n)-[:ABOUT]->(t))
|
|
78
|
+
|
|
79
|
+
// Link to the latest SessionBookmark for this session (if one exists)
|
|
80
|
+
WITH n
|
|
81
|
+
OPTIONAL MATCH (bm:SessionBookmark {projectId: $projectId, sessionId: $sessionId})
|
|
82
|
+
WITH n, bm ORDER BY bm.createdAt DESC
|
|
83
|
+
LIMIT 1
|
|
84
|
+
FOREACH (_ IN CASE WHEN bm IS NOT NULL THEN [1] ELSE [] END |
|
|
85
|
+
MERGE (bm)-[:HAS_NOTE]->(n)
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
RETURN n.id AS noteId
|
|
89
|
+
`;
|
|
90
|
+
const SET_NOTE_EMBEDDING_QUERY = `
|
|
91
|
+
MATCH (n:SessionNote {id: $noteId, projectId: $projectId})
|
|
92
|
+
SET n.embedding = $embedding
|
|
93
|
+
RETURN n.id AS noteId
|
|
94
|
+
`;
|
|
95
|
+
// ---------------------------------------------------------------------------
|
|
96
|
+
// Helpers
|
|
97
|
+
// ---------------------------------------------------------------------------
|
|
98
|
+
const generateBookmarkId = () => `bookmark_${Date.now().toString(36)}_${Math.random().toString(36).substring(2, 8)}`;
|
|
99
|
+
const generateNoteId = () => `note_${Date.now().toString(36)}_${Math.random().toString(36).substring(2, 8)}`;
|
|
100
|
+
// ---------------------------------------------------------------------------
|
|
101
|
+
// Tool registration
|
|
102
|
+
// ---------------------------------------------------------------------------
|
|
103
|
+
export const createSessionSaveTool = (server) => {
|
|
104
|
+
server.registerTool(TOOL_NAMES.sessionSave, {
|
|
105
|
+
title: TOOL_METADATA[TOOL_NAMES.sessionSave].title,
|
|
106
|
+
description: TOOL_METADATA[TOOL_NAMES.sessionSave].description,
|
|
107
|
+
inputSchema: {
|
|
108
|
+
projectId: z.string().describe('Project ID, name, or path'),
|
|
109
|
+
sessionId: z.string().describe('Session/conversation identifier'),
|
|
110
|
+
agentId: z.string().describe('Your agent identifier'),
|
|
111
|
+
type: z
|
|
112
|
+
.enum(['bookmark', 'note', 'auto'])
|
|
113
|
+
.optional()
|
|
114
|
+
.default('auto')
|
|
115
|
+
.describe('Force bookmark or note, or auto-detect from input'),
|
|
116
|
+
// Bookmark fields
|
|
117
|
+
summary: z.string().min(10).optional().describe('Current work state summary'),
|
|
118
|
+
workingSetNodeIds: z.array(z.string()).optional().describe('Code node IDs you are focused on'),
|
|
119
|
+
taskContext: z.string().optional().describe('High-level task being worked on'),
|
|
120
|
+
findings: z.string().optional().describe('Key discoveries or decisions'),
|
|
121
|
+
nextSteps: z.string().optional().describe('What to do next when resuming'),
|
|
122
|
+
// Note fields
|
|
123
|
+
topic: z.string().min(3).max(100).optional().describe('Short topic label'),
|
|
124
|
+
content: z.string().min(10).optional().describe('Full observation text'),
|
|
125
|
+
category: z
|
|
126
|
+
.enum(['architectural', 'bug', 'insight', 'decision', 'risk', 'todo'])
|
|
127
|
+
.optional()
|
|
128
|
+
.describe('Note category'),
|
|
129
|
+
severity: z.enum(['info', 'warning', 'critical']).optional().default('info').describe('Note severity'),
|
|
130
|
+
aboutNodeIds: z.array(z.string()).optional().describe('Code node IDs this note is about'),
|
|
131
|
+
expiresInHours: z.number().optional().describe('Auto-expire note after N hours'),
|
|
132
|
+
metadata: z.string().optional().describe('Additional structured data as JSON string'),
|
|
133
|
+
},
|
|
134
|
+
}, async ({ projectId, sessionId, agentId, type = 'auto', summary, workingSetNodeIds, taskContext, findings = '', nextSteps = '', topic, content, category, severity = 'info', aboutNodeIds = [], expiresInHours, metadata, }) => {
|
|
135
|
+
const neo4jService = new Neo4jService();
|
|
136
|
+
const projectResult = await resolveProjectIdOrError(projectId, neo4jService);
|
|
137
|
+
if (!projectResult.success) {
|
|
138
|
+
await neo4jService.close();
|
|
139
|
+
return projectResult.error;
|
|
140
|
+
}
|
|
141
|
+
const resolvedProjectId = projectResult.projectId;
|
|
142
|
+
// Determine effective operation mode
|
|
143
|
+
const hasBookmarkFields = workingSetNodeIds != null && workingSetNodeIds.length > 0;
|
|
144
|
+
const hasNoteFields = topic != null && content != null;
|
|
145
|
+
let effectiveType;
|
|
146
|
+
if (type === 'bookmark') {
|
|
147
|
+
effectiveType = 'bookmark';
|
|
148
|
+
}
|
|
149
|
+
else if (type === 'note') {
|
|
150
|
+
effectiveType = 'note';
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
// auto-detect
|
|
154
|
+
if (hasBookmarkFields && hasNoteFields) {
|
|
155
|
+
effectiveType = 'both';
|
|
156
|
+
}
|
|
157
|
+
else if (hasBookmarkFields) {
|
|
158
|
+
effectiveType = 'bookmark';
|
|
159
|
+
}
|
|
160
|
+
else if (hasNoteFields) {
|
|
161
|
+
effectiveType = 'note';
|
|
162
|
+
}
|
|
163
|
+
else {
|
|
164
|
+
await neo4jService.close();
|
|
165
|
+
return createErrorResponse('Cannot auto-detect type: provide workingSetNodeIds for a bookmark, topic+content for a note, or both.');
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
try {
|
|
169
|
+
// Validate required fields per operation
|
|
170
|
+
if ((effectiveType === 'bookmark' || effectiveType === 'both') && !summary) {
|
|
171
|
+
return createErrorResponse('summary is required when saving a bookmark.');
|
|
172
|
+
}
|
|
173
|
+
if ((effectiveType === 'bookmark' || effectiveType === 'both') && !taskContext) {
|
|
174
|
+
return createErrorResponse('taskContext is required when saving a bookmark.');
|
|
175
|
+
}
|
|
176
|
+
if ((effectiveType === 'bookmark' || effectiveType === 'both') &&
|
|
177
|
+
(!workingSetNodeIds || workingSetNodeIds.length === 0)) {
|
|
178
|
+
return createErrorResponse('workingSetNodeIds is required when saving a bookmark.');
|
|
179
|
+
}
|
|
180
|
+
if ((effectiveType === 'note' || effectiveType === 'both') && !topic) {
|
|
181
|
+
return createErrorResponse('topic is required when saving a note.');
|
|
182
|
+
}
|
|
183
|
+
if ((effectiveType === 'note' || effectiveType === 'both') && !content) {
|
|
184
|
+
return createErrorResponse('content is required when saving a note.');
|
|
185
|
+
}
|
|
186
|
+
if ((effectiveType === 'note' || effectiveType === 'both') && !category) {
|
|
187
|
+
return createErrorResponse('category is required when saving a note.');
|
|
188
|
+
}
|
|
189
|
+
const result = { success: true, projectId: resolvedProjectId, sessionId, agentId };
|
|
190
|
+
// ── Create bookmark ──────────────────────────────────────────────────
|
|
191
|
+
if (effectiveType === 'bookmark' || effectiveType === 'both') {
|
|
192
|
+
const bookmarkId = generateBookmarkId();
|
|
193
|
+
const metadataJson = metadata ?? null;
|
|
194
|
+
const bookmarkRows = await neo4jService.run(CREATE_BOOKMARK_QUERY, {
|
|
195
|
+
bookmarkId,
|
|
196
|
+
projectId: resolvedProjectId,
|
|
197
|
+
sessionId,
|
|
198
|
+
agentId,
|
|
199
|
+
summary: summary,
|
|
200
|
+
workingSetNodeIds: workingSetNodeIds,
|
|
201
|
+
taskContext: taskContext,
|
|
202
|
+
findings,
|
|
203
|
+
nextSteps,
|
|
204
|
+
metadata: metadataJson,
|
|
205
|
+
});
|
|
206
|
+
if (bookmarkRows.length === 0) {
|
|
207
|
+
return createErrorResponse(effectiveType === 'both'
|
|
208
|
+
? 'Failed to create session bookmark; note was not saved.'
|
|
209
|
+
: 'Failed to create session bookmark.');
|
|
210
|
+
}
|
|
211
|
+
const bm = bookmarkRows[0];
|
|
212
|
+
const linkedNodes = typeof bm.linkedNodes === 'object' && bm.linkedNodes?.toNumber
|
|
213
|
+
? bm.linkedNodes.toNumber()
|
|
214
|
+
: (bm.linkedNodes ?? 0);
|
|
215
|
+
result.bookmark = {
|
|
216
|
+
bookmarkId: bm.id,
|
|
217
|
+
summary: bm.summary,
|
|
218
|
+
taskContext: bm.taskContext,
|
|
219
|
+
workingSetSize: workingSetNodeIds.length,
|
|
220
|
+
linkedNodes,
|
|
221
|
+
createdAt: typeof bm.createdAt === 'object' && bm.createdAt?.toNumber ? bm.createdAt.toNumber() : bm.createdAt,
|
|
222
|
+
message: `Session bookmark saved. ${linkedNodes} of ${workingSetNodeIds.length} working set nodes linked in graph.`,
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
// ── Create note ──────────────────────────────────────────────────────
|
|
226
|
+
if (effectiveType === 'note' || effectiveType === 'both') {
|
|
227
|
+
const noteId = generateNoteId();
|
|
228
|
+
const expiresAt = expiresInHours != null ? Date.now() + expiresInHours * 3600 * 1000 : null;
|
|
229
|
+
const noteRows = await neo4jService.run(CREATE_SESSION_NOTE_QUERY, {
|
|
230
|
+
noteId,
|
|
231
|
+
projectId: resolvedProjectId,
|
|
232
|
+
sessionId,
|
|
233
|
+
agentId,
|
|
234
|
+
topic: topic,
|
|
235
|
+
content: content,
|
|
236
|
+
category: category,
|
|
237
|
+
severity,
|
|
238
|
+
aboutNodeIds,
|
|
239
|
+
expiresAt,
|
|
240
|
+
});
|
|
241
|
+
if (noteRows.length === 0) {
|
|
242
|
+
return createErrorResponse('Failed to create session note.');
|
|
243
|
+
}
|
|
244
|
+
let hasEmbedding = false;
|
|
245
|
+
try {
|
|
246
|
+
await neo4jService.run(QUERIES.CREATE_SESSION_NOTES_VECTOR_INDEX(getEmbeddingDimensions()));
|
|
247
|
+
const embeddingsService = new EmbeddingsService();
|
|
248
|
+
const embeddingText = `${topic}\n\n${content}`;
|
|
249
|
+
const embedding = await embeddingsService.embedText(embeddingText);
|
|
250
|
+
await neo4jService.run(SET_NOTE_EMBEDDING_QUERY, {
|
|
251
|
+
noteId,
|
|
252
|
+
projectId: resolvedProjectId,
|
|
253
|
+
embedding,
|
|
254
|
+
});
|
|
255
|
+
hasEmbedding = true;
|
|
256
|
+
}
|
|
257
|
+
catch (embErr) {
|
|
258
|
+
await debugLog('Session save note embedding failed (non-fatal)', { error: String(embErr), noteId });
|
|
259
|
+
}
|
|
260
|
+
result.note = {
|
|
261
|
+
noteId,
|
|
262
|
+
topic: topic,
|
|
263
|
+
category: category,
|
|
264
|
+
severity,
|
|
265
|
+
hasEmbedding,
|
|
266
|
+
expiresAt: expiresAt != null ? new Date(expiresAt).toISOString() : null,
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
result.type = effectiveType;
|
|
270
|
+
return createSuccessResponse(JSON.stringify(result, null, 2));
|
|
271
|
+
}
|
|
272
|
+
catch (error) {
|
|
273
|
+
await debugLog('Session save error', { error: String(error) });
|
|
274
|
+
return createErrorResponse(error instanceof Error ? error : String(error));
|
|
275
|
+
}
|
|
276
|
+
finally {
|
|
277
|
+
await neo4jService.close();
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
};
|
|
@@ -15,7 +15,7 @@ const inputSchema = z.object({
|
|
|
15
15
|
projectPath: z.string().describe('Path to the TypeScript project root directory'),
|
|
16
16
|
tsconfigPath: z.string().describe('Path to TypeScript project tsconfig.json file'),
|
|
17
17
|
projectId: z.string().optional().describe('Optional project ID override (auto-generated from path if omitted)'),
|
|
18
|
-
debounceMs: z.number().optional().default(1000).describe('Debounce delay in milliseconds
|
|
18
|
+
debounceMs: z.number().optional().default(1000).describe('Debounce delay in milliseconds'),
|
|
19
19
|
});
|
|
20
20
|
export const createStartWatchProjectTool = (server) => {
|
|
21
21
|
server.registerTool(TOOL_NAMES.startWatchProject, {
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { Neo4jService } from '../../storage/neo4j/neo4j.service.js';
|
|
3
|
+
import { TOOL_NAMES, TOOL_METADATA } from '../constants.js';
|
|
4
|
+
import { SwarmAdvanceHandler } from '../handlers/swarm/index.js';
|
|
5
|
+
import { createErrorResponse, createSuccessResponse, resolveProjectIdOrError, debugLog } from '../utils.js';
|
|
6
|
+
export const createSwarmAdvanceTaskTool = (server) => {
|
|
7
|
+
server.registerTool(TOOL_NAMES.swarmAdvanceTask, {
|
|
8
|
+
title: TOOL_METADATA[TOOL_NAMES.swarmAdvanceTask].title,
|
|
9
|
+
description: TOOL_METADATA[TOOL_NAMES.swarmAdvanceTask].description,
|
|
10
|
+
inputSchema: {
|
|
11
|
+
projectId: z.string().describe('Project ID, name, or path'),
|
|
12
|
+
taskId: z.string().describe('Task ID to advance'),
|
|
13
|
+
agentId: z.string().describe('Your agent identifier'),
|
|
14
|
+
force: z.boolean().optional().default(false).describe('Force start from stuck claimed or available state'),
|
|
15
|
+
reason: z.string().optional().describe('Reason for force starting'),
|
|
16
|
+
},
|
|
17
|
+
}, async ({ projectId, taskId, agentId, force = false, reason }) => {
|
|
18
|
+
const neo4jService = new Neo4jService();
|
|
19
|
+
const projectResult = await resolveProjectIdOrError(projectId, neo4jService);
|
|
20
|
+
if (!projectResult.success) {
|
|
21
|
+
await neo4jService.close();
|
|
22
|
+
return projectResult.error;
|
|
23
|
+
}
|
|
24
|
+
const resolvedProjectId = projectResult.projectId;
|
|
25
|
+
try {
|
|
26
|
+
if (force) {
|
|
27
|
+
const { error, data } = await new SwarmAdvanceHandler(neo4jService).forceStart(resolvedProjectId, taskId, agentId, reason);
|
|
28
|
+
if (error) {
|
|
29
|
+
return createErrorResponse(`Cannot force_start task ${taskId}. ` +
|
|
30
|
+
(data
|
|
31
|
+
? `Current state: ${data.status}, claimedBy: ${data.claimedBy || 'none'}. ` +
|
|
32
|
+
`force_start requires status=claimed|available and you must be the claimant.`
|
|
33
|
+
: 'Task not found.'));
|
|
34
|
+
}
|
|
35
|
+
return createSuccessResponse(JSON.stringify({ action: 'force_started', taskId: data.id, status: 'in_progress' }));
|
|
36
|
+
}
|
|
37
|
+
const { error, data } = await new SwarmAdvanceHandler(neo4jService).start(resolvedProjectId, taskId, agentId);
|
|
38
|
+
if (error) {
|
|
39
|
+
return createErrorResponse(`Cannot start task ${taskId}. ` +
|
|
40
|
+
(data
|
|
41
|
+
? `Current state: ${data.status}, claimedBy: ${data.claimedBy || 'none'}. ` +
|
|
42
|
+
`Tip: Use force=true to recover from stuck claimed state, ` +
|
|
43
|
+
`or use swarm_release_task to give up the task.`
|
|
44
|
+
: 'Task not found.'));
|
|
45
|
+
}
|
|
46
|
+
return createSuccessResponse(JSON.stringify({ action: 'started', taskId: data.id, status: 'in_progress' }));
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
await debugLog('Swarm advance task error', { error: String(error) });
|
|
50
|
+
return createErrorResponse(error instanceof Error ? error : String(error));
|
|
51
|
+
}
|
|
52
|
+
finally {
|
|
53
|
+
await neo4jService.close();
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
};
|