code-graph-context 2.8.0 → 2.10.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 +101 -26
- package/dist/cli/cli.js +250 -10
- package/dist/core/embeddings/embedding-sidecar.js +244 -0
- package/dist/core/embeddings/embeddings.service.js +60 -132
- package/dist/core/embeddings/local-embeddings.service.js +41 -0
- package/dist/core/embeddings/openai-embeddings.service.js +114 -0
- package/dist/mcp/constants.js +24 -1
- package/dist/mcp/handlers/graph-generator.handler.js +6 -5
- package/dist/mcp/mcp.server.js +2 -0
- package/dist/mcp/service-init.js +24 -3
- package/dist/mcp/tools/index.js +3 -0
- package/dist/mcp/tools/search-codebase.tool.js +37 -13
- package/dist/mcp/tools/session-note.tool.js +5 -6
- 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 -4
- package/package.json +3 -1
- package/sidecar/embedding_server.py +147 -0
- package/sidecar/requirements.txt +5 -0
package/dist/mcp/mcp.server.js
CHANGED
|
@@ -16,6 +16,7 @@ const rootDir = join(__dirname, '..', '..');
|
|
|
16
16
|
dotenv.config({ path: join(rootDir, '.env'), quiet: true });
|
|
17
17
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
18
18
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
19
|
+
import { stopEmbeddingSidecar } from '../core/embeddings/embedding-sidecar.js';
|
|
19
20
|
import { MCP_SERVER_CONFIG, MESSAGES } from './constants.js';
|
|
20
21
|
import { performIncrementalParse } from './handlers/incremental-parse.handler.js';
|
|
21
22
|
import { initializeServices } from './service-init.js';
|
|
@@ -109,6 +110,7 @@ const shutdown = async (signal) => {
|
|
|
109
110
|
await logServerStats(`shutdown-${signal}`);
|
|
110
111
|
try {
|
|
111
112
|
await watchManager.stopAllWatchers();
|
|
113
|
+
await stopEmbeddingSidecar();
|
|
112
114
|
await debugLog('Shutdown complete', { signal });
|
|
113
115
|
}
|
|
114
116
|
catch (error) {
|
package/dist/mcp/service-init.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import fs from 'fs/promises';
|
|
6
6
|
import { join } from 'path';
|
|
7
7
|
import { ensureNeo4jRunning, isDockerInstalled, isDockerRunning } from '../cli/neo4j-docker.js';
|
|
8
|
+
import { isOpenAIEnabled, getEmbeddingDimensions } from '../core/embeddings/embeddings.service.js';
|
|
8
9
|
import { Neo4jService, QUERIES } from '../storage/neo4j/neo4j.service.js';
|
|
9
10
|
import { FILE_PATHS, LOG_CONFIG } from './constants.js';
|
|
10
11
|
import { initializeNaturalLanguageService } from './tools/natural-language-to-cypher.tool.js';
|
|
@@ -13,12 +14,32 @@ import { debugLog } from './utils.js';
|
|
|
13
14
|
* Log startup warnings for missing configuration
|
|
14
15
|
*/
|
|
15
16
|
const checkConfiguration = async () => {
|
|
16
|
-
|
|
17
|
+
const openai = isOpenAIEnabled();
|
|
18
|
+
const dims = getEmbeddingDimensions();
|
|
19
|
+
const provider = openai ? 'openai' : 'local';
|
|
20
|
+
console.error(JSON.stringify({
|
|
21
|
+
level: 'info',
|
|
22
|
+
message: `[code-graph-context] Embedding provider: ${provider} (${dims} dimensions)`,
|
|
23
|
+
}));
|
|
24
|
+
await debugLog('Embedding configuration', { provider, dimensions: dims });
|
|
25
|
+
if (openai && !process.env.OPENAI_API_KEY) {
|
|
17
26
|
console.error(JSON.stringify({
|
|
18
27
|
level: 'warn',
|
|
19
|
-
message: '[code-graph-context] OPENAI_API_KEY not set.
|
|
28
|
+
message: '[code-graph-context] OPENAI_ENABLED=true but OPENAI_API_KEY not set. Embedding calls will fail.',
|
|
20
29
|
}));
|
|
21
|
-
await debugLog('Configuration warning', { warning: 'OPENAI_API_KEY not set' });
|
|
30
|
+
await debugLog('Configuration warning', { warning: 'OPENAI_ENABLED=true but OPENAI_API_KEY not set' });
|
|
31
|
+
}
|
|
32
|
+
if (!openai) {
|
|
33
|
+
console.error(JSON.stringify({
|
|
34
|
+
level: 'info',
|
|
35
|
+
message: '[code-graph-context] Using local embeddings (Python sidecar). Starts on first embedding request.',
|
|
36
|
+
}));
|
|
37
|
+
if (!process.env.OPENAI_API_KEY) {
|
|
38
|
+
console.error(JSON.stringify({
|
|
39
|
+
level: 'info',
|
|
40
|
+
message: '[code-graph-context] natural_language_to_cypher requires OPENAI_API_KEY and is unavailable.',
|
|
41
|
+
}));
|
|
42
|
+
}
|
|
22
43
|
}
|
|
23
44
|
};
|
|
24
45
|
/**
|
package/dist/mcp/tools/index.js
CHANGED
|
@@ -21,6 +21,7 @@ import { createSwarmClaimTaskTool } from './swarm-claim-task.tool.js';
|
|
|
21
21
|
import { createSwarmCleanupTool } from './swarm-cleanup.tool.js';
|
|
22
22
|
import { createSwarmCompleteTaskTool } from './swarm-complete-task.tool.js';
|
|
23
23
|
import { createSwarmGetTasksTool } from './swarm-get-tasks.tool.js';
|
|
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,6 +74,8 @@ export const registerAllTools = (server) => {
|
|
|
73
74
|
createSwarmClaimTaskTool(server);
|
|
74
75
|
createSwarmCompleteTaskTool(server);
|
|
75
76
|
createSwarmGetTasksTool(server);
|
|
77
|
+
// Register swarm messaging tools (direct agent-to-agent communication)
|
|
78
|
+
createSwarmMessageTool(server);
|
|
76
79
|
// Register session bookmark tools (cross-session context continuity)
|
|
77
80
|
createSaveSessionBookmarkTool(server);
|
|
78
81
|
createRestoreSessionBookmarkTool(server);
|
|
@@ -39,6 +39,12 @@ export const createSearchCodebaseTool = (server) => {
|
|
|
39
39
|
.optional()
|
|
40
40
|
.describe(`Length of code snippets to include (default: ${DEFAULTS.codeSnippetLength})`)
|
|
41
41
|
.default(DEFAULTS.codeSnippetLength),
|
|
42
|
+
topK: z
|
|
43
|
+
.number()
|
|
44
|
+
.int()
|
|
45
|
+
.optional()
|
|
46
|
+
.describe('Number of top vector matches to return (default: 3, max: 10). The best match is traversed; others shown as alternatives.')
|
|
47
|
+
.default(3),
|
|
42
48
|
minSimilarity: z
|
|
43
49
|
.number()
|
|
44
50
|
.optional()
|
|
@@ -50,7 +56,7 @@ export const createSearchCodebaseTool = (server) => {
|
|
|
50
56
|
.describe('Use weighted traversal strategy that scores each node for relevance (default: false)')
|
|
51
57
|
.default(true),
|
|
52
58
|
},
|
|
53
|
-
}, async ({ projectId, query, maxDepth = DEFAULTS.traversalDepth, maxNodesPerChain = 5, skip = 0, includeCode = true, snippetLength = DEFAULTS.codeSnippetLength, minSimilarity = 0.65, useWeightedTraversal = true, }) => {
|
|
59
|
+
}, async ({ projectId, query, maxDepth = DEFAULTS.traversalDepth, maxNodesPerChain = 5, skip = 0, includeCode = true, snippetLength = DEFAULTS.codeSnippetLength, topK = 3, minSimilarity = 0.65, useWeightedTraversal = true, }) => {
|
|
54
60
|
const neo4jService = new Neo4jService();
|
|
55
61
|
try {
|
|
56
62
|
// Resolve project ID from name, path, or ID
|
|
@@ -63,11 +69,12 @@ export const createSearchCodebaseTool = (server) => {
|
|
|
63
69
|
const sanitizedMaxNodesPerChain = sanitizeNumericInput(maxNodesPerChain, 5);
|
|
64
70
|
const sanitizedSkip = sanitizeNumericInput(skip, 0);
|
|
65
71
|
const sanitizedSnippetLength = sanitizeNumericInput(snippetLength, DEFAULTS.codeSnippetLength);
|
|
72
|
+
const sanitizedTopK = sanitizeNumericInput(topK, 3, 10);
|
|
66
73
|
const embeddingsService = new EmbeddingsService();
|
|
67
74
|
const traversalHandler = new TraversalHandler(neo4jService);
|
|
68
75
|
const embedding = await embeddingsService.embedText(query);
|
|
69
76
|
const vectorResults = await neo4jService.run(QUERIES.VECTOR_SEARCH, {
|
|
70
|
-
limit:
|
|
77
|
+
limit: sanitizedTopK,
|
|
71
78
|
embedding,
|
|
72
79
|
projectId: resolvedProjectId,
|
|
73
80
|
fetchMultiplier: 10,
|
|
@@ -77,28 +84,45 @@ export const createSearchCodebaseTool = (server) => {
|
|
|
77
84
|
return createSuccessResponse(`No code found with similarity >= ${minSimilarity}. ` +
|
|
78
85
|
`Try rephrasing your query or lowering the minSimilarity threshold. Query: "${query}"`);
|
|
79
86
|
}
|
|
80
|
-
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
return createSuccessResponse(`No sufficiently relevant code found. Best match score: ${similarityScore.toFixed(3)} ` +
|
|
87
|
+
// Filter results that meet the similarity threshold
|
|
88
|
+
const qualifiedResults = vectorResults.filter((r) => r.score >= minSimilarity);
|
|
89
|
+
if (qualifiedResults.length === 0) {
|
|
90
|
+
const bestScore = vectorResults[0].score;
|
|
91
|
+
return createSuccessResponse(`No sufficiently relevant code found. Best match score: ${bestScore.toFixed(3)} ` +
|
|
86
92
|
`(threshold: ${minSimilarity}). Try rephrasing your query.`);
|
|
87
93
|
}
|
|
88
|
-
//
|
|
89
|
-
const
|
|
90
|
-
|
|
94
|
+
// Best match — traverse from this node
|
|
95
|
+
const bestMatch = qualifiedResults[0];
|
|
96
|
+
const nodeId = bestMatch.node.properties.id;
|
|
97
|
+
const bestScore = bestMatch.score.toFixed(3);
|
|
98
|
+
// Build alternative matches summary for the response
|
|
99
|
+
const alternatives = qualifiedResults.slice(1);
|
|
100
|
+
const altLines = alternatives.map((r) => {
|
|
101
|
+
const props = r.node.properties;
|
|
102
|
+
const name = props.name ?? props.id;
|
|
103
|
+
const file = props.filePath ? ` (${props.filePath})` : '';
|
|
104
|
+
return ` - ${name}${file} [score: ${r.score.toFixed(3)}, id: ${props.id}]`;
|
|
105
|
+
});
|
|
106
|
+
const altSection = altLines.length > 0
|
|
107
|
+
? `\n\nAlternative matches (use traverse_from_node to explore):\n${altLines.join('\n')}`
|
|
108
|
+
: '';
|
|
109
|
+
const traversalResult = await traversalHandler.traverseFromNode(nodeId, embedding, {
|
|
91
110
|
projectId: resolvedProjectId,
|
|
92
111
|
maxDepth: sanitizedMaxDepth,
|
|
93
|
-
direction: 'BOTH',
|
|
112
|
+
direction: 'BOTH',
|
|
94
113
|
includeCode,
|
|
95
114
|
maxNodesPerChain: sanitizedMaxNodesPerChain,
|
|
96
115
|
skip: sanitizedSkip,
|
|
97
116
|
summaryOnly: false,
|
|
98
117
|
snippetLength: sanitizedSnippetLength,
|
|
99
|
-
title: `Search Results (
|
|
118
|
+
title: `Search Results (${qualifiedResults.length} matches, best: ${bestScore}) - Traversing from: ${nodeId}`,
|
|
100
119
|
useWeightedTraversal,
|
|
101
120
|
});
|
|
121
|
+
// Append alternatives to the traversal response
|
|
122
|
+
if (altSection && traversalResult.content?.[0]?.type === 'text') {
|
|
123
|
+
traversalResult.content[0].text += altSection;
|
|
124
|
+
}
|
|
125
|
+
return traversalResult;
|
|
102
126
|
}
|
|
103
127
|
catch (error) {
|
|
104
128
|
console.error('Search codebase error:', error);
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Save and recall cross-session observations, decisions, and insights
|
|
4
4
|
*/
|
|
5
5
|
import { z } from 'zod';
|
|
6
|
-
import { EmbeddingsService } from '../../core/embeddings/embeddings.service.js';
|
|
6
|
+
import { EmbeddingsService, getEmbeddingDimensions } from '../../core/embeddings/embeddings.service.js';
|
|
7
7
|
import { Neo4jService, QUERIES } from '../../storage/neo4j/neo4j.service.js';
|
|
8
8
|
import { TOOL_NAMES, TOOL_METADATA } from '../constants.js';
|
|
9
9
|
import { createErrorResponse, createSuccessResponse, resolveProjectIdOrError, debugLog } from '../utils.js';
|
|
@@ -31,17 +31,16 @@ const CREATE_SESSION_NOTE_QUERY = `
|
|
|
31
31
|
// Link to referenced code nodes (filter out internal coordination nodes)
|
|
32
32
|
WITH n
|
|
33
33
|
UNWIND CASE WHEN size($aboutNodeIds) = 0 THEN [null] ELSE $aboutNodeIds END AS aboutNodeId
|
|
34
|
-
WITH n, aboutNodeId
|
|
35
|
-
WHERE aboutNodeId IS NOT NULL
|
|
36
34
|
OPTIONAL MATCH (target)
|
|
37
|
-
WHERE
|
|
35
|
+
WHERE aboutNodeId IS NOT NULL
|
|
36
|
+
AND target.id = aboutNodeId
|
|
38
37
|
AND target.projectId = $projectId
|
|
39
38
|
AND NOT target:SessionNote
|
|
40
39
|
AND NOT target:SessionBookmark
|
|
41
40
|
AND NOT target:Pheromone
|
|
42
41
|
AND NOT target:SwarmTask
|
|
43
42
|
WITH n, collect(target) AS targets
|
|
44
|
-
FOREACH (t IN targets | MERGE (n)-[:ABOUT]->(t))
|
|
43
|
+
FOREACH (t IN [x IN targets WHERE x IS NOT NULL] | MERGE (n)-[:ABOUT]->(t))
|
|
45
44
|
|
|
46
45
|
// Link to the latest SessionBookmark for this session (if one exists)
|
|
47
46
|
WITH n
|
|
@@ -194,7 +193,7 @@ export const createSaveSessionNoteTool = (server) => {
|
|
|
194
193
|
// Ensure vector index exists (idempotent — IF NOT EXISTS)
|
|
195
194
|
let hasEmbedding = false;
|
|
196
195
|
try {
|
|
197
|
-
await neo4jService.run(QUERIES.CREATE_SESSION_NOTES_VECTOR_INDEX);
|
|
196
|
+
await neo4jService.run(QUERIES.CREATE_SESSION_NOTES_VECTOR_INDEX(getEmbeddingDimensions()));
|
|
198
197
|
const embeddingsService = new EmbeddingsService();
|
|
199
198
|
const embeddingText = `${topic}\n\n${content}`;
|
|
200
199
|
const embedding = await embeddingsService.embedText(embeddingText);
|
|
@@ -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
|
/**
|