From 19c53a6de181aaac0a912238feab9b7d2e08a703 Mon Sep 17 00:00:00 2001 From: Natalie Date: Thu, 25 Jun 2026 02:37:36 -0400 Subject: [PATCH] =?UTF-8?q?fix(@projects/@magic-civilization):=20?= =?UTF-8?q?=F0=9F=A7=B9=20player-API=20harness=20exits=20on=20stdin=20EOF?= =?UTF-8?q?=20+=20adds=20explicit=20stop=20request?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The pump loop treated an empty stdin read as "EOF or no line yet" and yielded + retried forever, so `cat reqs | player-api-server.sh` hung until CP_TIMEOUT_SEC instead of the documented clean EOF exit. Godot 4.6's read_string_from_stdin blocks, so an empty return is end-of-stream — break and quit(0). Also add a `{"type":"stop"}` request (acked, sets the loop flag) so interactive clients that hold stdin open (Popen drivers, the MCP adapter) can exit cleanly without relying on EOF — the clean-exit request path the docs promised but never implemented. Verified: 11-request batch pipe now exits 0 in ~1s (was 124/timeout); view+stop exits 0 in ~4s with both responses acked. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../engine/scenes/headless/player_api_main.gd | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/src/game/engine/scenes/headless/player_api_main.gd b/src/game/engine/scenes/headless/player_api_main.gd index 7073e925..832a5930 100644 --- a/src/game/engine/scenes/headless/player_api_main.gd +++ b/src/game/engine/scenes/headless/player_api_main.gd @@ -550,9 +550,11 @@ func _pump() -> void: while not _shutdown: var line: String = OS.read_string_from_stdin(4096) if line == "": - # EOF or no line yet — yield to the engine and try again. - await get_tree().process_frame - continue + # A blocking stdin read returns "" only at end-of-stream — the + # client closed stdin. Honour the documented EOF clean-exit instead + # of busy-looping until CP_TIMEOUT_SEC (prior tech debt: this branch + # yielded + retried forever, so `cat reqs | harness` never exited). + break line = line.strip_edges() if line.is_empty(): continue @@ -579,6 +581,15 @@ func _handle_request(req: Dictionary) -> void: "view": var view_json: String = String(_api.view_json(target_slot)) _emit_response_with_view(rid_int, has_id, view_json) + "stop": + # Explicit clean-exit request for interactive clients that hold + # stdin open (Popen drivers, the MCP adapter). Ack, then flag the + # pump loop to break → get_tree().quit(0). Complements EOF exit. + _shutdown = true + if has_id: + _write_line(JSON.stringify({"id": rid_int, "ok": true})) + else: + _write_line(JSON.stringify({"ok": true})) "suggest": # Stage 6.1.6 — read-only "what would the in-box AI do here". # `suggest_json` returns a full `{"ok":...,"actions":[...]}`