Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -377,8 +377,9 @@ This file defines how coding agents should work in this repository.
message so the user can inspect logs or explicitly force-stop it.
- Non-force `keep-gpu service-stop` must require a reachable service,
successful status/RPC checks, a clean `stop_keep` result with no stopped,
timed-out, or failed sessions, and a final no-active-session status check
before signaling; use `--force` for unresponsive auto-started daemons.
timed-out, or failed sessions and no non-empty message, and a final
no-active-session status check before signaling; use `--force` for
unresponsive auto-started daemons.
- Treat custom `job_id` values as reserved from the moment startup begins; duplicate starts must fail before another controller can begin keep-alive work.
- Keep custom `job_id` validation centralized in `session_config.py`: only `None` means omitted/all-sessions, and custom IDs must be non-empty URL-path-safe strings before any session state changes.
- MCP tool schemas that expose `job_id` must reuse the shared
Expand Down
17 changes: 9 additions & 8 deletions docs/reference/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,14 +150,15 @@ reported as JSON errors before RPC.
Stops the ownership-verified local daemon process created by auto-start logic.
Invalid endpoint values are rejected locally before service checks or
ownership-verified stop operations. Non-force shutdown requires the service to
be reachable, `stop_keep` to report no stopped, timed-out, or failed sessions,
and a final status check to show no active sessions before the daemon process is
signaled. Malformed PID records with float or boolean numeric identity values
are ignored rather than coerced before signaling. Auto-start cleans up and fails
if it cannot create a trustworthy ownership record for the daemon it just
spawned. On systems without `/proc`, KeepGPU may recover daemon identity from
platform process metadata, but it still signals only when recovered identity is
known and exactly matches the stored ownership record.
be reachable, `stop_keep` to report no stopped, timed-out, or failed sessions
and no non-empty message, and a final status check to show no active sessions
before the daemon process is signaled. Malformed PID records with float or
boolean numeric identity values are ignored rather than coerced before
signaling. Auto-start cleans up and fails if it cannot create a trustworthy
ownership record for the daemon it just spawned. On systems without `/proc`,
KeepGPU may recover daemon identity from platform process metadata, but it still
signals only when recovered identity is known and exactly matches the stored
ownership record.

| Option | Description |
| --- | --- |
Expand Down
4 changes: 4 additions & 0 deletions src/keep_gpu/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -887,6 +887,10 @@ def _require_clean_stop_keep_for_service_stop(result: Dict[str, Any]) -> None:
incomplete.append(f"timed out: {', '.join(result['timed_out'])}")
if result["failed"]:
incomplete.append(f"failed: {', '.join(result['failed'])}")
message = result.get("message")
if isinstance(message, str) and message:
message_detail = message.strip() or repr(message)
incomplete.append(f"message: {message_detail}")
if incomplete:
raise RuntimeError(
"Stop sessions before stopping the service daemon. Incomplete stop_keep result "
Expand Down
44 changes: 44 additions & 0 deletions tests/test_cli_service_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -2553,6 +2553,50 @@ def fake_stop_process(host, port):
assert "Traceback" not in result.output


@pytest.mark.parametrize(
("message", "expected_fragment"),
[
(
"Timed out while stopping some sessions.",
"Timed out while stopping some sessions.",
),
(" ", "' '"),
],
)
def test_service_stop_rejects_stop_keep_message_before_stopping_daemon(
monkeypatch, message, expected_fragment
):
calls = {"stop_process": 0}

def fake_rpc(method, params, host, port, timeout=8.0):
if method == "status":
return {"active_jobs": []}
if method == "stop_keep":
return {
"stopped": [],
"timed_out": [],
"failed": [],
"errors": {},
"message": message,
}
raise AssertionError(f"unexpected method {method}")

def fake_stop_process(host, port):
calls["stop_process"] += 1
return True

monkeypatch.setattr(cli, "_rpc_call", fake_rpc)
monkeypatch.setattr(cli, "_stop_service_process", fake_stop_process)

result = runner.invoke(cli.app, ["service-stop"])

assert result.exit_code == 1
assert expected_fragment in result.output
assert "service-stop --force" in result.output
assert calls["stop_process"] == 0
assert "Traceback" not in result.output


def test_service_stop_rejects_newly_stopped_session_before_stopping_daemon(
monkeypatch,
):
Expand Down
Loading