@@ -201,16 +201,26 @@ class CognitiveLoadBalancer extends EventEmitter {
201201 * @param {number } [profile.processingSpeed=1.0] - Processing speed multiplier
202202 * @returns {Object } Registered agent profile
203203 * @throws {Error } If agentId is not a non-empty string
204+ * @throws {Error } If profile is provided but is not an object
204205 */
205206 registerAgent ( agentId , profile = { } ) {
206207 if ( ! agentId || typeof agentId !== 'string' ) {
207208 throw new Error ( 'agentId must be a non-empty string' ) ;
208209 }
209210
210- const agentProfile = createAgentProfile ( agentId , profile ) ;
211+ // Validate profile overrides must be object or undefined
212+ if ( profile !== undefined && profile !== null && typeof profile !== 'object' ) {
213+ throw new Error ( 'profile must be an object or undefined' ) ;
214+ }
215+
216+ const agentProfile = createAgentProfile ( agentId , profile || { } ) ;
211217 this . agents . set ( agentId , agentProfile ) ;
212218
213219 this . emit ( 'agent:registered' , { agentId, profile : agentProfile } ) ;
220+
221+ // Process queue when new agent registers so queued tasks get assigned
222+ this . _processQueue ( ) ;
223+
214224 return agentProfile ;
215225 }
216226
@@ -261,13 +271,21 @@ class CognitiveLoadBalancer extends EventEmitter {
261271 * @param {number } [taskInput.complexity=5] - Complexity 1-10
262272 * @param {string[] } [taskInput.requiredSpecialties=[]] - Required specialties
263273 * @returns {Object } Submission result with taskId and assignedTo
274+ * @throws {Error } If task input is not a non-null object
275+ * @throws {Error } If task with same ID already exists
264276 */
265277 submitTask ( taskInput ) {
266278 if ( ! taskInput || typeof taskInput !== 'object' ) {
267279 throw new Error ( 'Task must be a non-null object' ) ;
268280 }
269281
270282 const task = createTask ( taskInput ) ;
283+
284+ // Reject duplicate task IDs
285+ if ( this . tasks . has ( task . id ) ) {
286+ throw new Error ( `Task '${ task . id } ' already exists` ) ;
287+ }
288+
271289 this . tasks . set ( task . id , task ) ;
272290 this . metrics . totalSubmitted ++ ;
273291
@@ -308,13 +326,22 @@ class CognitiveLoadBalancer extends EventEmitter {
308326 * @param {string } agentId - Target agent
309327 * @returns {Object } Assignment result
310328 * @throws {Error } If task or agent not found
329+ * @throws {Error } If task is already completed or failed
311330 */
312331 assignTask ( taskId , agentId ) {
313332 const task = this . tasks . get ( taskId ) ;
314333 if ( ! task ) {
315334 throw new Error ( `Task '${ taskId } ' not found` ) ;
316335 }
317336
337+ // Enforce task state transitions - cannot assign completed/failed tasks
338+ if ( task . status === TaskStatus . COMPLETED ) {
339+ throw new Error ( `Task '${ taskId } ' is already completed` ) ;
340+ }
341+ if ( task . status === TaskStatus . FAILED ) {
342+ throw new Error ( `Task '${ taskId } ' is already failed` ) ;
343+ }
344+
318345 const agent = this . agents . get ( agentId ) ;
319346 if ( ! agent ) {
320347 throw new Error ( `Agent '${ agentId } ' not found` ) ;
@@ -348,13 +375,22 @@ class CognitiveLoadBalancer extends EventEmitter {
348375 * @param {* } [result=null] - Task result
349376 * @returns {Object } Completion info
350377 * @throws {Error } If task not found
378+ * @throws {Error } If task is already completed or failed
351379 */
352- completeTask ( taskId , result = null ) {
380+ async completeTask ( taskId , result = null ) {
353381 const task = this . tasks . get ( taskId ) ;
354382 if ( ! task ) {
355383 throw new Error ( `Task '${ taskId } ' not found` ) ;
356384 }
357385
386+ // Enforce task state transitions
387+ if ( task . status === TaskStatus . COMPLETED ) {
388+ throw new Error ( `Task '${ taskId } ' is already completed` ) ;
389+ }
390+ if ( task . status === TaskStatus . FAILED ) {
391+ throw new Error ( `Task '${ taskId } ' is already failed` ) ;
392+ }
393+
358394 task . status = TaskStatus . COMPLETED ;
359395 task . completedAt = Date . now ( ) ;
360396 task . result = result ;
@@ -375,8 +411,8 @@ class CognitiveLoadBalancer extends EventEmitter {
375411 // Try to process queue after freeing capacity
376412 this . _processQueue ( ) ;
377413
378- // Persist metrics
379- this . _persistMetrics ( ) ;
414+ // Await metrics persistence instead of fire-and-forget
415+ await this . _persistMetrics ( ) ;
380416
381417 return {
382418 taskId,
@@ -391,13 +427,22 @@ class CognitiveLoadBalancer extends EventEmitter {
391427 * @param {string|Error } [error='Unknown error'] - Error description
392428 * @returns {Object } Failure info
393429 * @throws {Error } If task not found
430+ * @throws {Error } If task is already completed or failed
394431 */
395- failTask ( taskId , error = 'Unknown error' ) {
432+ async failTask ( taskId , error = 'Unknown error' ) {
396433 const task = this . tasks . get ( taskId ) ;
397434 if ( ! task ) {
398435 throw new Error ( `Task '${ taskId } ' not found` ) ;
399436 }
400437
438+ // Enforce task state transitions
439+ if ( task . status === TaskStatus . COMPLETED ) {
440+ throw new Error ( `Task '${ taskId } ' is already completed` ) ;
441+ }
442+ if ( task . status === TaskStatus . FAILED ) {
443+ throw new Error ( `Task '${ taskId } ' is already failed` ) ;
444+ }
445+
401446 const errorMessage = error instanceof Error ? error . message : String ( error ) ;
402447 task . status = TaskStatus . FAILED ;
403448 task . completedAt = Date . now ( ) ;
@@ -416,8 +461,8 @@ class CognitiveLoadBalancer extends EventEmitter {
416461 // Try to process queue after freeing capacity
417462 this . _processQueue ( ) ;
418463
419- // Persist metrics
420- this . _persistMetrics ( ) ;
464+ // Await metrics persistence instead of fire-and-forget
465+ await this . _persistMetrics ( ) ;
421466
422467 return {
423468 taskId,
@@ -727,6 +772,9 @@ class CognitiveLoadBalancer extends EventEmitter {
727772 * @private
728773 */
729774 _assignTaskToAgent ( task , agent ) {
775+ // Save previous status for transition-only events
776+ const previousStatus = agent . status ;
777+
730778 task . assignedTo = agent . id ;
731779 task . status = TaskStatus . ASSIGNED ;
732780 task . startedAt = Date . now ( ) ;
@@ -737,9 +785,9 @@ class CognitiveLoadBalancer extends EventEmitter {
737785 this . _updateAgentStatus ( agent ) ;
738786 this . emit ( 'task:assigned' , { taskId : task . id , agentId : agent . id } ) ;
739787
740- // Check if agent became overloaded
741- const loadPct = agent . maxLoad > 0 ? ( agent . currentLoad / agent . maxLoad ) * 100 : 100 ;
742- if ( loadPct >= OVERLOAD_THRESHOLD ) {
788+ // Only emit agent:overloaded on actual status transition
789+ if ( agent . status === AgentStatus . OVERLOADED && previousStatus !== AgentStatus . OVERLOADED ) {
790+ const loadPct = agent . maxLoad > 0 ? ( agent . currentLoad / agent . maxLoad ) * 100 : 100 ;
743791 this . emit ( 'agent:overloaded' , { agentId : agent . id , load : loadPct } ) ;
744792 }
745793 }
@@ -751,6 +799,9 @@ class CognitiveLoadBalancer extends EventEmitter {
751799 * @private
752800 */
753801 _removeTaskFromAgent ( agent , taskId ) {
802+ // Save previous status for transition-only events
803+ const previousStatus = agent . status ;
804+
754805 const idx = agent . activeTasks . indexOf ( taskId ) ;
755806 if ( idx !== - 1 ) {
756807 agent . activeTasks . splice ( idx , 1 ) ;
@@ -763,9 +814,10 @@ class CognitiveLoadBalancer extends EventEmitter {
763814
764815 this . _updateAgentStatus ( agent ) ;
765816
766- // Check if agent became available again
767- const loadPct = agent . maxLoad > 0 ? ( agent . currentLoad / agent . maxLoad ) * 100 : 100 ;
768- if ( loadPct < OVERLOAD_THRESHOLD && agent . status !== AgentStatus . OFFLINE ) {
817+ // Only emit agent:available on transition FROM overloaded
818+ if ( agent . status !== AgentStatus . OVERLOADED && agent . status !== AgentStatus . OFFLINE
819+ && previousStatus === AgentStatus . OVERLOADED ) {
820+ const loadPct = agent . maxLoad > 0 ? ( agent . currentLoad / agent . maxLoad ) * 100 : 100 ;
769821 this . emit ( 'agent:available' , { agentId : agent . id , load : loadPct } ) ;
770822 }
771823 }
@@ -914,8 +966,9 @@ class CognitiveLoadBalancer extends EventEmitter {
914966
915967 await fs . mkdir ( metricsDir , { recursive : true } ) ;
916968 await fs . writeFile ( metricsPath , JSON . stringify ( this . getMetrics ( ) , null , 2 ) , 'utf8' ) ;
917- } catch {
918- // Silently ignore persistence errors in production
969+ } catch ( err ) {
970+ // Log persistence errors with context instead of silently ignoring
971+ console . error ( `Failed to persist load balancer metrics: ${ err . message } ` ) ;
919972 }
920973 }
921974}
0 commit comments