@@ -9,7 +9,7 @@ import { log } from "../../shared/logger"
99import { ConcurrencyManager } from "./concurrency"
1010import type { BackgroundTaskConfig } from "../../config/schema"
1111
12- import { subagentSessions } from "../claude-code-session-state"
12+ import { subagentSessions , clearSessionAgent } from "../claude-code-session-state"
1313import { getTaskToastManager } from "../task-toast-manager"
1414
1515const TASK_TTL_MS = 30 * 60 * 1000
@@ -468,6 +468,17 @@ export class BackgroundManager {
468468 }
469469 task . status = "completed"
470470 task . completedAt = new Date ( )
471+
472+ // Release concurrency key immediately on completion to free up slots
473+ if ( task . concurrencyKey ) {
474+ this . concurrencyManager . release ( task . concurrencyKey )
475+ task . concurrencyKey = undefined // Prevent double-release
476+ }
477+
478+ // Clean up session tracking to prevent memory leaks
479+ subagentSessions . delete ( sessionID )
480+ clearSessionAgent ( sessionID )
481+
471482 this . markForNotification ( task )
472483 this . notifyParentSession ( task ) . catch ( err => {
473484 log ( "[background-agent] Error notifying parent on completion:" , err )
@@ -496,6 +507,7 @@ export class BackgroundManager {
496507 this . tasks . delete ( task . id )
497508 this . clearNotificationsForTask ( task . id )
498509 subagentSessions . delete ( sessionID )
510+ clearSessionAgent ( sessionID )
499511 }
500512 }
501513
@@ -516,6 +528,9 @@ export class BackgroundManager {
516528 /**
517529 * Validates that a session has actual assistant/tool output before marking complete.
518530 * Prevents premature completion when session.idle fires before agent responds.
531+ *
532+ * NOTE: This is used in pollRunningTasks() but NOT in session.idle handler.
533+ * Using it in session.idle was causing stuck tasks due to timing issues.
519534 */
520535 private async validateSessionHasOutput ( sessionID : string ) : Promise < boolean > {
521536 try {
@@ -546,18 +561,18 @@ export class BackgroundManager {
546561 const hasContent = messages . some ( ( m : any ) => {
547562 if ( m . info ?. role !== "assistant" && m . info ?. role !== "tool" ) return false
548563 const parts = m . parts ?? [ ]
549- // eslint-disable-next-line @typescript-eslint/no-explicit-any
550- return parts . some ( ( p : any ) =>
551- // Text content (final output)
552- ( p . type === "text" && p . text && p . text . trim ( ) . length > 0 ) ||
553- // Reasoning content (thinking blocks)
554- ( p . type === "reasoning" && p . text && p . text . trim ( ) . length > 0 ) ||
555- // Tool calls (indicates work was done)
556- p . type === "tool" ||
557- // Tool results (output from executed tools) - important for tool-only tasks
558- ( p . type === "tool_result" && p . content &&
559- ( typeof p . content === "string" ? p . content . trim ( ) . length > 0 : p . content . length > 0 ) )
560- )
564+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
565+ return parts . some ( ( p : any ) =>
566+ // Text content (final output)
567+ ( p . type === "text" && p . text && p . text . trim ( ) . length > 0 ) ||
568+ // Reasoning content (thinking blocks)
569+ ( p . type === "reasoning" && p . text && p . text . trim ( ) . length > 0 ) ||
570+ // Tool calls (indicates work was done)
571+ p . type === "tool" ||
572+ // Tool results (output from executed tools) - important for tool-only tasks
573+ ( p . type === "tool_result" && p . content &&
574+ ( typeof p . content === "string" ? p . content . trim ( ) . length > 0 : p . content . length > 0 ) )
575+ )
561576 } )
562577
563578 if ( ! hasContent ) {
0 commit comments