fix(api): default event loop to asyncio + cancellation-safe commit — kills the CI segfault (#340)

* fix(api): default the event loop to asyncio + cancellation-safe commit

The recurring CI e2e segfault traced to uvloop: the harness's
uvicorn.run() auto-selected it while production's serve() path never
consulted Config.loop (stock asyncio, accidentally safe). Every launch
site now resolves ROBOCO_UVICORN_LOOP (default asyncio; uvloop opt-in),
and DbCommitMiddleware's commit-in-send can no longer be interrupted
mid-wire: on cancellation it gets a bounded grace to finish (committed
data survives the 504), else invalidate-and-reraise.

* feat(runtime): expected-stop breadcrumbs attribute container deaths

Two production exit-143s had no attributable source: every orchestrator
kill path now records a short reason breadcrumb, and the exit monitor
consumes it -- an expected stop logs its reason at info, a genuinely
unexpected one logs none_recorded plus docker-inspect diagnostics
(OOMKilled, timestamps) so the next mystery SIGTERM self-identifies.

---------

Co-authored-by: Renn F <rennf93@users.noreply.github.com>
This commit is contained in:
Renzo F
2026-07-08 16:01:01 +02:00
committed by GitHub
co-authored by Renn F
parent 312ec990dd
commit 0e9f21de69
18 changed files with 710 additions and 65 deletions
+6 -1
View File
@@ -62,6 +62,7 @@ class _FakeHost:
def __init__(self) -> None:
self.removed: list[str] = []
self.remove_stop_reasons: list[str | None] = []
self.spawn_args: tuple[object, ...] | None = None
self.mount_config: OrchestratorAgentConfig | None = None
self.data_dirs_ensured: list[str] = []
@@ -75,8 +76,11 @@ class _FakeHost:
self.spawn_args = (config, initial_prompt, agent_settings_path)
return "container-id-abc123"
async def _remove_container(self, container_name: str) -> None:
async def _remove_container(
self, container_name: str, *, stop_reason: str | None = None
) -> None:
self.removed.append(container_name)
self.remove_stop_reasons.append(stop_reason)
def _ensure_grok_usage_dir(self, agent_id: str) -> None:
self.data_dirs_ensured.append(agent_id)
@@ -227,6 +231,7 @@ async def test_grok_spawn_wires_gateway_env_and_image_last() -> None:
# The image is the final docker-run argument.
assert cmd[-1] == "roboco-agent-grok:test"
assert host.removed == ["roboco-agent-be-dev-1"]
assert host.remove_stop_reasons == ["pre_spawn_stale_clear"]
assert result == SpawnResult(
instance_id="roboco-agent-be-dev-1",
extra={"container_id": "cid", "model": "grok-build"},