v1.76.0 ·
An agent that is not installed no longer stops the run
The problem
realCtx.spawnDetached (packages/cli/src/autopilot/run.ts:150) spawns the agent and returns
child.pid ?? -1. When the command cannot be started — it is not on the PATH, or not executable —
Node gives the child no pid, emits no exit, and emits an error event on the next tick. Nothing
listens for it, so it is thrown: the background aidlc autopilot run --foreground loop dies, and a
one-shot aidlc autopilot tick exits with a stack trace.
The pass that spawned it had already saved the item as building with pid: -1 and said
agent started (pid -1). isAlive(-1) (run.ts:49) calls process.kill(-1, 0), which succeeds
whenever the user may signal any process, and advanceBuilding (driver.ts:752) reads -1 as a
running agent. Every later pass waits on it: the item holds a build slot and is never parked.
Read in the code before writing these criteria:
- The
exitlistener inspawnDetachedis what feedslaunchFailure(driver.ts:336). A spawn that fails never reaches it, so the launch-failure park from codex-preset-removed-flag cannot see this case even once the pid reads as dead. - With the pid read as dead and nothing else changed, the item would fall through to the session
result and the phase: a Claude preset reads no result and parks as "agent exited before the
release boundary"; a Codex preset reads an empty session and tries to resume a session that never
existed; a CI-fix or conflict relaunch at
deploymentwould push and reopen its pull request. None of those names the cause. aidlc autopilot tickends withprocess.exitCode, notprocess.exit(), so anerrorevent queued by its spawn is still delivered before that process ends. The record of it is not: the next pass, in another process, cannot know what the error was.pausealready skips a pid it cannot find in the process table, so a negative pid is not frozen or reported there.identifyalready returns null for a pid that is not positive.
How it could be solved
Three choices, each about a failure that left no trace of itself.
The first was to name a launch that never happened by what it was. When a program cannot be found, the operating system gives it no process number, and the old code stood in a placeholder of minus one. That placeholder was the whole bug: asked whether minus one is alive, the system answers about every process the user owns, and says yes. So a launch that never started now gets its own negative number, one no real process can have, and the error that explains it is kept under that number until the next check reads it and parks the item with that error.
The second was to close every road from that placeholder to a signal, not only the one in the report. The report was about checking whether the agent was alive. The same number could also reach the call that stops an agent over its cost limit, and stopping minus one would have sent a stop signal to every program the user had open. Both now refuse anything that is not a real process number, and the tests that prove it replace the system's signal call with a stand-in, so a mistake in the code cannot reach the machine running them.
The third was to prove each guard by taking it away. With the error handler removed, the test run printed the same unhandled error the report described. With each other guard removed, the tests written for it failed. One removal also showed that an assumption written into the plan was wrong: a launch that never started would not have been retried, only described afterwards as a session with no result. The guard stayed, and the record now says what it actually prevents.
How AIDLC solves it
Autopilot no longer dies when the agent it is set up to run is not installed. Before, a missing
command such as codex ended the background run on the spot, and the item it was starting stayed
marked as building for ever, holding a build slot with nothing behind it.
Now the run carries on. At its next check it parks the item with the reason the system gave, such
as agent failed to launch: spawn codex ENOENT, frees the slot, and moves to the next ready item.
An item already stuck this way by an older version is parked the same way at the first check after
upgrading. Agents that start behave exactly as before.
Changes
packages/cli/src/autopilot/driver.ts—isPid;AgentExit.error;launchAgentsaysagent did not start;advanceBuildingchecksisPidand gives a never-started typed launch no result;launchFailureanswers first for a non-pid.packages/cli/src/autopilot/run.ts—isAliveandrealCtx.killrefuse a non-pid;spawnDetachedlistens forerrorand records it under a negative id.packages/cli/test/autopilot-spawn-failure.test.ts(new, 22 tests). No existing test changed.packages/content/docs/roadmap.md— one sentence on a command that cannot start..aidlc/knowledge/decisions/autopilot-unstarted-spawn-id.yaml.packages/website/content/blog/overrides/agent-binary-missing-crashes-loop-post.jsonandpackages/website/content/blog/overrides/agent-binary-missing-crashes-loop-options.md.
Patch, not minor: no command, flag, config key, state field, file format or dependency is added; behaviour changes only where a launch never started, where it was wrong.