/ tasks / LocalShellTask / killShellTasks.ts
killShellTasks.ts
 1  // Pure (non-React) kill helpers for LocalShellTask.
 2  // Extracted so runAgent.ts can kill agent-scoped bash tasks without pulling
 3  // React/Ink into its module graph (same rationale as guards.ts).
 4  
 5  import type { AppState } from '../../state/AppState.js'
 6  import type { AgentId } from '../../types/ids.js'
 7  import { logForDebugging } from '../../utils/debug.js'
 8  import { logError } from '../../utils/log.js'
 9  import { dequeueAllMatching } from '../../utils/messageQueueManager.js'
10  import { evictTaskOutput } from '../../utils/task/diskOutput.js'
11  import { updateTaskState } from '../../utils/task/framework.js'
12  import { isLocalShellTask } from './guards.js'
13  
14  type SetAppStateFn = (updater: (prev: AppState) => AppState) => void
15  
16  export function killTask(taskId: string, setAppState: SetAppStateFn): void {
17    updateTaskState(taskId, setAppState, task => {
18      if (task.status !== 'running' || !isLocalShellTask(task)) {
19        return task
20      }
21  
22      try {
23        logForDebugging(`LocalShellTask ${taskId} kill requested`)
24        task.shellCommand?.kill()
25        task.shellCommand?.cleanup()
26      } catch (error) {
27        logError(error)
28      }
29  
30      task.unregisterCleanup?.()
31      if (task.cleanupTimeoutId) {
32        clearTimeout(task.cleanupTimeoutId)
33      }
34  
35      return {
36        ...task,
37        status: 'killed',
38        notified: true,
39        shellCommand: null,
40        unregisterCleanup: undefined,
41        cleanupTimeoutId: undefined,
42        endTime: Date.now(),
43      }
44    })
45    void evictTaskOutput(taskId)
46  }
47  
48  /**
49   * Kill all running bash tasks spawned by a given agent.
50   * Called from runAgent.ts finally block so background processes don't outlive
51   * the agent that started them (prevents 10-day fake-logs.sh zombies).
52   */
53  export function killShellTasksForAgent(
54    agentId: AgentId,
55    getAppState: () => AppState,
56    setAppState: SetAppStateFn,
57  ): void {
58    const tasks = getAppState().tasks ?? {}
59    for (const [taskId, task] of Object.entries(tasks)) {
60      if (
61        isLocalShellTask(task) &&
62        task.agentId === agentId &&
63        task.status === 'running'
64      ) {
65        logForDebugging(
66          `killShellTasksForAgent: killing orphaned shell task ${taskId} (agent ${agentId} exiting)`,
67        )
68        killTask(taskId, setAppState)
69      }
70    }
71    // Purge any queued notifications addressed to this agent — its query loop
72    // has exited and won't drain them. killTask fires 'killed' notifications
73    // asynchronously; drop the ones already queued and any that land later sit
74    // harmlessly (no consumer matches a dead agentId).
75    dequeueAllMatching(cmd => cmd.agentId === agentId)
76  }