Răsfoiți Sursa

fix(abilities): fix cancel/abort propagation and execution lifecycle

- cancelActive() no longer double-aborts; delegates to cancel() which
  owns both abort signal and state mutation
- onSessionDeleted() and cleanup() now abort the controller so in-flight
  execution loops actually stop
- Set activeExecution before awaiting executeAbility so getActive()
  returns a live reference during execution (fixes chat-context injection)
- Add early abort check in executeAbility before the step loop starts
- opencode-plugin.ts uses cancelActive() instead of cancel()

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
darrenhinde 4 luni în urmă
părinte
comite
9e33583e16

+ 22 - 6
packages/plugin-abilities/src/executor/execution-manager.ts

@@ -3,10 +3,10 @@ import { executeAbility } from './index.js'
 
 /**
  * Minimal ExecutionManager
- * 
+ *
  * Simplified to track SINGLE execution at a time.
  * No session management, no cleanup timers, no multi-execution.
- * 
+ *
  * This is the bare minimum to test the core concept.
  */
 export class ExecutionManager {
@@ -28,6 +28,22 @@ export class ExecutionManager {
     console.log(`[abilities] Starting execution: ${ability.name}`)
 
     this.abortController = new AbortController()
+
+    // Set activeExecution BEFORE awaiting so getActive() returns a live
+    // reference during execution (needed for chat-context injection and
+    // concurrent-execution guards).
+    this.activeExecution = {
+      id: `exec_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
+      ability,
+      inputs,
+      status: 'running',
+      currentStep: null,
+      currentStepIndex: -1,
+      completedSteps: [],
+      pendingSteps: [...ability.steps],
+      startedAt: Date.now(),
+    }
+
     const execution = await executeAbility(ability, inputs, ctx, this.abortController.signal)
     this.activeExecution = execution
 
@@ -61,7 +77,6 @@ export class ExecutionManager {
     if (!this.activeExecution) return false
 
     if (this.activeExecution.status === 'running') {
-      // Signal the in-flight executeAbility loop to stop at the next iteration
       this.abortController?.abort()
       this.abortController = null
       this.activeExecution.status = 'failed'
@@ -75,14 +90,13 @@ export class ExecutionManager {
   }
 
   cancelActive(): boolean {
-    // Signal the in-flight executeAbility loop to stop at the next iteration
-    this.abortController?.abort()
-    this.abortController = null
     return this.cancel()
   }
 
   onSessionDeleted(sessionId: string): void {
     if (this.activeExecution && this.activeExecution.status === 'running') {
+      this.abortController?.abort()
+      this.abortController = null
       this.activeExecution.status = 'failed'
       this.activeExecution.error = `Session ${sessionId} deleted`
       this.activeExecution.completedAt = Date.now()
@@ -91,6 +105,8 @@ export class ExecutionManager {
   }
 
   cleanup(): void {
+    this.abortController?.abort()
+    this.abortController = null
     this.activeExecution = null
   }
 }

+ 18 - 0
packages/plugin-abilities/src/executor/index.ts

@@ -493,6 +493,24 @@ export async function executeAbility(
 
   // Build execution order based on dependencies
   const orderedSteps = buildExecutionOrder(ability.steps)
+
+  // Check for cancellation before starting execution
+  if (signal?.aborted) {
+    return {
+      id: generateExecutionId(),
+      ability,
+      inputs: resolvedInputs,
+      status: 'failed',
+      currentStep: null,
+      currentStepIndex: -1,
+      completedSteps: [],
+      pendingSteps: orderedSteps,
+      startedAt: Date.now(),
+      completedAt: Date.now(),
+      error: 'Cancelled',
+    }
+  }
+
   const stepOutputs = new Map<string, string>()
 
   const execution: AbilityExecution = {

+ 1 - 1
packages/plugin-abilities/src/opencode-plugin.ts

@@ -214,7 +214,7 @@ export const AbilitiesPlugin: Plugin = async (ctx) => {
         description: 'Cancel the active ability execution',
         args: {},
         async execute() {
-          const cancelled = executionManager.cancel()
+          const cancelled = executionManager.cancelActive()
           return JSON.stringify(cancelled
             ? { status: 'cancelled', message: 'Ability cancelled' }
             : { status: 'none', message: 'No active ability' })