code-graph-context 2.5.0 → 2.5.2

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 CHANGED
@@ -616,12 +616,6 @@ MIT - see [LICENSE](LICENSE)
616
616
 
617
617
  ---
618
618
 
619
- ## Documentation
620
-
621
- - [Graph + LLM Inference Research](docs/GRAPH_LLM_INFERENCE_RESEARCH.md) - Research on GNN embeddings, Graph Neural Prompting, and inference integration
622
-
623
- ---
624
-
625
619
  ## Links
626
620
 
627
621
  - [Issues](https://github.com/drewdrewH/code-graph-context/issues)
@@ -20,8 +20,14 @@ const CLAIM_TASK_BY_ID_QUERY = `
20
20
  WHERE dep.status <> 'completed'
21
21
  WITH t, count(dep) as incompleteDeps
22
22
 
23
- // Only claim if no incomplete dependencies (or task was already available)
24
- WHERE incompleteDeps = 0 OR t.status = 'available'
23
+ // Only claim if all dependencies are complete
24
+ WHERE incompleteDeps = 0
25
+
26
+ // Acquire exclusive lock to prevent race conditions
27
+ CALL apoc.lock.nodes([t])
28
+
29
+ // Double-check status after acquiring lock
30
+ WITH t WHERE t.status IN ['available', 'blocked']
25
31
 
26
32
  // Atomic claim
27
33
  SET t.status = 'claimed',
@@ -57,6 +63,7 @@ const CLAIM_TASK_BY_ID_QUERY = `
57
63
  `;
58
64
  /**
59
65
  * Query to claim the highest priority available task matching criteria
66
+ * Uses APOC locking to prevent race conditions between parallel workers
60
67
  */
61
68
  const CLAIM_NEXT_TASK_QUERY = `
62
69
  // Find available tasks not blocked by dependencies
@@ -75,6 +82,12 @@ const CLAIM_NEXT_TASK_QUERY = `
75
82
  ORDER BY t.priorityScore DESC, t.createdAt ASC
76
83
  LIMIT 1
77
84
 
85
+ // Acquire exclusive lock to prevent race conditions
86
+ CALL apoc.lock.nodes([t])
87
+
88
+ // Double-check status after acquiring lock (another worker may have claimed it)
89
+ WITH t WHERE t.status = 'available'
90
+
78
91
  // Atomic claim
79
92
  SET t.status = 'claimed',
80
93
  t.claimedBy = $agentId,
@@ -11,7 +11,7 @@ import { createErrorResponse, createSuccessResponse, resolveProjectIdOrError, de
11
11
  */
12
12
  const COMPLETE_TASK_QUERY = `
13
13
  MATCH (t:SwarmTask {id: $taskId, projectId: $projectId})
14
- WHERE t.status = 'in_progress' AND t.claimedBy = $agentId
14
+ WHERE t.status IN ['in_progress', 'claimed'] AND t.claimedBy = $agentId
15
15
 
16
16
  SET t.status = 'completed',
17
17
  t.completedAt = timestamp(),
@@ -29,17 +29,21 @@ const COMPLETE_TASK_QUERY = `
29
29
 
30
30
  // Check if waiting tasks now have all dependencies completed
31
31
  WITH t, collect(waiting) as waitingTasks
32
+
33
+ // Unblock tasks that have all dependencies met
32
34
  UNWIND (CASE WHEN size(waitingTasks) = 0 THEN [null] ELSE waitingTasks END) as waiting
33
35
  OPTIONAL MATCH (waiting)-[:DEPENDS_ON]->(otherDep:SwarmTask)
34
36
  WHERE otherDep.status <> 'completed' AND otherDep.id <> t.id
35
37
  WITH t, waiting, count(otherDep) as remainingDeps
36
- WHERE waiting IS NOT NULL AND remainingDeps = 0
37
38
 
38
- // Unblock tasks that now have all dependencies met
39
- SET waiting.status = 'available',
40
- waiting.updatedAt = timestamp()
39
+ // Update status for tasks with no remaining deps (but don't filter out the row)
40
+ FOREACH (_ IN CASE WHEN waiting IS NOT NULL AND remainingDeps = 0 THEN [1] ELSE [] END |
41
+ SET waiting.status = 'available', waiting.updatedAt = timestamp()
42
+ )
41
43
 
42
- WITH t, collect(waiting.id) as unblockedTaskIds
44
+ WITH t, CASE WHEN waiting IS NOT NULL AND remainingDeps = 0 THEN waiting.id ELSE null END as unblockedId
45
+ WITH t, collect(unblockedId) as allUnblockedIds
46
+ WITH t, [id IN allUnblockedIds WHERE id IS NOT NULL] as unblockedTaskIds
43
47
 
44
48
  RETURN t.id as id,
45
49
  t.title as title,
@@ -112,16 +116,21 @@ const APPROVE_TASK_QUERY = `
112
116
 
113
117
  // Check if waiting tasks now have all dependencies completed
114
118
  WITH t, collect(waiting) as waitingTasks
119
+
120
+ // Unblock tasks that have all dependencies met
115
121
  UNWIND (CASE WHEN size(waitingTasks) = 0 THEN [null] ELSE waitingTasks END) as waiting
116
122
  OPTIONAL MATCH (waiting)-[:DEPENDS_ON]->(otherDep:SwarmTask)
117
123
  WHERE otherDep.status <> 'completed' AND otherDep.id <> t.id
118
124
  WITH t, waiting, count(otherDep) as remainingDeps
119
- WHERE waiting IS NOT NULL AND remainingDeps = 0
120
125
 
121
- SET waiting.status = 'available',
122
- waiting.updatedAt = timestamp()
126
+ // Update status for tasks with no remaining deps (but don't filter out the row)
127
+ FOREACH (_ IN CASE WHEN waiting IS NOT NULL AND remainingDeps = 0 THEN [1] ELSE [] END |
128
+ SET waiting.status = 'available', waiting.updatedAt = timestamp()
129
+ )
123
130
 
124
- WITH t, collect(waiting.id) as unblockedTaskIds
131
+ WITH t, CASE WHEN waiting IS NOT NULL AND remainingDeps = 0 THEN waiting.id ELSE null END as unblockedId
132
+ WITH t, collect(unblockedId) as allUnblockedIds
133
+ WITH t, [id IN allUnblockedIds WHERE id IS NOT NULL] as unblockedTaskIds
125
134
 
126
135
  RETURN t.id as id,
127
136
  t.title as title,
@@ -128,7 +128,7 @@ const GET_ACTIVE_WORKERS_QUERY = `
128
128
  WHERE ($swarmId IS NULL OR p.swarmId = $swarmId)
129
129
  AND p.type IN ['modifying', 'claiming']
130
130
  WITH p.agentId as agentId, p.type as type,
131
- max(p.updatedAt) as lastActivity,
131
+ max(p.timestamp) as lastActivity,
132
132
  count(p) as nodeCount
133
133
  RETURN agentId, type,
134
134
  lastActivity,
@@ -14,12 +14,12 @@ import { Neo4jService } from '../../storage/neo4j/neo4j.service.js';
14
14
  import { TOOL_NAMES, TOOL_METADATA } from '../constants.js';
15
15
  import { TaskDecompositionHandler, } from '../handlers/task-decomposition.handler.js';
16
16
  import { createErrorResponse, createSuccessResponse, resolveProjectIdOrError, debugLog } from '../utils.js';
17
- import { TASK_PRIORITIES, generateSwarmId, ORCHESTRATOR_CONFIG, } from './swarm-constants.js';
17
+ import { TASK_PRIORITIES, generateSwarmId, ORCHESTRATOR_CONFIG, getHalfLife, } from './swarm-constants.js';
18
18
  /**
19
19
  * Query to search for nodes matching the task description
20
20
  */
21
21
  const SEMANTIC_SEARCH_QUERY = `
22
- CALL db.index.vector.queryNodes('code_embeddings', toInteger($limit), $embedding)
22
+ CALL db.index.vector.queryNodes('embedded_nodes_idx', toInteger($limit), $embedding)
23
23
  YIELD node, score
24
24
  WHERE node.projectId = $projectId
25
25
  AND score >= $minSimilarity
@@ -73,14 +73,15 @@ const CREATE_PHEROMONE_QUERY = `
73
73
  projectId: $projectId
74
74
  })
75
75
  ON CREATE SET
76
+ p.id = randomUUID(),
76
77
  p.swarmId = $swarmId,
77
78
  p.intensity = $intensity,
78
- p.createdAt = timestamp(),
79
- p.updatedAt = timestamp(),
79
+ p.timestamp = timestamp(),
80
+ p.halfLife = $halfLife,
80
81
  p.data = $data
81
82
  ON MATCH SET
82
83
  p.intensity = $intensity,
83
- p.updatedAt = timestamp(),
84
+ p.timestamp = timestamp(),
84
85
  p.data = $data
85
86
  MERGE (p)-[:MARKS]->(target)
86
87
  RETURN p.nodeId AS nodeId
@@ -107,6 +108,25 @@ const CREATE_TASK_QUERY = `
107
108
  updatedAt: timestamp(),
108
109
  metadata: $metadata
109
110
  })
111
+
112
+ // Link to target code nodes if they exist
113
+ WITH t
114
+ OPTIONAL MATCH (target)
115
+ WHERE target.id IN $targetNodeIds
116
+ AND target.projectId = $projectId
117
+ AND NOT target:SwarmTask
118
+ AND NOT target:Pheromone
119
+ WITH t, collect(DISTINCT target) as targets
120
+ FOREACH (target IN targets | MERGE (t)-[:TARGETS]->(target))
121
+
122
+ // Link to dependency tasks if they exist
123
+ WITH t
124
+ OPTIONAL MATCH (dep:SwarmTask)
125
+ WHERE dep.id IN $dependencies
126
+ AND dep.projectId = $projectId
127
+ WITH t, collect(DISTINCT dep) as deps
128
+ FOREACH (dep IN deps | MERGE (t)-[:DEPENDS_ON]->(dep))
129
+
110
130
  RETURN t.id AS id
111
131
  `;
112
132
  export const createSwarmOrchestrateTool = (server) => {
@@ -278,6 +298,7 @@ export const createSwarmOrchestrateTool = (server) => {
278
298
  swarmId,
279
299
  type: 'proposal',
280
300
  intensity: 1.0,
301
+ halfLife: getHalfLife('proposal'),
281
302
  data: JSON.stringify({ task, swarmId }),
282
303
  });
283
304
  }
@@ -77,6 +77,14 @@ const CHECK_DEPENDENCIES_QUERY = `
77
77
  size(incompleteDeps) as incompleteDeps,
78
78
  [d IN incompleteDeps | {id: d.id, title: d.title, status: d.status}] as blockedBy
79
79
  `;
80
+ /**
81
+ * Query to update task status to blocked
82
+ */
83
+ const SET_TASK_BLOCKED_QUERY = `
84
+ MATCH (t:SwarmTask {id: $taskId, projectId: $projectId})
85
+ SET t.status = 'blocked', t.updatedAt = timestamp()
86
+ RETURN t.id as id
87
+ `;
80
88
  export const createSwarmPostTaskTool = (server) => {
81
89
  server.registerTool(TOOL_NAMES.swarmPostTask, {
82
90
  title: TOOL_METADATA[TOOL_NAMES.swarmPostTask].title,
@@ -180,6 +188,13 @@ export const createSwarmPostTaskTool = (server) => {
180
188
  }
181
189
  }
182
190
  const isBlocked = dependencyStatus.incompleteDeps > 0;
191
+ // Update task status to blocked if there are incomplete dependencies
192
+ if (isBlocked) {
193
+ await neo4jService.run(SET_TASK_BLOCKED_QUERY, {
194
+ taskId,
195
+ projectId: resolvedProjectId,
196
+ });
197
+ }
183
198
  return createSuccessResponse(JSON.stringify({
184
199
  success: true,
185
200
  task: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "code-graph-context",
3
- "version": "2.5.0",
3
+ "version": "2.5.2",
4
4
  "description": "MCP server that builds code graphs to provide rich context to LLMs",
5
5
  "type": "module",
6
6
  "homepage": "https://github.com/drewdrewH/code-graph-context#readme",