Skip to content

fix(cli): stop the Lemonade Server GAIA uses in gaia kill --lemonade - #6991

Open
ityashag wants to merge 6 commits into
amd:mainfrom
ityashag:fix/kill-lemonade-through-owner
Open

ityashag wants to merge 6 commits into
amd:mainfrom
ityashag:fix/kill-lemonade-through-owner

Conversation

@ityashag

@ityashag ityashag commented Nov 9, 2026 •

Copy link
Copy Markdown
Contributor

Summary

gaia kill --lemonade now stops the Lemonade Server GAIA actually uses. Before, it ran the removed lemonade-server stop, then killed whatever held port 13305, so it never reached GAIA's own server, which picks its own port, or a server set with LEMONADE_BASE_URL. With a remote LEMONADE_BASE_URL it even went after port 13305 on this machine.

Why

The issue asks to route the kill through what starts the server and to fail loudly when it can't stop it. A silent fallback to a fixed port is what CLAUDE.md rules out.

Linked issue

Closes #4437

Two of the issue's four parts were already fixed on main: the "start lemonade-server" log text and the lemonade-server pull docs. This PR does the other two.

Changes

Server GAIA uses Before Now
GAIA's own (no LEMONADE_BASE_URL or setup choice) Port 13305, which it doesn't use Stopped the way gaia lemonade embedded stop does, child processes included
Chosen, on this machine (localhost, a loopback address, 0.0.0.0 or this machine's hostname) Port 13305, not the chosen port Stopped by the chosen port, but only a process identified as Lemonade (lemond, LemonadeServer, llama-server). Anything else there, including a Python, Node or Electron dev server, is left running and named with its PID
Chosen, on another machine A local port 13305 Refused with exit 1: "Stop it on that machine"

Nothing running still exits 0, so the documented gaia kill --lemonade && … keeps working.

The removed-CLI guard test:

Test plan

Run in a python:3.12-slim container with .[ui] and the test tools installed:

  • pytest tests/unit/test_kill_lemonade.py tests/unit/test_no_removed_lemonade_cli.py: 9 passed.
  • pytest tests/unit/test_cli_refusal_exit_codes.py: passes except test_kill_lemonade_stays_chainable_when_nothing_is_running, which runs gaia kill --port 1 and fails in my container because it has no lsof. It fails the same way on main, and this PR doesn't touch that path.
  • python util/lint.py --all: Black, isort, Flake8, Bandit and the MyPy ratchet pass. Pylint crashed in a parallel worker on my machine; run alone with the same flags, it reports nothing.
  • pytest tests/unit -n 3: 25,993 passed and 31 failed under load. Rerun on their own, 15 still fail the same way on main in my container. Two failures were caused by this change and are now fixed: test_kill_process_by_port.py::test_lemonade_port_stop_propagates_exit_status tested the removed lemonade-server path (deleted; test_kill_lemonade.py covers the same exit codes), and the process-safety ratchet needed cli.py's untimed-subprocess count lowered from 3 to 2.

With main's cli.py, all eight kill tests fail (the old code ran lemonade-server in every case), and the widened guard reports what the old one missed:

E       src/gaia/cli.py:5495: ["lemonade-server", "stop"],
E       src/gaia/cli.py:5504: log.warning(f"lemonade-server stop failed: {result.stderr}")

Evidence

gaia kill --lemonade run from main and from this branch, with a stand-in HTTP server on port 8123 and lsof installed:

======== main
$ LEMONADE_BASE_URL=http://127.0.0.1:8123/api/v1 gaia kill --lemonade
✅ No process is listening on port 13305
(exit 0)
$ LEMONADE_BASE_URL=http://10.0.0.5:13305/api/v1 gaia kill --lemonade
✅ No process is listening on port 13305
(exit 0)
======== this PR
$ LEMONADE_BASE_URL=http://127.0.0.1:8123/api/v1 gaia kill --lemonade
❌ Refusing to kill 26465 (unknown process) on port 8123: not a GAIA or Lemonade process. Stop it with its own tooling, or kill it by PID if that is really what you want.
(exit 1)
$ LEMONADE_BASE_URL=http://10.0.0.5:13305/api/v1 gaia kill --lemonade
❌ GAIA uses the Lemonade Server at http://10.0.0.5:13305/api/v1, which runs on another machine, so `gaia kill` can't stop it. Stop it on that machine.
(exit 1)

The stand-in is a Python HTTP server, so it is refused. (Since this capture, --lemonade refuses any listener not identified as Lemonade, not just non-interpreter ones, and the message says it may be your own program.) The point is that it now looks at port 8123 at all. GAIA's own server isn't installed in my container, so that path is covered by the unit tests, not a live run.

  • Agent exposed in the Agent UI: N/A
  • MCP tools / servers: N/A
  • CLI: gaia kill --lemonade, above.
  • HTTP API / REST: N/A

Checklist

  • I have linked a GitHub issue above (Closes #N / Fixes #N / Refs #N).
  • I have described why this change is being made, not just what changed.
  • I have run linting and tests locally (python util/lint.py --all, pytest tests/unit/).
  • I have attached real-world evidence matched to the surface I changed (see Evidence above), or marked each surface N/A.
  • I have updated documentation if user-visible behavior changed (see CONTRIBUTING.md). The gaia kill table in the CLI reference.

gaia kill --lemonade ran the removed `lemonade-server stop`, then killed
whatever held port 13305, so it never reached GAIA's own server on its own
port or one set with LEMONADE_BASE_URL. It now stops GAIA's own server the
way `gaia lemonade embedded stop` does, stops a chosen server on this
machine by its port, and refuses one on another machine. The removed-CLI
guard drops its stale exemptions, skips docstrings as well as comments, and
catches every removed subcommand, including the argv form.

Closes amd#4437
@github-actions github-actions Bot added documentation Documentation changes cli CLI changes tests Test changes labels Nov 9, 2026
@github-actions

github-actions Bot commented Nov 9, 2026

Copy link
Copy Markdown
Contributor

Verdict: Approve

gaia kill --lemonade now stops the Lemonade Server GAIA actually uses. Before, it ran a lemonade-server command that no longer exists and then killed whatever held port 13305. Now it stops GAIA's own server through the same code as gaia lemonade embedded stop. A server you chose is stopped by its own port. A server on another machine is refused with exit 1 instead of hitting a local port. I found no blocking issues. The widened guard test also closes the gap that let this bug through.

Real-world evidence

N/A here: no evidence bundle was produced for this run. The PR description has before/after gaia kill --lemonade output for a chosen local URL (now looks at port 8123, not 13305) and a remote URL (now refused, exit 1). The path that stops GAIA's own server has unit tests only; the author's container had no embedded Lemonade. This runner has no pytest, so I couldn't run the new kill tests. I did run the widened removed-CLI guard against this checkout with the standard library, and it passes. For the own-server path, the verdict rests on static review.

🔍 Technical details

Issues found

🟢 Unclear message when GAIA's own server isn't running (src/gaia/cli.py:6625). With no URL set and no embedded server running, the code falls through to the default URL and prints only No process is listening on port 13305. A user expecting GAIA's private server sees a port number they never set. A one-line note before the port kill, such as "GAIA's own Lemonade Server isn't running", would make the result clear. This is optional.

Checks done

  • Sibling paths: nothing else in src/, hub/ or tui/ runs lemonade-server stop or kills port 13305 directly. The other _is_loopback importers (ui/server.py, lemonade_service.py) use it the same way.
  • Known bug classes:
    • No deadline: EmbeddedLemonade.stop() has its own timeout.
    • Shared state: the port path still goes through kill_process_by_port's GAIA/Lemonade-only check, and stop() checks that the pid is GAIA's before signalling it.
    • Dishonest status: a stop that fails, a remote server, and a refused process all exit 1. "Nothing running" stays 0, so gaia kill --lemonade && … still works.
  • Doc drift: docs/reference/troubleshooting.mdx:345,386 still says "stops it and its child processes". That stays accurate for GAIA's own server. The only stale wording I found is in docs/releases/v0.15.2.mdx, a past release note, which is fine.
  • Guard test: ast.parse succeeds on every .py file under the scan roots, so skipping docstrings can't crash collection. With _PENDING gone, readiness.py, providers/lemonade.py and modelcmd.go mention the old CLI only in comments or docstrings.

Strengths

  • It fails loudly where it used to fall back silently: no more "try lemonade-server, then port 13305". Every outcome now has an honest exit code.
  • The new tests patch at the real boundaries (configured_lemonade_url, EmbeddedLemonade, kill_process_by_port). A teardown assertion fails the test if any case runs the removed CLI.
  • The regex now also matches the argv-list form (["lemonade-server", "stop"]), which is how this bug slipped past the old guard.

@kovtcharov-amd

Copy link
Copy Markdown
Collaborator

Thanks for this — stopping GAIA's own server through stop() and refusing a remote URL are both right. It breaks two existing tests on all three OS, though, so a couple of fixes are needed before merge:

  • tests/unit/cli/test_kill_process_by_port.py::test_lemonade_port_stop_propagates_exit_status ([True-False] and [True-True]) still mocks the lemonade-server subprocess path this PR removes, and now fails with SystemExit: 0. Please update or remove it.
  • tests/unit/test_process_safety_ratchet.py fails because the PR removed a process call: lower src/gaia/cli.py:subprocess_no_timeout from 3 to 2 in tests/unit/process_safety_allowlist.txt.
  • The PR body says no tests fail only with this change; please correct that, since it becomes the commit message.

One question we'll come back to you on: when GAIA's own Lemonade isn't running and no server was chosen, kill_lemonade() still falls through to the Lemonade on port 13305, which can be one the user started themselves. That predates your PR, so it isn't a blocker for it.

🔍 Technical details
  • Your new test_kill_lemonade.py and the guard test fail on main's cli.py and pass here, as they should; no lemonade-server command is invoked anywhere.
  • The red Offline Integration run is a 504 downloading the embeddable Lemonade release — unrelated.

The lemonade-server stop path is gone, so its test now fails; the exit-code
cases it covered live in test_kill_lemonade.py. cli.py makes one fewer
untimed subprocess call, so its ratchet count drops from 3 to 2.
@kovtcharov-amd

Copy link
Copy Markdown
Collaborator

Thanks @ityashag. I merged main into the branch and made the two test fixes from the review: the old --lemonade exit-status test that mocked the removed lemonade-server path is gone (your test_kill_lemonade.py covers the same exit codes), and cli.py's untimed-subprocess count in the process-safety allowlist is now 2. I also corrected the test-plan line in the description. The kill, ratchet and guard tests pass on the result.

…onade

The port comes from LEMONADE_BASE_URL or the setup choice, so a Python,
Node or Electron listener there is the user's own program. It is now left
running and named, and only lemond, LemonadeServer or llama-server is
killed. 0.0.0.0, :: and this machine's own hostname count as this machine
instead of being reported as another one.
@kovtcharov-amd

Copy link
Copy Markdown
Collaborator

Thanks @ityashag, this is a real fix and the widened removed-CLI guard is a nice catch. I merged main and closed one gap: with LEMONADE_BASE_URL pointing at a local port, gaia kill --lemonade would still kill a Python, Node or Electron program on that port, so a user's own dev server could go. Now it only kills a process it can identify as Lemonade and names anything else, and 0.0.0.0 or this machine's hostname count as local. I updated the table in the description to match.

🔍 Technical details
  • kill_process_by_port(port, lemonade_only=True) uses the new gaia.ports.is_lemonade_process; --port keeps its existing interpreter-level check.
  • _names_this_machine replaces _is_loopback for the remote check (unspecified addresses, own hostname/FQDN/addresses).
  • New tests in tests/unit/test_kill_lemonade.py run the real kill_process_by_port over a mocked listing: python/node/electron/gaia/unknown refused with nothing terminated, Lemonade names killed, mixed listeners spare the foreign one. They fail on the previous head.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cli CLI changes documentation Documentation changes tests Test changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(cli): gaia kill --lemonade still calls the removed lemonade-server stop

2 participants