Skip to content

fix(cli): a running server notices a CLI the installer just added - #778

Merged
Ark0N merged 6 commits into
masterfrom
feat/cli-installed-marker
Nov 8, 2026
Merged

Ark0N merged 6 commits into
masterfrom
feat/cli-installed-marker

Conversation

@Ark0N

@Ark0N Ark0N commented Nov 6, 2026

Copy link
Copy Markdown
Owner

The problem

A running Codeman remembers that a coding agent is missing, for up to 5 minutes after it last looked. That is there for a good reason: the last step of the lookup spawns a login shell, and without the memory every status poll would freeze the server for up to 5 seconds.

The Run button reads that same memory. So this sequence, the most likely one for a new user, goes wrong:

  1. Press Run. Codeman says the agent is not installed.
  2. Add the agent, with install.sh agent <id> or Codeman Desktop's Add a coding agent (feat(desktop): install Codeman from the app, with no Terminal and no password #773).
  3. Press Run again. Codeman still says it is not installed, for up to 5 minutes, right after being told it was ready.

Restarting the service would clear it, but a tool that adds an agent has no business restarting a server with sessions on it. The only code that cleared the memory is the CLI management routes in Settings, which are off by default.

The fix

The installer leaves a note, and the server reads it.

  • install.sh touches ~/.codeman/cli-installed once an agent has landed, and for an agent it finds already there, since that one may have been installed by hand a minute ago. The file is empty. A failed install writes nothing.
  • resolve() in src/utils/cli-executable-resolver.ts leaves its backoff when that file's mtime changes.

The change buys one run of the lookup, so an agent that is still missing goes back to waiting. Each agent keeps its own resolver, so each one notices separately.

Why this and not a re-scan

The obvious alternative is to re-walk PATH during the backoff, skipping only the login shell. It would also find agents installed by hand, but it was rejected:

This check is one stat of one file in the data folder, which the server reads constantly anyway. It sits after the cached-success return, so a resolved agent never pays for it.

Stacked on #773

The base is feat/graphical-installer, because the only thing that really needs this, install.sh agent, is in #773 and is not on master. On master it would be a reader with almost nothing writing to it, plus a conflict in install.sh. Merge order: #763, #773, then this one. GitHub retargets it when its base merges.

The diff is one commit, and it merges cleanly with the local branch that combines #763's unpushed review commits with #773. Against master it adds no conflict of its own. #773 has one, a single line of install.sh --help text that a later fix on master also changed, which this PR neither causes nor solves.

It also changes the wording on the desktop app's last screen, because "within a few minutes" is no longer the normal case.

No changeset: COM writes it.

Tested

On a real Mac (Apple Silicon, macOS 27.2, stock /bin/bash 3.2). A real install of this branch with no agent, under a throwaway home folder, with its server running and already remembering every agent as missing. Then real agents, through the same Rust code the desktop app calls:

Before this change With it
Codex (npm route): the server sees it 56 seconds after the install 48 ms
Claude Code (curl route): the server sees it not measured 43 ms
  • The 56 seconds is from the same scenario without this change, on a first miss, which is the shortest backoff there is. The longest is 5 minutes.
  • GitHub Copilot, which was not installed, stayed unavailable afterwards.
  • The server's process never changed, and the page a browser loads listed both new agents.
  • A real shell session in that server ran claude --version: 2.1.318 (Claude Code).

In the test suites. New tests in test/cli-executable-resolver.test.ts and test/install-sh-invariants.test.ts, and the marker's name is pinned on both sides. Each part of the fix was broken on purpose to prove a test catches it, which is how one test that could not fail was found and fixed.

Known limits

  • An agent installed by hand is not covered. npm install -g @openai/codex in a terminal writes no marker, so it waits out the backoff as it always has. Running install.sh agent codex afterwards clears it.
  • Only servers with this change read the marker. An older Codeman ignores the file, so the app's last screen still says it can take a few minutes.
  • An open page still needs a reload. The list of installed agents is part of the page. Opening Codeman from the app loads a fresh one.
  • Another CODEMAN_INSTANCE has its own data folder, and the installer only writes under ~/.codeman, so a beta instance waits out the backoff too.

…deman

Adds packages/desktop, a small native app (Rust: tao, wry/WKWebView, muda,
objc2) that opens Codeman in its own window. The universal app is about 5 MB
(3.2 MB as a DMG) and runs on macOS 11.3 or later.

- Connect screen with two tabs. This Mac finds the local Codeman and offers
  Open, Start or Install. Start uses launchd for a LaunchAgent install and
  codeman web -d otherwise. It never starts a second server while one runs,
  because two servers on one data dir share live sessions. Another machine
  connects by address (recent servers, Keychain logins) and can install
  Codeman there over SSH.
- Installs run Codeman's install.sh in Terminal from a self-deleting
  .command script. The installer needs a terminal for sudo, Homebrew and
  ssh. A local install uses --local --service and the first free port from
  3000, and the app opens Codeman once it answers.
- The local Codeman's login is read from its LaunchAgent or
  ~/.codeman/.env. It is sent only to 127.0.0.1 on that port, only while
  the user's own Codeman process holds it, and is never saved.
- Native JS dialogs (alert, confirm, prompt) and HTTP Basic challenge
  answers, both scoped to Codeman's own pages, so the page keeps working
  after a server restart. App cookies are cleared before every connect.
- Pop-outs through window.CodemanHost, with an allowlist (webview/ never
  opens a window), external links in the default browser, downloads to
  ~/Downloads, real full screen and window state that is remembered.
- The connect page refuses a WebKit older than Safari 16.4, which Codeman's
  terminal needs.

Codeman side: viewSubagentTranscript shows a toast when the pop-up is
refused instead of failing, with zh-CN strings and a test that pins them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The graphical install runs install.sh --no-admin, so this branch needs both
PR #763 (the app) and PR #768 (the route that needs no password).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…password

The This Mac tab runs install.sh --no-admin itself. The two questions only a
person can answer move into the app: which coding agent to install, shown only
when this Mac has none, and who may reach Codeman. The installer's output goes
to a file, so a running install survives the app quitting and is picked up
again when it reopens.

install.sh gains --ai-cli, --progress and a preflight subcommand, which is what
the app reads its choices from, so it keeps no copy of the CLI catalogue.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The agent chooser only appeared during the install, and only on a Mac with
no agent. After "Skip for now", or with Codeman already installed, there was
no screen for it.

The same chooser now works after the install: "Add a coding agent" on the
This Mac tab, "Add a Coding Agent to This Mac..." in the app menu (it also
works from a Codeman page), and after a Skip the app says Codeman has no
agent yet before it opens Codeman.

`install.sh agent <id>` installs one agent and nothing else. It checks that
THAT agent landed: install_offered_cli is fatal only when no CLI is left, so
with another one present a failed install would have reported success.

The app runs it as a run of its own (`gui_install::Run::Agent`), in its own
temporary folder, so `ps` never reads it as "Codeman is being installed". It
may run next to a live server, which an install must refuse, and stops only
for another run. `--no-admin` goes to both `preflight` and `agent`: it decides
what counts as installable, so the two must agree.

CI: the bash 3.2 preflight check never tested anything. Its child run
inherited CODEMAN_INSTALL_SH_LIB and stopped at the sourcing guard, and a
false [[ ]] does not trip set -e on bash 3.2. Both are fixed for that check.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
After "Skip for now" there is no other agent, so a failed add hit
install_offered_cli's gate first, and the app showed its first-install text
("re-run the installer") instead of naming the agent and its command. That
is the main path of adding an agent.

run_offered_cli now runs a trusted command and looks again; the gate stays in
install_offered_cli for the install, and `agent` judges its own run. The one
line that executes a trusted command is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A running Codeman remembers a missing CLI for up to 5 minutes, so a status
poll never spawns a login shell, and its Run button reads that same memory.
So "Run fails, add the agent, Run again" kept saying the CLI was not
installed, right after the app had said it was ready.

install.sh now touches `cli-installed` in the data dir once a CLI has landed,
and resolve() leaves the backoff when that file's mtime changes. It is one
stat, inside the backoff branch, and buys one run of the chain, so a resolved
CLI never pays for it and a CLI that is still missing goes back to waiting.
Re-walking PATH during the backoff would have found hand installs too, but it
would put a synchronous stat of every PATH entry on the request path, where
one dead network mount freezes the server (#516), and execute candidates that
were already refused.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Ark0N
Ark0N changed the base branch from feat/graphical-installer to master November 8, 2026 10:33
@Ark0N
Ark0N merged commit 98c4a3d into master Nov 8, 2026
2 checks passed
Ark0N pushed a commit that referenced this pull request Nov 8, 2026
…coding model on this Mac (experimental)

Conflicts resolved as on test/desktop-macbook: config/test-suites.ts keeps both browser tests (desktop-connect-page and local-model-ui), and the CLAUDE.md state-files line keeps #778's cli-installed entry with the local model's local-model.json and local-model/ entries.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.

2 participants