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
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,7 @@ This file defines how coding agents should work in this repository.
outcome lists whose `errors` keys exactly match `failed`, and `list_gpus` GPU
records with visible ordinal metadata. Service-returned job IDs must satisfy
the shared URL-path-safe job-id contract. Known nested `status.params` fields
must match the public session contract while extra fields remain
must be present and match the public session contract while extra fields remain
forward-compatible. Session `state` values in status records must be one of
`active`, `starting`, `stopping`, `runtime_failed`, or `stop_failed`.
- Targeted CLI service commands must reject success payloads for the wrong
Expand Down
3 changes: 2 additions & 1 deletion docs/guides/mcp.md
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,8 @@ is still in progress. That includes both `status(job_id)` and the all-session
`status()` list, so agents do not mistake an in-progress start for no session.
Returned session `params` are snapshots; local embedding callers can inspect
or modify the returned object without mutating service state. Known fields in
`params` follow the same public session contract as `start_keep`.
`params` are always present and follow the same public session contract as
`start_keep`.
If an already-started worker later reports a terminal runtime or allocation
failure, the retained session is refreshed to `state="runtime_failed"` with
`last_error`. It remains visible and stoppable. This is distinct from normal
Expand Down
3 changes: 3 additions & 0 deletions docs/reference/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,9 @@ legacy local scripts.
Omitted direct-call `params` are treated as `{}` for compatibility, but explicit
`params: null` or any other non-object `params` value returns `-32602 Invalid
params` before method side effects.
Status records include a complete `params` snapshot with `gpu_ids`, `vram`,
`interval`, and `busy_threshold`; omitted `gpu_ids` is represented as `null`,
not by omitting the key.

| Endpoint | Method | Purpose |
| --- | --- | --- |
Expand Down
2 changes: 1 addition & 1 deletion src/keep_gpu/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -846,7 +846,7 @@ def _validate_status_params(params: Dict[str, Any], method: str, prefix: str) ->
}
for field, validator in validators.items():
if field not in params:
continue
raise _malformed_method_result(method, f"{prefix}.{field} is required")
try:
validator(params[field])
except (TypeError, ValueError, OverflowError) as exc:
Expand Down
67 changes: 29 additions & 38 deletions tests/test_cli_service_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -997,14 +997,7 @@ def fake_rpc(method, params, host, port):
assert params == {}
return {
"active": True,
"active_jobs": [
{
"job_id": "job-1",
"params": {"gpu_ids": [0]},
"state": "active",
"last_error": None,
}
],
"active_jobs": [_status_session_record()],
}

monkeypatch.setattr(cli, "_rpc_call", fake_rpc)
Expand Down Expand Up @@ -1078,13 +1071,7 @@ def test_status_job_outputs_single_decoded_json_object(monkeypatch):
def fake_rpc(method, params, host, port):
assert method == "status"
assert params == {"job_id": "job-1"}
return {
"active": True,
"job_id": "job-1",
"params": {"gpu_ids": [0]},
"state": "active",
"last_error": None,
}
return {"active": True, **_status_session_record()}

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

Expand Down Expand Up @@ -1263,6 +1250,30 @@ def fake_rpc(method, params, host, port):
assert "Traceback" not in result.output


@pytest.mark.parametrize(
"missing_field",
["gpu_ids", "vram", "interval", "busy_threshold"],
)
def test_status_rejects_missing_session_params(monkeypatch, missing_field):
session = _status_session_record()
del session["params"][missing_field]

def fake_rpc(method, params, host, port):
assert method == "status"
assert params == {}
return {"active_jobs": [session]}

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

result = runner.invoke(cli.app, ["status"])

assert result.exit_code == 1
decoded = _single_decoded_json_object(result.output)
assert "Malformed status response" in decoded["error"]
assert f"active_jobs[0].params.{missing_field} is required" in decoded["error"]
assert "Traceback" not in result.output


@pytest.mark.parametrize(
"payload",
[
Expand Down Expand Up @@ -1358,10 +1369,8 @@ def fake_rpc(method, params, host, port):
{"active": False, "job_id": "other-job"},
{
"active": True,
**_status_session_record(),
"job_id": "other-job",
"params": {},
"state": "active",
"last_error": None,
},
],
)
Expand Down Expand Up @@ -2914,16 +2923,7 @@ def fake_rpc(method, params, host, port, timeout=8.0):
calls["status"] += 1
if calls["status"] == 1:
return {"active_jobs": []}
return {
"active_jobs": [
{
"job_id": "late-job",
"params": {},
"state": "active",
"last_error": None,
}
]
}
return {"active_jobs": [{**_status_session_record(), "job_id": "late-job"}]}
if method == "stop_keep":
return {
"stopped": [],
Expand Down Expand Up @@ -3036,16 +3036,7 @@ def test_service_stop_refuses_active_sessions_without_force(monkeypatch):
cli,
"_rpc_call",
lambda method, params, host, port, timeout=8.0: (
{
"active_jobs": [
{
"job_id": "j1",
"params": {},
"state": "active",
"last_error": None,
}
]
}
{"active_jobs": [{**_status_session_record(), "job_id": "j1"}]}
if method == "status"
else {"stopped": []}
),
Expand Down