In the previous chapter, we built a dashboard to see all our running tasks. We learned how to filter them and summarize them into a neat little "Pill" in the footer.
But looking at tasks is easy. The hard part is ending them.
Imagine you start a task that runs a server: npm start. It listens on port 3000.
Then, you delete that task from your dashboard list.
If you didn't do it right, the dashboard list is empty, but your computer is still running the server in the background. If you try to start a new one, it crashes because "Port 3000 is already in use."
This is a Zombie Task. It's dead in your application's memory, but alive in the Operating System.
We need a standardized way to ensure that when we say "Stop," we mean "Stop, Kill, and Clean Up."
Think of our Lifecycle & Termination system as a Facility Manager for an apartment building.
When a tenant (Task) gets evicted (Stopped):
This ensures the "room" (System Resources) is ready for the next tenant.
As a developer using this system, you don't need to know how a specific task works to stop it. You just need its ID.
We use a universal function called stopTask.
// Example: The user clicked the "Stop" button in the UI
try {
await stopTask('task-123', context);
console.log("Task stopped successfully.");
} catch (error) {
console.error("Could not stop task:", error.message);
}
What happens here:
task-123.
How does stopTask handle the differences between an AI Agent (which is just a network request) and a Shell Command (which is an OS process)?
It uses the Polymorphism we learned in Chapter 1.
Let's look at the actual code that orchestrates this.
In stopTask.ts, we perform checks that apply to every task, regardless of type.
// From stopTask.ts
export async function stopTask(taskId: string, context: ...): Promise<Result> {
const task = appState.tasks[taskId]
// Check 1: Does it exist?
if (!task) throw new StopTaskError('Task not found', 'not_found')
// Check 2: Is it actually running?
if (task.status !== 'running') {
throw new StopTaskError('Task is not running', 'not_running')
}
// Check 3: Find the specific "driver" for this task type
const taskImpl = getTaskByType(task.type)
// Delegate the actual killing to the implementation
await taskImpl.kill(taskId, setAppState)
}
Now, let's look at what taskImpl.kill actually does for a Local Shell Task (from Chapter 2). This code lives in LocalShellTask/killShellTasks.ts.
It has to do three things: Kill the process, Stop the Watchdog, and Update the State.
// From LocalShellTask/killShellTasks.ts
export function killTask(taskId: string, setAppState: SetAppStateFn): void {
updateTaskState(taskId, setAppState, task => {
// 1. Kill the OS Process (The "Eviction")
task.shellCommand?.kill()
// 2. Stop the Watchdog Timer (Turn off utilities)
if (task.cleanupTimeoutId) {
clearTimeout(task.cleanupTimeoutId)
}
// 3. Mark as dead in the UI
return { ...task, status: 'killed', endTime: Date.now() }
})
// 4. Clean up the hard drive (Remove furniture)
void evictTaskOutput(taskId)
}
Explanation:
shellCommand?.kill(): Sends the signal to the Operating System to stop the command immediately.clearTimeout: Stops the "Stall Watchdog" (from Chapter 2) so it doesn't try to alert us about a dead task.evictTaskOutput: Deletes the temporary text file where logs were stored.There is one more complex scenario: Sub-contractors.
In Chapter 3, we learned that an Agent can run shell commands. If you fire the Agent (stop the main task), you must also fire the commands it started.
If you don't, the Agent stops "thinking," but the npm install it started keeps running forever.
We handle this with killShellTasksForAgent.
// From LocalShellTask/killShellTasks.ts
export function killShellTasksForAgent(agentId: string, ...): void {
// Loop through ALL tasks
for (const [taskId, task] of Object.entries(tasks)) {
// If it's a shell task AND it belongs to this agent...
if (isLocalShellTask(task) && task.agentId === agentId) {
// ...kill it too!
killTask(taskId, setAppState)
}
}
}
This ensures that when the "Boss" leaves, the entire department is shut down cleanly.
In this final chapter, we learned:
stopTask is the polymorphic entry point that handles safety checks.Congratulations! You have completed the Tasks project tutorial. You now understand the full architecture:
You are now ready to build your own robust, multi-tasking AI applications!
Generated by Code IQ