Skip to content

Refuse to launch an agent whose CLI is missing, instead of a dead pane - #217

Merged
pallaoro merged 1 commit into
mainfrom
this-happens-on-mobile-remote-box
Sep 5, 2026
Merged

pallaoro merged 1 commit into
mainfrom
this-happens-on-mobile-remote-box

Conversation

@pallaoro

@pallaoro pallaoro commented Sep 5, 2026

Copy link
Copy Markdown
Member

Reported as "command not found: opencode" in a task pane, on the phone against a box and locally after a reboot.

What was wrong

A launch runs <agent>; exec $SHELL -l in a PTY, so a missing binary produced one line of shell output in a pane that then sat there looking healthy. The card was filed running, a loop tick reported run N started with status ok, and nothing anywhere said the agent never ran. spawnAgentInTask validated the agent id against the registry, never the binary.

That is not an exotic state: an agent installed yesterday can be gone today, via an uninstall or a PATH that moved under a reboot. No availability list captured at connect time can catch it, and the loop runner never consults one anyway.

The fix

Every launch asks the login shell whether the binary resolves, before anything is written down. spawnAgentInTask is the single choke point for the composer, relaunch, a restored tab, the phone and a loop tick, so one guard covers all of them.

  • probeAgentBinary() uses the same shell and flags the launch does ($SHELL -lc 'command -v <bin>', the idiom the box installer already uses). Probing /bin/sh with the daemon's own PATH answers a different question and produces false absents on exactly the Macs whose boot-time PATH probe missed, and a false absent refuses a launch that would have worked.
  • It reports present | absent | unknown, and only a definite absent refuses. A shell that times out knows nothing about the binary; blocking work on that would be a worse bug than the one this prevents.
  • The error names the agent and carries its install command.

Three side effects a refusal would have stranded

Each of these was already latent; the guard is what made them reachable.

  • Loops leaked a worktree per tick. The task link was persisted only after the launch, so a failing tick left the loop unlinked and the next one built another branch and worktree. Now claimed before the launch.
  • Restore lost the conversation. The tab was stamped restored before spawning, and that is a one-way door off the restorable strip. Now stamped only once the replacement is up.
  • The loop form could not repair a broken loop. Its <select> omitted an uninstalled agent, so it displayed a different one while the state still held the missing one, and Save wrote the missing one straight back.

Also gates the phone's agent picker, which listed agents the box hasn't got with no indication; the desktop's has disabled them since #108.

Verification

  • Full suite 404 pass / 0 fail, run both normally and with agent CLIs stripped from PATH (the CI condition, where claude does not exist). Two existing suites drove the real launch path with agentId: "claude" and would have gone red on CI, so the probe is a seam on Services, defaulting to the real one.
  • Six new regression tests, each proven non-vacuous by reverting the fix it guards and watching it fail.
  • Live on a Mac with no opencode: the real path returns OpenCode isn't installed here — `opencode` is not on PATH. Install it with: curl -fsSL https://opencode.ai/install | bash, writes no session row, and leaves the card in its column. claude still launches.
  • Measured: listAgents() 148-153ms for three parallel probes, against ~0ms before. That lands on the handshake and the launch, both of which already do far slower work.

A launch runs `<agent>; exec $SHELL -l` in a PTY, so a missing binary
became one line of shell output in a pane that then looked healthy: the
card was filed `running`, a loop tick reported "run N started", and
nothing said the agent never ran. An agent installed yesterday and gone
today (an uninstall, a PATH that moved under a reboot) failed silently
every hour.

Every launch now asks the login shell whether the binary resolves before
anything is written down. The probe uses the same shell and flags the
launch does, because probing /bin/sh with the daemon's own PATH answers a
different question and produces false absents on exactly the Macs whose
boot-time PATH probe missed. It reports present/absent/unknown and only a
definite absent refuses: a shell that fails to answer knows nothing about
the binary, and blocking work on that would be worse than the bug.

Three callers did irreversible bookkeeping before spawning, which a
refusal would have stranded:
  - a loop persisted its task link only after the launch, so a failing
    tick left it unlinked and the next one built another worktree;
  - restore stamped the tab `restored` first, and that is a one-way door
    off the restorable strip;
  - the loop form's <select> omitted an uninstalled agent, so it showed a
    different one while saving the missing one straight back.

Also gates the phone's agent picker, which offered agents the box hasn't
got while the desktop's has always disabled them.

The probe is a seam on Services so tests don't assert which CLIs happen
to be installed on the machine running them (a CI runner has none).
@pallaoro
pallaoro enabled auto-merge (squash) September 5, 2026 08:12
@pallaoro
pallaoro merged commit f84a647 into main Sep 5, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant