Skip to content

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 exit listener in spawnDetached is what feeds launchFailure (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 deployment would push and reopen its pull request. None of those names the cause.
  • aidlc autopilot tick ends with process.exitCode, not process.exit(), so an error event 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.
  • pause already skips a pid it cannot find in the process table, so a negative pid is not frozen or reported there. identify already 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; launchAgent says agent did not start; advanceBuilding checks isPid and gives a never-started typed launch no result; launchFailure answers first for a non-pid.
  • packages/cli/src/autopilot/run.ts — isAlive and realCtx.kill refuse a non-pid; spawnDetached listens for error and 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.json and packages/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.