In the previous chapter, we learned how the system treats all tasks generically using Task State Polymorphism. We learned that the system looks at task.type to decide how to handle a specific task.
Now, we are going to look at the most common type of task in our system: Local Shell Execution (local_bash).
Running a command like git push or npm install seems simple. You type it, and it runs. But for an automated system, it's tricky.
Imagine you hire an Invisible Typist to work in a separate room. You slide a note under the door: "Please run npm install."
Do you want to continue? (y/n)? The Typist is waiting for an answer. You are in the hallway waiting for them to finish. The process is now stuck forever.We need a way to manage these invisible commands and know when they are crying out for help.
Our LocalShellTask isn't just a "fire and forget" command runner. It includes a Watchdog.
The Watchdog is a timer that periodically checks the Typist's work:
y/n, Password:, Press Enter).If the log stops growing and looks like a question, the Watchdog rings a bell to notify you.
In our codebase, we don't just run exec('cmd'). We spawn a LocalShellTask.
To start a command, we use spawnShellTask. This sets up the state and starts the process.
// Example: Starting a build process
const taskHandle = await spawnShellTask({
command: 'npm run build',
description: 'Building project...',
agentId: 'agent-123',
shellCommand: myShellInstance // The actual process runner
}, context);
What happens here:
TaskState is created with type: 'local_bash'.Let's look at the lifecycle of a shell command, specifically focusing on how we detect if it gets stuck.
The logic for this lives in LocalShellTask.tsx.
First, we define what a "question" looks like to a computer. These are regular expressions that match common terminal prompts.
// From LocalShellTask.tsx
const PROMPT_PATTERNS = [
/\(y\/n\)/i, // Matches "(y/n)"
/\[y\/n\]/i, // Matches "[Y/n]"
/Press (any key|Enter)/i,
/Password:/i
];
The startStallWatchdog function sets up an interval. It checks the output file size on disk.
// From LocalShellTask.tsx
const timer = setInterval(() => {
// Check if the file has grown since last check
if (currentSize > lastSize) {
lastSize = currentSize;
lastGrowth = Date.now();
return; // It's working, do nothing.
}
// ... proceed to check for stall
}, 5000); // Check every 5 seconds
If the file size hasn't changed for a while (e.g., 45 seconds), we read the end of the file to see why.
// From LocalShellTask.tsx
// If we haven't seen growth in 45 seconds...
if (Date.now() - lastGrowth > STALL_THRESHOLD_MS) {
// Read the last few bytes of the file
const content = await tailFile(outputPath, 1024);
// Does it look like a prompt?
if (looksLikePrompt(content)) {
sendNotification("Task appears to be waiting for input");
}
}
This simple logic prevents our automated agents from staring blankly at a screen that is waiting for them to press "Y".
When a command finishes (or is killed), we must ensure the Watchdog stops barking.
// From LocalShellTask.tsx
void shellCommand.result.then(async result => {
// 1. Stop the timer
cancelStallWatchdog();
// 2. Update status to 'completed' or 'failed'
updateTaskState(taskId, setAppState, task => ({
...task,
status: result.code === 0 ? 'completed' : 'failed'
}));
});
In this chapter, we learned:
y/n).Now that we can run simple commands, what happens when we need to run a complex AI that thinks, plans, and spawns other commands?
Next Chapter: Background Agent Execution
Generated by Code IQ