From 6c2998d9bc8ace3f58fc093a698fa442632fe3b7 Mon Sep 17 00:00:00 2001 From: Duang777 Date: Tue, 6 Oct 2026 10:28:56 +0800 Subject: [PATCH 1/2] fix(macos): bound Chat runtime verification retries The LaunchAgent verifier retried up to 50 times while each nested curl could block for five seconds, turning startup verification into a multi-minute wait. Give all readiness reads one 15-second deadline, cap each curl to the remaining one-second request budget, and cover a server that accepts the connection but never responds. Signed-off-by: Duang777 --- ...acos-dashboard-launchagent-status-smoke.py | 62 +++++++++++++++++++ scripts/macos-dashboard-launchagent.sh | 19 ++++-- 2 files changed, 77 insertions(+), 4 deletions(-) diff --git a/examples/macos-dashboard-launchagent-status-smoke.py b/examples/macos-dashboard-launchagent-status-smoke.py index 9f8a7d0b14..c22dca5e7f 100644 --- a/examples/macos-dashboard-launchagent-status-smoke.py +++ b/examples/macos-dashboard-launchagent-status-smoke.py @@ -110,6 +110,66 @@ def read_status() -> str: thread.join(timeout=2) +def check_chat_runtime_verification_deadline(tmp: Path, fake_bin: Path) -> None: + """A hung readiness read must share the outer startup deadline.""" + real_curl = shutil.which("curl") + assert real_curl, "curl is required for the Chat runtime deadline regression" + request_started = threading.Event() + + class Handler(BaseHTTPRequestHandler): + def do_GET(self) -> None: + assert self.path == "/api/chat/capabilities", self.path + request_started.set() + time.sleep(10) + + def log_message(self, *_args: object) -> None: + pass + + server = ThreadingHTTPServer(("127.0.0.1", 0), Handler) + thread = threading.Thread(target=server.serve_forever, daemon=True) + thread.start() + source, separator, _ = LAUNCHAGENT_SCRIPT.read_text(encoding="utf-8").partition( + "\nparsed_args=()\n" + ) + assert separator, "could not isolate LaunchAgent helper definitions" + probe = tmp / "chat-runtime-deadline-probe.sh" + write_executable( + probe, + source + + f""" +chat_runtime_endpoint=127.0.0.1:{server.server_port} +started="$SECONDS" +if verify_current_chat_runtime '{{"schema_version":"loopx_runtime_identity_v1"}}' 1; then + exit 2 +fi +elapsed=$((SECONDS - started)) +if (( elapsed > 2 )); then + echo "verification exceeded its shared deadline: ${{elapsed}}s" >&2 + exit 3 +fi +""", + ) + try: + path = os.pathsep.join( + part for part in os.environ.get("PATH", "").split(os.pathsep) + if Path(part or ".").resolve() != fake_bin.resolve() + ) + result = subprocess.run( + [str(probe)], + env={**os.environ, "PATH": path}, + check=False, + capture_output=True, + text=True, + timeout=3, + ) + assert request_started.is_set(), "Chat verification did not reach the HTTP server" + assert result.returncode == 0, (result.stdout, result.stderr) + finally: + server.shutdown() + server.server_close() + thread.join(timeout=2) + + def log_rotation_prelude(plist: Path) -> str: """The rotation step the agent wrapper runs before it execs the service.""" command = plistlib.loads(plist.read_bytes())["ProgramArguments"][2] @@ -309,6 +369,8 @@ def main() -> int: "EOF\n", ) + check_chat_runtime_verification_deadline(tmp, fake_bin) + old_output = run_status(fake_bin, home, schema_version=1) assert "- com.loopx.status: loaded" in old_output, old_output assert "- com.loopx.chat: loaded" in old_output, old_output diff --git a/scripts/macos-dashboard-launchagent.sh b/scripts/macos-dashboard-launchagent.sh index a4aed69d98..9c64c582bf 100755 --- a/scripts/macos-dashboard-launchagent.sh +++ b/scripts/macos-dashboard-launchagent.sh @@ -563,10 +563,11 @@ print(json.dumps(identity, sort_keys=True, separators=(",", ":"))) } chat_runtime_identity() { - local python_command payload + local python_command payload request_timeout python_command="$(resolve_python_command)" + request_timeout="${1:-5}" # A scheme-less curl endpoint defaults to local HTTP; managed replacement rejects non-loopback hosts. - payload="$(curl -fsS --connect-timeout 1 --max-time 5 "$chat_runtime_endpoint/api/chat/capabilities" 2>/dev/null)" + payload="$(curl -fsS --connect-timeout 1 --max-time "$request_timeout" "$chat_runtime_endpoint/api/chat/capabilities" 2>/dev/null)" "$python_command" -c ' import json import sys @@ -582,7 +583,7 @@ print(json.dumps(identity, sort_keys=True, separators=(",", ":"))) } verify_current_chat_runtime() { - local expected actual attempt + local expected actual attempt timeout_seconds deadline request_timeout if ! command -v curl >/dev/null 2>&1; then echo "curl is required to verify the restarted LoopX Chat runtime." >&2 return 1 @@ -591,12 +592,22 @@ verify_current_chat_runtime() { echo "Could not resolve the installed LoopX runtime identity." >&2 return 1 } + timeout_seconds="${2:-15}" + [[ "$timeout_seconds" =~ ^[1-9][0-9]*$ ]] || { + echo "LoopX Chat runtime verification timeout must be a positive integer." >&2 + return 2 + } + deadline=$((SECONDS + timeout_seconds)) for attempt in {1..50}; do - actual="$(chat_runtime_identity 2>/dev/null || true)" + (( SECONDS < deadline )) || break + request_timeout=$((deadline - SECONDS)) + (( request_timeout <= 1 )) || request_timeout=1 + actual="$(chat_runtime_identity "$request_timeout" 2>/dev/null || true)" if [[ -n "$actual" && "$actual" == "$expected" ]]; then echo "- chat_runtime: current release identity verified" return 0 fi + (( SECONDS < deadline )) || break sleep 0.2 done echo "LoopX Chat did not start with the current release identity at local endpoint $chat_runtime_endpoint." >&2 From 9c06838063f80c0a53b162f510e7d79f71b6cd60 Mon Sep 17 00:00:00 2001 From: Duang777 Date: Wed, 7 Oct 2026 23:40:26 +0800 Subject: [PATCH 2/2] fix(macos): preserve slow runtime identity reads The shared startup deadline accidentally replaced the existing five-second per-read window with a one-second cap. Bound each read by the smaller of the remaining deadline and five seconds, and cover both a slow valid response and a hung response with real HTTP. Signed-off-by: Duang777 --- ...acos-dashboard-launchagent-status-smoke.py | 66 ++++++++++++++++--- scripts/macos-dashboard-launchagent.sh | 2 +- 2 files changed, 59 insertions(+), 9 deletions(-) diff --git a/examples/macos-dashboard-launchagent-status-smoke.py b/examples/macos-dashboard-launchagent-status-smoke.py index c22dca5e7f..a005124053 100644 --- a/examples/macos-dashboard-launchagent-status-smoke.py +++ b/examples/macos-dashboard-launchagent-status-smoke.py @@ -111,16 +111,33 @@ def read_status() -> str: def check_chat_runtime_verification_deadline(tmp: Path, fake_bin: Path) -> None: - """A hung readiness read must share the outer startup deadline.""" + """A valid slow read succeeds while a hung read shares the outer deadline.""" real_curl = shutil.which("curl") assert real_curl, "curl is required for the Chat runtime deadline regression" request_started = threading.Event() + request_count = [0] + response = {"delay": 1.25} + runtime_identity = {"schema_version": "loopx_runtime_identity_v1"} class Handler(BaseHTTPRequestHandler): def do_GET(self) -> None: assert self.path == "/api/chat/capabilities", self.path + request_count[0] += 1 request_started.set() - time.sleep(10) + time.sleep(response["delay"]) + payload = json.dumps( + { + "ok": True, + "schema_version": "loopx_chat_capabilities_v1", + "runtime_identity": runtime_identity, + } + ).encode() + try: + self.send_response(200) + self.end_headers() + self.wfile.write(payload) + except (BrokenPipeError, ConnectionResetError): + pass def log_message(self, *_args: object) -> None: pass @@ -138,12 +155,21 @@ def log_message(self, *_args: object) -> None: source + f""" chat_runtime_endpoint=127.0.0.1:{server.server_port} +expected='{json.dumps(runtime_identity, sort_keys=True, separators=(",", ":"))}' +timeout_seconds="${{1:?timeout seconds required}}" +expected_result="${{2:?expected result required}}" started="$SECONDS" -if verify_current_chat_runtime '{{"schema_version":"loopx_runtime_identity_v1"}}' 1; then - exit 2 +if verify_current_chat_runtime "$expected" "$timeout_seconds"; then + actual_result=success +else + actual_result=failure fi elapsed=$((SECONDS - started)) -if (( elapsed > 2 )); then +if [[ "$actual_result" != "$expected_result" ]]; then + echo "verification result $actual_result, expected $expected_result" >&2 + exit 2 +fi +if (( elapsed > timeout_seconds + 1 )); then echo "verification exceeded its shared deadline: ${{elapsed}}s" >&2 exit 3 fi @@ -154,8 +180,28 @@ def log_message(self, *_args: object) -> None: part for part in os.environ.get("PATH", "").split(os.pathsep) if Path(part or ".").resolve() != fake_bin.resolve() ) - result = subprocess.run( - [str(probe)], + started = time.monotonic() + slow_result = subprocess.run( + [str(probe), "5", "success"], + env={**os.environ, "PATH": path}, + check=False, + capture_output=True, + text=True, + timeout=7, + ) + assert request_started.is_set(), "Chat verification did not reach the HTTP server" + assert slow_result.returncode == 0, ( + slow_result.stdout, + slow_result.stderr, + ) + assert time.monotonic() - started < 3 + assert request_count[0] == 1 + + response["delay"] = 10 + request_started.clear() + started = time.monotonic() + hung_result = subprocess.run( + [str(probe), "1", "failure"], env={**os.environ, "PATH": path}, check=False, capture_output=True, @@ -163,7 +209,11 @@ def log_message(self, *_args: object) -> None: timeout=3, ) assert request_started.is_set(), "Chat verification did not reach the HTTP server" - assert result.returncode == 0, (result.stdout, result.stderr) + assert hung_result.returncode == 0, ( + hung_result.stdout, + hung_result.stderr, + ) + assert time.monotonic() - started < 2.5 finally: server.shutdown() server.server_close() diff --git a/scripts/macos-dashboard-launchagent.sh b/scripts/macos-dashboard-launchagent.sh index 4f31cbf179..fe8b8aa012 100755 --- a/scripts/macos-dashboard-launchagent.sh +++ b/scripts/macos-dashboard-launchagent.sh @@ -611,7 +611,7 @@ verify_current_chat_runtime() { for attempt in {1..50}; do (( SECONDS < deadline )) || break request_timeout=$((deadline - SECONDS)) - (( request_timeout <= 1 )) || request_timeout=1 + (( request_timeout <= 5 )) || request_timeout=5 actual="$(chat_runtime_identity "$request_timeout" 2>/dev/null || true)" if [[ -n "$actual" && "$actual" == "$expected" ]]; then echo "- chat_runtime: current release identity verified"