You start three coding agents on a task before lunch and wrap each one in timeout 1800. Thirty minutes should be plenty. When you come back, one of them is still going: it failed its tests, the wrapper retried, the retry ran the checks again, and every new process got its own fresh 30 minutes. Nothing was broken. The limit just applied to each process, while the thing you wanted to bound was the whole run.
Ctrl-C has a similar gap. If your orchestration script starts each agent CLI in its own process group (which you want, so one agent's crash does not take the others down), killing the script does not reach those groups. The agents keep editing files and spending tokens with nobody watching.
An agent run is a chain of steps that each look short on their own:
A timeout on any one of those does not bound the sum. What does bound it is a single absolute deadline, written down when the session starts, that every later step reads instead of starting its own clock.
I build oh-my-agent (OMA), a CLI and skill set for running coding agents like Claude Code and Codex in parallel. Version 20.13 added a session execution budget for exactly this. You set it once in .agents/oma-config.yaml:
session:
execution:
timeout_seconds: 1800
or per command:
oma agent parallel tasks.yaml --timeout-seconds 1800
oma agent resume <session-id> --deadline 2026-11-01T18:00:00+11:00
spawn, parallel, review, resume, begin and verify all accept --timeout-seconds or --deadline. They share one deadline per session, stored under .agents/state/executions/. If a config value, a parent process and a persisted session each set a deadline, the earliest one wins. A retry or a resume reads that deadline; it cannot push it back. Child CLIs that OMA launches inherit it through OMA_EXECUTION_DEADLINE, so a nested oma agent spawn inside an agent's task stays inside the same budget.
When the deadline passes or you cancel, OMA stops admitting new work for that session: no new agents, retries, reviews or verification. It then terminates the process groups it started, with SIGTERM and a short grace period before SIGKILL. The command exits with 124 on timeout (the same code GNU timeout uses) and 130 when you cancel with Ctrl-C, and the stop reason is recorded in the session.
If OMA cannot confirm that a process is gone, it keeps the ownership record and reports the surviving process rather than calling the session cleaned up. oma cleanup --dry-run lists what it would stop, and it only touches processes whose identity still matches the record, so it will not kill an unrelated process that happens to reuse the PID.
Stopping also does not delete anything. Logs, results, worktrees and caches stay where they are, so you can read what each agent got done before the clock ran out.
session.quota_cap) do not interrupt a child that is already running, so the time budget is the one that actually stops a running child.
I built OMA, and I'd like to hear how you bound long agent runs today, or where this falls short for you.
curl -fsSL https://raw.githubusercontent.com/first-fluke/oh-my-agent/main/cli/install.sh | bash
irm https://raw.githubusercontent.com/first-fluke/oh-my-agent/main/cli/install.ps1 | iex