diff --git a/AGENTS.md b/AGENTS.md index 45f345b..8b3f3df 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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 diff --git a/docs/reference/cli.md b/docs/reference/cli.md index 3b75126..6d88709 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -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 | | --- | --- | diff --git a/src/keep_gpu/cli.py b/src/keep_gpu/cli.py index 85ce0e1..072bb8c 100644 --- a/src/keep_gpu/cli.py +++ b/src/keep_gpu/cli.py @@ -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 " diff --git a/tests/test_cli_service_commands.py b/tests/test_cli_service_commands.py index b2ab571..df969be 100644 --- a/tests/test_cli_service_commands.py +++ b/tests/test_cli_service_commands.py @@ -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, ):