From a4ba740149bc9f0022173a4c10396ca4d60f5c42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaqu=C3=ADn=20Cartagena?= Date: Sun, 19 Jul 2026 16:53:38 +0200 Subject: [PATCH 1/5] fix(windows): make claude-cli provider work on Windows Two issues prevented the claude-cli LLM provider from running on Windows: - cwd="/tmp" is a POSIX-only path; on Windows this raises WinError 267 (invalid directory name). Replaced with tempfile.gettempdir() for both the claude and codex subprocess calls, which resolves correctly on all platforms. - The prompt was passed as a trailing argv element (["claude", "-p", "--output-format", "json", prompt]). Large prompts (tens of KB, common once a few input files are attached) exceed Windows' command-line length limit and raise WinError 206 ("filename or extension is too long"). Passing the prompt via stdin instead removes this ceiling. Bumped the subprocess timeout from 300s to 600s to give slower/CLI-based providers headroom, and set encoding="utf-8", errors="replace" explicitly since Windows' default locale encoding (cp1252) was raising UnicodeDecodeError on non-ASCII input/output. Tested on Windows 11 running full simulations (100+ agents, 15 rounds) end to end with LLM_PROVIDER=claude-cli. --- app/utils/llm_client.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/app/utils/llm_client.py b/app/utils/llm_client.py index f4fbb4c..a417d40 100644 --- a/app/utils/llm_client.py +++ b/app/utils/llm_client.py @@ -6,6 +6,7 @@ import re import subprocess import time +import tempfile from typing import Optional, Dict, Any, List from ..config import Config @@ -92,9 +93,11 @@ def _chat_claude_cli( try: result = subprocess.run( - ["claude", "-p", "--output-format", "json", prompt], - capture_output=True, text=True, timeout=300, - cwd="/tmp" + ["claude", "-p", "--output-format", "json"], + input=prompt, + capture_output=True, text=True, timeout=600, + encoding="utf-8", errors="replace", + cwd=tempfile.gettempdir() ) if result.returncode != 0: @@ -140,7 +143,7 @@ def _chat_codex_cli( ["codex", "exec", "--skip-git-repo-check"], input=prompt, capture_output=True, text=True, timeout=180, - cwd="/tmp" + cwd=tempfile.gettempdir() ) if result.returncode != 0: From 4d655bad2e76fea95d3e0b20a72e443d2dd90877 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaqu=C3=ADn=20Cartagena?= Date: Sun, 19 Jul 2026 16:53:45 +0200 Subject: [PATCH 2/5] fix(deps): pin rapidfuzz to 3.13.0 for Windows wheel availability rapidfuzz's latest release at time of writing (3.14.4) has no win_amd64 wheel published, which makes `uv sync` fail outright on Windows (no sdist build toolchain assumed). Overriding to 3.13.0, the latest version with a Windows wheel, unblocks installation. This can be relaxed/removed once upstream publishes Windows wheels for a newer version again. --- pyproject.toml | 3 ++ uv.lock | 87 +++++++++++++++++++++++++++++++------------------- 2 files changed, 57 insertions(+), 33 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index ab4f119..f015395 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,3 +43,6 @@ dev = [ [tool.hatch.build.targets.wheel] packages = ["app"] + +[tool.uv] +override-dependencies = ["rapidfuzz==3.13.0"] diff --git a/uv.lock b/uv.lock index 8fa92c6..a936512 100644 --- a/uv.lock +++ b/uv.lock @@ -6,6 +6,9 @@ resolution-markers = [ "python_full_version < '3.12'", ] +[manifest] +overrides = [{ name = "rapidfuzz", specifier = "==3.13.0" }] + [[package]] name = "aiofiles" version = "25.1.0" @@ -352,37 +355,37 @@ wheels = [ [package.optional-dependencies] cublas = [ - { name = "nvidia-cublas", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "nvidia-cublas" }, ] cudart = [ - { name = "nvidia-cuda-runtime", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "nvidia-cuda-runtime" }, ] cufft = [ - { name = "nvidia-cufft", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "nvidia-cufft" }, ] cufile = [ - { name = "nvidia-cufile", marker = "sys_platform == 'linux'" }, + { name = "nvidia-cufile" }, ] cupti = [ - { name = "nvidia-cuda-cupti", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "nvidia-cuda-cupti" }, ] curand = [ - { name = "nvidia-curand", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "nvidia-curand" }, ] cusolver = [ - { name = "nvidia-cusolver", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "nvidia-cusolver" }, ] cusparse = [ - { name = "nvidia-cusparse", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "nvidia-cusparse" }, ] nvjitlink = [ - { name = "nvidia-nvjitlink", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "nvidia-nvjitlink" }, ] nvrtc = [ - { name = "nvidia-cuda-nvrtc", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "nvidia-cuda-nvrtc" }, ] nvtx = [ - { name = "nvidia-nvtx", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "nvidia-nvtx" }, ] [[package]] @@ -1656,28 +1659,46 @@ wheels = [ [[package]] name = "rapidfuzz" -version = "3.14.4" -source = { registry = "https://pypi.org/simple" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6e/11/b33c1d3f35c946a90184817727be4750d5ffa7c6d99bdd9400a8bb6ce9f4/rapidfuzz-3.14.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ce41a3a8ff4c672007e65c68f0e70662fac69469e57dfb5241e886ba3b536430", size = 1952370, upload-time = "2026-04-07T00:00:24.593Z" }, - { url = "https://files.pythonhosted.org/packages/5c/3b/e80612ea074fce99dad4b22e53a4577bdb115cf04cabe28ed2dc67740b8e/rapidfuzz-3.14.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3ea968514d4377f85fd65719d14d1a694cf4336f69bde4bd917b4a02b7433e6d", size = 1159777, upload-time = "2026-04-07T00:00:26.591Z" }, - { url = "https://files.pythonhosted.org/packages/2a/a5/47b0f262f397e390fb4628a374a8760dac008b57848011020ce2f9eefff8/rapidfuzz-3.14.4-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6888bfae9d6efe3846cbb37a56c5883b3f738bb2de81060a9e7f17a145f91b26", size = 1383674, upload-time = "2026-04-07T00:00:28.561Z" }, - { url = "https://files.pythonhosted.org/packages/36/c2/24da03cb3c1fc199def4f4f42e513b2424d749a76574eb2d122f1e3e0310/rapidfuzz-3.14.4-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9766370de1cef0fe2867abbf3669500f19c664b9d682385d3e905b8fc3b9feb0", size = 3168903, upload-time = "2026-04-07T00:00:30.63Z" }, - { url = "https://files.pythonhosted.org/packages/8f/76/458e34b6992e4d3a64c846948b49577b35e0384e1a2138d8c15957e16b5c/rapidfuzz-3.14.4-cp311-cp311-manylinux_2_39_riscv64.whl", hash = "sha256:4ccd1f37abefdd1e3d45ea6d65ff57192f76b2fd7ca1c6976314bbb65c85c7f3", size = 1478174, upload-time = "2026-04-07T00:00:32.337Z" }, - { url = "https://files.pythonhosted.org/packages/56/06/c274acd94662fddb4d1d376c0879423d0b8e48ebb6b1e40003ab09c013a6/rapidfuzz-3.14.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0c1cf90fbea5d6e992d2d3d4ff66212e7eb703ba7e4f1300f0a06717b91a21b3", size = 2402439, upload-time = "2026-04-07T00:00:34.312Z" }, - { url = "https://files.pythonhosted.org/packages/14/c4/86beae78be8640168e1239afcdbd789792f45eec4d3f6f7469f183bdda32/rapidfuzz-3.14.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:fe1011ccba99a83adfe47c089d1c4ce002a3368eb4cb03959e95a57ece6f59e0", size = 2511625, upload-time = "2026-04-07T00:00:36.631Z" }, - { url = "https://files.pythonhosted.org/packages/aa/3a/562bc5ca5ad7bcc8443d2b998999bf2c6e3cfd6df13974aea4ab1622e8e4/rapidfuzz-3.14.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f5b3c1c7ac2a4bd5b38c1b5e4c6fb594df55085d3d478c00e68ab7e397537517", size = 4275483, upload-time = "2026-04-07T00:00:39.086Z" }, - { url = "https://files.pythonhosted.org/packages/d5/81/186a4ac40e23de1256dd89100aab3d74da0463e7d11ac1a7461f24baebc9/rapidfuzz-3.14.4-cp311-cp311-win32.whl", hash = "sha256:e8e50e7ffe682e4b343ec0927e8fd305d03ba9edf68f6f458f753855e0eb8f5d", size = 1725589, upload-time = "2026-04-07T00:00:41.243Z" }, - { url = "https://files.pythonhosted.org/packages/17/72/71f7ebf61edbaecd4e595936107efe5c4c5bb7485efab0da51f00e224179/rapidfuzz-3.14.4-cp311-cp311-win_amd64.whl", hash = "sha256:2f525b81cd6d5132ec713f4369b8f05ba9bf42a54d1d0c349461f18f59242b42", size = 1545958, upload-time = "2026-04-07T00:00:43.154Z" }, - { url = "https://files.pythonhosted.org/packages/e0/36/92e31439ba7b2e9acd1979121fa8622aa9eb029da01a52ed1beb152e6423/rapidfuzz-3.14.4-cp311-cp311-win_arm64.whl", hash = "sha256:89ea914e81d0c1863c99fe9dfbee6010eecfc61cefa4f88e2af6ce976b171f9c", size = 816833, upload-time = "2026-04-07T00:00:44.901Z" }, - { url = "https://files.pythonhosted.org/packages/de/0d/33d2a36f210a503de648bf3c6b3692aac189e1fe42675f0c68bdf3815235/rapidfuzz-3.14.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:efdc67c5b2eb58e4e74ec1069b67c3df681e4ab07c1436f0a78f915a5ce97444", size = 1944599, upload-time = "2026-04-07T00:00:47.2Z" }, - { url = "https://files.pythonhosted.org/packages/5e/69/c3026437e339b90f71d4dc648cf82974419b6397aaf08c06bdec8ef4c547/rapidfuzz-3.14.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d66ac238f8b30c9e71456533c993b88487fcba9366b1e95f5f836c93efb0ba38", size = 1164293, upload-time = "2026-04-07T00:00:48.977Z" }, - { url = "https://files.pythonhosted.org/packages/18/75/587b46ab8a31b0081c9fe3ea2a3532b8527d551566f962df685c05ce630a/rapidfuzz-3.14.4-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fe0b2895971834d5f59d2efd193810534834a131a5482333f9112c0cec2bd7af", size = 1371999, upload-time = "2026-04-07T00:00:50.701Z" }, - { url = "https://files.pythonhosted.org/packages/b2/99/b7fdf163d3a85a8860af736e959c0a3f8146f1795eacdfbc40083122d075/rapidfuzz-3.14.4-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:893c172f170ab401f52082505d240f166c338b3b16f962acd12b31e2462a3008", size = 3145710, upload-time = "2026-04-07T00:00:53.152Z" }, - { url = "https://files.pythonhosted.org/packages/f4/39/780651ebe59923a16479756f6d732cb950fd9da9257949fb1fa8159bb210/rapidfuzz-3.14.4-cp312-cp312-manylinux_2_39_riscv64.whl", hash = "sha256:5fb9d58e8cfce5c5f9021da76924f2d5e86a21cb25bc4d694aa336b617a063e5", size = 1456303, upload-time = "2026-04-07T00:00:55.066Z" }, - { url = "https://files.pythonhosted.org/packages/59/be/a3427347176ad0b38ebf6c049c2885269d75f39e722d4559c8b0805306e3/rapidfuzz-3.14.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ca4449bc19e73a32279820ac0412dd1da66c2d6a5ae83f8abbf40ef77d793f03", size = 2389085, upload-time = "2026-04-07T00:00:57.019Z" }, - { url = "https://files.pythonhosted.org/packages/68/fb/3043f671dfc37eeceeb0d2398cffd86a595fb5e9c11739c6d8dc1c8c5527/rapidfuzz-3.14.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:e1487b9ac54cdf835625d6347786b7b5df4e8f99102c8f5b2d221ab8229d1b50", size = 2493403, upload-time = "2026-04-07T00:00:58.935Z" }, - { url = "https://files.pythonhosted.org/packages/46/2a/8ef715f863f482a15c6b60e86ef7bfac84a3b5203ec03a957f24f909028a/rapidfuzz-3.14.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:38798a4cead67dd04917c5d63e3108f4bb8a1ca0ba022deef383d808908dec9e", size = 4251706, upload-time = "2026-04-07T00:01:01.077Z" }, +version = "3.13.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ed/f6/6895abc3a3d056b9698da3199b04c0e56226d530ae44a470edabf8b664f0/rapidfuzz-3.13.0.tar.gz", hash = "sha256:d2eaf3839e52cbcc0accbe9817a67b4b0fcf70aaeb229cfddc1c28061f9ce5d8", size = 57904226, upload-time = "2025-04-03T20:38:51.226Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/17/9be9eff5a3c7dfc831c2511262082c6786dca2ce21aa8194eef1cb71d67a/rapidfuzz-3.13.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d395a5cad0c09c7f096433e5fd4224d83b53298d53499945a9b0e5a971a84f3a", size = 1999453, upload-time = "2025-04-03T20:35:40.804Z" }, + { url = "https://files.pythonhosted.org/packages/75/67/62e57896ecbabe363f027d24cc769d55dd49019e576533ec10e492fcd8a2/rapidfuzz-3.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b7b3eda607a019169f7187328a8d1648fb9a90265087f6903d7ee3a8eee01805", size = 1450881, upload-time = "2025-04-03T20:35:42.734Z" }, + { url = "https://files.pythonhosted.org/packages/96/5c/691c5304857f3476a7b3df99e91efc32428cbe7d25d234e967cc08346c13/rapidfuzz-3.13.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:98e0bfa602e1942d542de077baf15d658bd9d5dcfe9b762aff791724c1c38b70", size = 1422990, upload-time = "2025-04-03T20:35:45.158Z" }, + { url = "https://files.pythonhosted.org/packages/46/81/7a7e78f977496ee2d613154b86b203d373376bcaae5de7bde92f3ad5a192/rapidfuzz-3.13.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bef86df6d59667d9655905b02770a0c776d2853971c0773767d5ef8077acd624", size = 5342309, upload-time = "2025-04-03T20:35:46.952Z" }, + { url = "https://files.pythonhosted.org/packages/51/44/12fdd12a76b190fe94bf38d252bb28ddf0ab7a366b943e792803502901a2/rapidfuzz-3.13.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fedd316c165beed6307bf754dee54d3faca2c47e1f3bcbd67595001dfa11e969", size = 1656881, upload-time = "2025-04-03T20:35:49.954Z" }, + { url = "https://files.pythonhosted.org/packages/27/ae/0d933e660c06fcfb087a0d2492f98322f9348a28b2cc3791a5dbadf6e6fb/rapidfuzz-3.13.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5158da7f2ec02a930be13bac53bb5903527c073c90ee37804090614cab83c29e", size = 1608494, upload-time = "2025-04-03T20:35:51.646Z" }, + { url = "https://files.pythonhosted.org/packages/3d/2c/4b2f8aafdf9400e5599b6ed2f14bc26ca75f5a923571926ccbc998d4246a/rapidfuzz-3.13.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b6f913ee4618ddb6d6f3e387b76e8ec2fc5efee313a128809fbd44e65c2bbb2", size = 3072160, upload-time = "2025-04-03T20:35:53.472Z" }, + { url = "https://files.pythonhosted.org/packages/60/7d/030d68d9a653c301114101c3003b31ce01cf2c3224034cd26105224cd249/rapidfuzz-3.13.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d25fdbce6459ccbbbf23b4b044f56fbd1158b97ac50994eaae2a1c0baae78301", size = 2491549, upload-time = "2025-04-03T20:35:55.391Z" }, + { url = "https://files.pythonhosted.org/packages/8e/cd/7040ba538fc6a8ddc8816a05ecf46af9988b46c148ddd7f74fb0fb73d012/rapidfuzz-3.13.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:25343ccc589a4579fbde832e6a1e27258bfdd7f2eb0f28cb836d6694ab8591fc", size = 7584142, upload-time = "2025-04-03T20:35:57.71Z" }, + { url = "https://files.pythonhosted.org/packages/c1/96/85f7536fbceb0aa92c04a1c37a3fc4fcd4e80649e9ed0fb585382df82edc/rapidfuzz-3.13.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:a9ad1f37894e3ffb76bbab76256e8a8b789657183870be11aa64e306bb5228fd", size = 2896234, upload-time = "2025-04-03T20:35:59.969Z" }, + { url = "https://files.pythonhosted.org/packages/55/fd/460e78438e7019f2462fe9d4ecc880577ba340df7974c8a4cfe8d8d029df/rapidfuzz-3.13.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:5dc71ef23845bb6b62d194c39a97bb30ff171389c9812d83030c1199f319098c", size = 3437420, upload-time = "2025-04-03T20:36:01.91Z" }, + { url = "https://files.pythonhosted.org/packages/cc/df/c3c308a106a0993befd140a414c5ea78789d201cf1dfffb8fd9749718d4f/rapidfuzz-3.13.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b7f4c65facdb94f44be759bbd9b6dda1fa54d0d6169cdf1a209a5ab97d311a75", size = 4410860, upload-time = "2025-04-03T20:36:04.352Z" }, + { url = "https://files.pythonhosted.org/packages/75/ee/9d4ece247f9b26936cdeaae600e494af587ce9bf8ddc47d88435f05cfd05/rapidfuzz-3.13.0-cp311-cp311-win32.whl", hash = "sha256:b5104b62711565e0ff6deab2a8f5dbf1fbe333c5155abe26d2cfd6f1849b6c87", size = 1843161, upload-time = "2025-04-03T20:36:06.802Z" }, + { url = "https://files.pythonhosted.org/packages/c9/5a/d00e1f63564050a20279015acb29ecaf41646adfacc6ce2e1e450f7f2633/rapidfuzz-3.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:9093cdeb926deb32a4887ebe6910f57fbcdbc9fbfa52252c10b56ef2efb0289f", size = 1629962, upload-time = "2025-04-03T20:36:09.133Z" }, + { url = "https://files.pythonhosted.org/packages/3b/74/0a3de18bc2576b794f41ccd07720b623e840fda219ab57091897f2320fdd/rapidfuzz-3.13.0-cp311-cp311-win_arm64.whl", hash = "sha256:f70f646751b6aa9d05be1fb40372f006cc89d6aad54e9d79ae97bd1f5fce5203", size = 866631, upload-time = "2025-04-03T20:36:11.022Z" }, + { url = "https://files.pythonhosted.org/packages/13/4b/a326f57a4efed8f5505b25102797a58e37ee11d94afd9d9422cb7c76117e/rapidfuzz-3.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a1a6a906ba62f2556372282b1ef37b26bca67e3d2ea957277cfcefc6275cca7", size = 1989501, upload-time = "2025-04-03T20:36:13.43Z" }, + { url = "https://files.pythonhosted.org/packages/b7/53/1f7eb7ee83a06c400089ec7cb841cbd581c2edd7a4b21eb2f31030b88daa/rapidfuzz-3.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2fd0975e015b05c79a97f38883a11236f5a24cca83aa992bd2558ceaa5652b26", size = 1445379, upload-time = "2025-04-03T20:36:16.439Z" }, + { url = "https://files.pythonhosted.org/packages/07/09/de8069a4599cc8e6d194e5fa1782c561151dea7d5e2741767137e2a8c1f0/rapidfuzz-3.13.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d4e13593d298c50c4f94ce453f757b4b398af3fa0fd2fde693c3e51195b7f69", size = 1405986, upload-time = "2025-04-03T20:36:18.447Z" }, + { url = "https://files.pythonhosted.org/packages/5d/77/d9a90b39c16eca20d70fec4ca377fbe9ea4c0d358c6e4736ab0e0e78aaf6/rapidfuzz-3.13.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ed6f416bda1c9133000009d84d9409823eb2358df0950231cc936e4bf784eb97", size = 5310809, upload-time = "2025-04-03T20:36:20.324Z" }, + { url = "https://files.pythonhosted.org/packages/1e/7d/14da291b0d0f22262d19522afaf63bccf39fc027c981233fb2137a57b71f/rapidfuzz-3.13.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1dc82b6ed01acb536b94a43996a94471a218f4d89f3fdd9185ab496de4b2a981", size = 1629394, upload-time = "2025-04-03T20:36:22.256Z" }, + { url = "https://files.pythonhosted.org/packages/b7/e4/79ed7e4fa58f37c0f8b7c0a62361f7089b221fe85738ae2dbcfb815e985a/rapidfuzz-3.13.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e9d824de871daa6e443b39ff495a884931970d567eb0dfa213d234337343835f", size = 1600544, upload-time = "2025-04-03T20:36:24.207Z" }, + { url = "https://files.pythonhosted.org/packages/4e/20/e62b4d13ba851b0f36370060025de50a264d625f6b4c32899085ed51f980/rapidfuzz-3.13.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2d18228a2390375cf45726ce1af9d36ff3dc1f11dce9775eae1f1b13ac6ec50f", size = 3052796, upload-time = "2025-04-03T20:36:26.279Z" }, + { url = "https://files.pythonhosted.org/packages/cd/8d/55fdf4387dec10aa177fe3df8dbb0d5022224d95f48664a21d6b62a5299d/rapidfuzz-3.13.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9f5fe634c9482ec5d4a6692afb8c45d370ae86755e5f57aa6c50bfe4ca2bdd87", size = 2464016, upload-time = "2025-04-03T20:36:28.525Z" }, + { url = "https://files.pythonhosted.org/packages/9b/be/0872f6a56c0f473165d3b47d4170fa75263dc5f46985755aa9bf2bbcdea1/rapidfuzz-3.13.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:694eb531889f71022b2be86f625a4209c4049e74be9ca836919b9e395d5e33b3", size = 7556725, upload-time = "2025-04-03T20:36:30.629Z" }, + { url = "https://files.pythonhosted.org/packages/5d/f3/6c0750e484d885a14840c7a150926f425d524982aca989cdda0bb3bdfa57/rapidfuzz-3.13.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:11b47b40650e06147dee5e51a9c9ad73bb7b86968b6f7d30e503b9f8dd1292db", size = 2859052, upload-time = "2025-04-03T20:36:32.836Z" }, + { url = "https://files.pythonhosted.org/packages/6f/98/5a3a14701b5eb330f444f7883c9840b43fb29c575e292e09c90a270a6e07/rapidfuzz-3.13.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:98b8107ff14f5af0243f27d236bcc6e1ef8e7e3b3c25df114e91e3a99572da73", size = 3390219, upload-time = "2025-04-03T20:36:35.062Z" }, + { url = "https://files.pythonhosted.org/packages/e9/7d/f4642eaaeb474b19974332f2a58471803448be843033e5740965775760a5/rapidfuzz-3.13.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b836f486dba0aceb2551e838ff3f514a38ee72b015364f739e526d720fdb823a", size = 4377924, upload-time = "2025-04-03T20:36:37.363Z" }, + { url = "https://files.pythonhosted.org/packages/8e/83/fa33f61796731891c3e045d0cbca4436a5c436a170e7f04d42c2423652c3/rapidfuzz-3.13.0-cp312-cp312-win32.whl", hash = "sha256:4671ee300d1818d7bdfd8fa0608580d7778ba701817216f0c17fb29e6b972514", size = 1823915, upload-time = "2025-04-03T20:36:39.451Z" }, + { url = "https://files.pythonhosted.org/packages/03/25/5ee7ab6841ca668567d0897905eebc79c76f6297b73bf05957be887e9c74/rapidfuzz-3.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:6e2065f68fb1d0bf65adc289c1bdc45ba7e464e406b319d67bb54441a1b9da9e", size = 1616985, upload-time = "2025-04-03T20:36:41.631Z" }, + { url = "https://files.pythonhosted.org/packages/76/5e/3f0fb88db396cb692aefd631e4805854e02120a2382723b90dcae720bcc6/rapidfuzz-3.13.0-cp312-cp312-win_arm64.whl", hash = "sha256:65cc97c2fc2c2fe23586599686f3b1ceeedeca8e598cfcc1b7e56dc8ca7e2aa7", size = 860116, upload-time = "2025-04-03T20:36:43.915Z" }, + { url = "https://files.pythonhosted.org/packages/88/df/6060c5a9c879b302bd47a73fc012d0db37abf6544c57591bcbc3459673bd/rapidfuzz-3.13.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:1ba007f4d35a45ee68656b2eb83b8715e11d0f90e5b9f02d615a8a321ff00c27", size = 1905935, upload-time = "2025-04-03T20:38:18.07Z" }, + { url = "https://files.pythonhosted.org/packages/a2/6c/a0b819b829e20525ef1bd58fc776fb8d07a0c38d819e63ba2b7c311a2ed4/rapidfuzz-3.13.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d7a217310429b43be95b3b8ad7f8fc41aba341109dc91e978cd7c703f928c58f", size = 1383714, upload-time = "2025-04-03T20:38:20.628Z" }, + { url = "https://files.pythonhosted.org/packages/6a/c1/3da3466cc8a9bfb9cd345ad221fac311143b6a9664b5af4adb95b5e6ce01/rapidfuzz-3.13.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:558bf526bcd777de32b7885790a95a9548ffdcce68f704a81207be4a286c1095", size = 1367329, upload-time = "2025-04-03T20:38:23.01Z" }, + { url = "https://files.pythonhosted.org/packages/da/f0/9f2a9043bfc4e66da256b15d728c5fc2d865edf0028824337f5edac36783/rapidfuzz-3.13.0-pp311-pypy311_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:202a87760f5145140d56153b193a797ae9338f7939eb16652dd7ff96f8faf64c", size = 5251057, upload-time = "2025-04-03T20:38:25.52Z" }, + { url = "https://files.pythonhosted.org/packages/6a/ff/af2cb1d8acf9777d52487af5c6b34ce9d13381a753f991d95ecaca813407/rapidfuzz-3.13.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cfcccc08f671646ccb1e413c773bb92e7bba789e3a1796fd49d23c12539fe2e4", size = 2992401, upload-time = "2025-04-03T20:38:28.196Z" }, + { url = "https://files.pythonhosted.org/packages/c1/c5/c243b05a15a27b946180db0d1e4c999bef3f4221505dff9748f1f6c917be/rapidfuzz-3.13.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:1f219f1e3c3194d7a7de222f54450ce12bc907862ff9a8962d83061c1f923c86", size = 1553782, upload-time = "2025-04-03T20:38:30.778Z" }, ] [[package]] From 6b579aa7d9d4532922cff090f5d6a3c360d49aab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaqu=C3=ADn=20Cartagena?= Date: Sun, 19 Jul 2026 16:54:14 +0200 Subject: [PATCH 3/5] fix(interview): raise batch interview timeout from 180s to 600s MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit interview_agents() (used by the report generator to interview the top N most-active agents post-simulation) sends a batch IPC command with a hardcoded 180s timeout covering up to N agents x 2 platforms of individual LLM interview calls. With CLI-based LLM providers this was consistently timing out with 0/N successful interviews on runs with 100+ agent profiles, even though the simulation itself completed normally — the report would silently fall back to "(no interview data)" since the failure is caught as non-fatal by the caller. 180s doesn't leave enough headroom when each individual call can legitimately take up to the LLM client's own per-call ceiling (600s in llm_client.py, e.g. for claude-cli subprocess spawns). Raised to match that ceiling. --- app/services/graph_tools.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/app/services/graph_tools.py b/app/services/graph_tools.py index 92d3b62..291df71 100644 --- a/app/services/graph_tools.py +++ b/app/services/graph_tools.py @@ -352,7 +352,14 @@ def interview_agents( simulation_id=simulation_id, interviews=interviews_request, platform=None, - timeout=180.0, + # 180s was too tight for CLI-based LLM providers (e.g. claude-cli): + # each interview response is a subprocess spawn that can legitimately + # take up to the LLM client's own per-call ceiling (600s in + # llm_client.py). A batch of up to len(selected_indices) agents x 2 + # platforms was routinely timing out at 0/N successful interviews + # even on runs with 100+ agent profiles. Raised to match that ceiling + # with headroom for a few sequential/parallel calls. + timeout=600.0, ) if not api_result.get("success", False): From 24b4a587fe122c553296102454ef72a213e0961f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaqu=C3=ADn=20Cartagena?= Date: Sun, 19 Jul 2026 18:12:45 +0200 Subject: [PATCH 4/5] fix(review): harden CLI provider handling per review feedback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses self-review findings on the original three commits: - Single CLI_CALL_TIMEOUT_SECONDS constant governs both providers and every timeout error message (the claude handler still said "300s" after the ceiling moved to 600; codex was left at 180 with no encoding handling — same cp1252 hazards the claude path already fixed). - Interview batch budget now scales with the batch instead of a flat 600s: one full per-call ceiling per interviewed agent. A fixed budget equals the worst case of a single call and still starves multi-agent batches. - rapidfuzz override scoped with sys_platform == 'win32' marker so macOS/Linux keep the resolver's pick; pyproject now documents why the pin exists and when to drop it (the missing wheels are specifically Windows cp312, not all of win_amd64). - Provider binaries resolved via shutil.which for a clear early error when the CLI is missing (and to tolerate npm-style .cmd shims on Windows). - Warn when undecodable bytes were replaced with U+FFFD instead of corrupting output silently. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- app/services/graph_tools.py | 14 ++++++------- app/utils/llm_client.py | 42 +++++++++++++++++++++++++++++++------ pyproject.toml | 5 ++++- uv.lock | 33 ++--------------------------- 4 files changed, 48 insertions(+), 46 deletions(-) diff --git a/app/services/graph_tools.py b/app/services/graph_tools.py index 291df71..afa411a 100644 --- a/app/services/graph_tools.py +++ b/app/services/graph_tools.py @@ -299,6 +299,7 @@ def interview_agents( ) -> InterviewResult: """Interview simulated agents via OASIS IPC.""" from .simulation_runner import SimulationRunner + from ..utils.llm_client import CLI_CALL_TIMEOUT_SECONDS result = InterviewResult( interview_topic=interview_requirement, @@ -348,18 +349,15 @@ def interview_agents( for idx in selected_indices ] + # One full CLI-call ceiling per interviewed agent: a fixed 180s budget + # starved whole batches to 0/N with subprocess-based providers. + batch_timeout = float(CLI_CALL_TIMEOUT_SECONDS * max(1, len(interviews_request))) + api_result = SimulationRunner.interview_agents_batch( simulation_id=simulation_id, interviews=interviews_request, platform=None, - # 180s was too tight for CLI-based LLM providers (e.g. claude-cli): - # each interview response is a subprocess spawn that can legitimately - # take up to the LLM client's own per-call ceiling (600s in - # llm_client.py). A batch of up to len(selected_indices) agents x 2 - # platforms was routinely timing out at 0/N successful interviews - # even on runs with 100+ agent profiles. Raised to match that ceiling - # with headroom for a few sequential/parallel calls. - timeout=600.0, + timeout=batch_timeout, ) if not api_result.get("success", False): diff --git a/app/utils/llm_client.py b/app/utils/llm_client.py index a417d40..78c0ff6 100644 --- a/app/utils/llm_client.py +++ b/app/utils/llm_client.py @@ -4,6 +4,7 @@ import json import re +import shutil import subprocess import time import tempfile @@ -17,6 +18,28 @@ MAX_RETRIES = 3 RETRY_BASE_DELAY = 2.0 # seconds +# Wall-clock ceiling for a single CLI provider call. Long prompts (large +# knowledge-graph context, interview batches) can legitimately take several +# minutes through a subprocess-based provider. Callers that wait on multiple +# sequential calls (e.g. interview batches) should budget per call using this +# constant rather than hardcoding their own number. +CLI_CALL_TIMEOUT_SECONDS = 600 + + +def _resolve_cli(binary: str) -> str: + """Resolve a provider CLI to an absolute path, failing with a clear error. + + On Windows, invoking a bare name only works for .exe; npm-style .cmd shims + need the resolved path. shutil.which also gives us a clean early failure + instead of an opaque FileNotFoundError mid-simulation. + """ + resolved = shutil.which(binary) + if resolved is None: + raise RuntimeError( + f"'{binary}' CLI not found on PATH. Install it and log in before running simulations." + ) + return resolved + class LLMClient: """LLM Client — supports claude-cli and codex-cli.""" @@ -93,9 +116,9 @@ def _chat_claude_cli( try: result = subprocess.run( - ["claude", "-p", "--output-format", "json"], + [_resolve_cli("claude"), "-p", "--output-format", "json"], input=prompt, - capture_output=True, text=True, timeout=600, + capture_output=True, text=True, timeout=CLI_CALL_TIMEOUT_SECONDS, encoding="utf-8", errors="replace", cwd=tempfile.gettempdir() ) @@ -104,6 +127,9 @@ def _chat_claude_cli( logger.error(f"Claude CLI error: {result.stderr[:200]}") raise RuntimeError(f"Claude CLI failed: {result.stderr[:200]}") + if "�" in result.stdout: + logger.warning("Claude CLI output contained undecodable bytes; replaced with U+FFFD") + try: output = json.loads(result.stdout) content = output.get("result", result.stdout) @@ -113,7 +139,7 @@ def _chat_claude_cli( return self._clean_content(content) except subprocess.TimeoutExpired: - raise RuntimeError("Claude CLI timed out after 300s") + raise RuntimeError(f"Claude CLI timed out after {CLI_CALL_TIMEOUT_SECONDS}s") def _chat_codex_cli( self, @@ -140,9 +166,10 @@ def _chat_codex_cli( try: result = subprocess.run( - ["codex", "exec", "--skip-git-repo-check"], + [_resolve_cli("codex"), "exec", "--skip-git-repo-check"], input=prompt, - capture_output=True, text=True, timeout=180, + capture_output=True, text=True, timeout=CLI_CALL_TIMEOUT_SECONDS, + encoding="utf-8", errors="replace", cwd=tempfile.gettempdir() ) @@ -150,6 +177,9 @@ def _chat_codex_cli( logger.error(f"Codex CLI error: {result.stderr[:200]}") raise RuntimeError(f"Codex CLI failed: {result.stderr[:200]}") + if "�" in result.stdout: + logger.warning("Codex CLI output contained undecodable bytes; replaced with U+FFFD") + raw = result.stdout.strip() parts = raw.split("\ncodex\n") if len(parts) > 1: @@ -166,7 +196,7 @@ def _chat_codex_cli( return self._clean_content(content) except subprocess.TimeoutExpired: - raise RuntimeError("Codex CLI timed out after 180s") + raise RuntimeError(f"Codex CLI timed out after {CLI_CALL_TIMEOUT_SECONDS}s") def chat_json( self, diff --git a/pyproject.toml b/pyproject.toml index f015395..52aca74 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -45,4 +45,7 @@ dev = [ packages = ["app"] [tool.uv] -override-dependencies = ["rapidfuzz==3.13.0"] +# rapidfuzz >=3.14 ships no Windows cp312 wheels (and no sdist in-range), which +# breaks `uv sync` under requires-python <3.13 on Windows. Scoped to win32 so +# macOS/Linux keep the resolver's pick; drop once upstream publishes wheels. +override-dependencies = ["rapidfuzz==3.13.0; sys_platform == 'win32'"] diff --git a/uv.lock b/uv.lock index a936512..b2d0726 100644 --- a/uv.lock +++ b/uv.lock @@ -7,7 +7,7 @@ resolution-markers = [ ] [manifest] -overrides = [{ name = "rapidfuzz", specifier = "==3.13.0" }] +overrides = [{ name = "rapidfuzz", marker = "sys_platform == 'win32'", specifier = "==3.13.0" }] [[package]] name = "aiofiles" @@ -1663,41 +1663,12 @@ version = "3.13.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/ed/f6/6895abc3a3d056b9698da3199b04c0e56226d530ae44a470edabf8b664f0/rapidfuzz-3.13.0.tar.gz", hash = "sha256:d2eaf3839e52cbcc0accbe9817a67b4b0fcf70aaeb229cfddc1c28061f9ce5d8", size = 57904226, upload-time = "2025-04-03T20:38:51.226Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/87/17/9be9eff5a3c7dfc831c2511262082c6786dca2ce21aa8194eef1cb71d67a/rapidfuzz-3.13.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d395a5cad0c09c7f096433e5fd4224d83b53298d53499945a9b0e5a971a84f3a", size = 1999453, upload-time = "2025-04-03T20:35:40.804Z" }, - { url = "https://files.pythonhosted.org/packages/75/67/62e57896ecbabe363f027d24cc769d55dd49019e576533ec10e492fcd8a2/rapidfuzz-3.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b7b3eda607a019169f7187328a8d1648fb9a90265087f6903d7ee3a8eee01805", size = 1450881, upload-time = "2025-04-03T20:35:42.734Z" }, - { url = "https://files.pythonhosted.org/packages/96/5c/691c5304857f3476a7b3df99e91efc32428cbe7d25d234e967cc08346c13/rapidfuzz-3.13.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:98e0bfa602e1942d542de077baf15d658bd9d5dcfe9b762aff791724c1c38b70", size = 1422990, upload-time = "2025-04-03T20:35:45.158Z" }, - { url = "https://files.pythonhosted.org/packages/46/81/7a7e78f977496ee2d613154b86b203d373376bcaae5de7bde92f3ad5a192/rapidfuzz-3.13.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bef86df6d59667d9655905b02770a0c776d2853971c0773767d5ef8077acd624", size = 5342309, upload-time = "2025-04-03T20:35:46.952Z" }, - { url = "https://files.pythonhosted.org/packages/51/44/12fdd12a76b190fe94bf38d252bb28ddf0ab7a366b943e792803502901a2/rapidfuzz-3.13.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fedd316c165beed6307bf754dee54d3faca2c47e1f3bcbd67595001dfa11e969", size = 1656881, upload-time = "2025-04-03T20:35:49.954Z" }, - { url = "https://files.pythonhosted.org/packages/27/ae/0d933e660c06fcfb087a0d2492f98322f9348a28b2cc3791a5dbadf6e6fb/rapidfuzz-3.13.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5158da7f2ec02a930be13bac53bb5903527c073c90ee37804090614cab83c29e", size = 1608494, upload-time = "2025-04-03T20:35:51.646Z" }, - { url = "https://files.pythonhosted.org/packages/3d/2c/4b2f8aafdf9400e5599b6ed2f14bc26ca75f5a923571926ccbc998d4246a/rapidfuzz-3.13.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b6f913ee4618ddb6d6f3e387b76e8ec2fc5efee313a128809fbd44e65c2bbb2", size = 3072160, upload-time = "2025-04-03T20:35:53.472Z" }, - { url = "https://files.pythonhosted.org/packages/60/7d/030d68d9a653c301114101c3003b31ce01cf2c3224034cd26105224cd249/rapidfuzz-3.13.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d25fdbce6459ccbbbf23b4b044f56fbd1158b97ac50994eaae2a1c0baae78301", size = 2491549, upload-time = "2025-04-03T20:35:55.391Z" }, - { url = "https://files.pythonhosted.org/packages/8e/cd/7040ba538fc6a8ddc8816a05ecf46af9988b46c148ddd7f74fb0fb73d012/rapidfuzz-3.13.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:25343ccc589a4579fbde832e6a1e27258bfdd7f2eb0f28cb836d6694ab8591fc", size = 7584142, upload-time = "2025-04-03T20:35:57.71Z" }, - { url = "https://files.pythonhosted.org/packages/c1/96/85f7536fbceb0aa92c04a1c37a3fc4fcd4e80649e9ed0fb585382df82edc/rapidfuzz-3.13.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:a9ad1f37894e3ffb76bbab76256e8a8b789657183870be11aa64e306bb5228fd", size = 2896234, upload-time = "2025-04-03T20:35:59.969Z" }, - { url = "https://files.pythonhosted.org/packages/55/fd/460e78438e7019f2462fe9d4ecc880577ba340df7974c8a4cfe8d8d029df/rapidfuzz-3.13.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:5dc71ef23845bb6b62d194c39a97bb30ff171389c9812d83030c1199f319098c", size = 3437420, upload-time = "2025-04-03T20:36:01.91Z" }, - { url = "https://files.pythonhosted.org/packages/cc/df/c3c308a106a0993befd140a414c5ea78789d201cf1dfffb8fd9749718d4f/rapidfuzz-3.13.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b7f4c65facdb94f44be759bbd9b6dda1fa54d0d6169cdf1a209a5ab97d311a75", size = 4410860, upload-time = "2025-04-03T20:36:04.352Z" }, { url = "https://files.pythonhosted.org/packages/75/ee/9d4ece247f9b26936cdeaae600e494af587ce9bf8ddc47d88435f05cfd05/rapidfuzz-3.13.0-cp311-cp311-win32.whl", hash = "sha256:b5104b62711565e0ff6deab2a8f5dbf1fbe333c5155abe26d2cfd6f1849b6c87", size = 1843161, upload-time = "2025-04-03T20:36:06.802Z" }, { url = "https://files.pythonhosted.org/packages/c9/5a/d00e1f63564050a20279015acb29ecaf41646adfacc6ce2e1e450f7f2633/rapidfuzz-3.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:9093cdeb926deb32a4887ebe6910f57fbcdbc9fbfa52252c10b56ef2efb0289f", size = 1629962, upload-time = "2025-04-03T20:36:09.133Z" }, { url = "https://files.pythonhosted.org/packages/3b/74/0a3de18bc2576b794f41ccd07720b623e840fda219ab57091897f2320fdd/rapidfuzz-3.13.0-cp311-cp311-win_arm64.whl", hash = "sha256:f70f646751b6aa9d05be1fb40372f006cc89d6aad54e9d79ae97bd1f5fce5203", size = 866631, upload-time = "2025-04-03T20:36:11.022Z" }, - { url = "https://files.pythonhosted.org/packages/13/4b/a326f57a4efed8f5505b25102797a58e37ee11d94afd9d9422cb7c76117e/rapidfuzz-3.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a1a6a906ba62f2556372282b1ef37b26bca67e3d2ea957277cfcefc6275cca7", size = 1989501, upload-time = "2025-04-03T20:36:13.43Z" }, - { url = "https://files.pythonhosted.org/packages/b7/53/1f7eb7ee83a06c400089ec7cb841cbd581c2edd7a4b21eb2f31030b88daa/rapidfuzz-3.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2fd0975e015b05c79a97f38883a11236f5a24cca83aa992bd2558ceaa5652b26", size = 1445379, upload-time = "2025-04-03T20:36:16.439Z" }, - { url = "https://files.pythonhosted.org/packages/07/09/de8069a4599cc8e6d194e5fa1782c561151dea7d5e2741767137e2a8c1f0/rapidfuzz-3.13.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d4e13593d298c50c4f94ce453f757b4b398af3fa0fd2fde693c3e51195b7f69", size = 1405986, upload-time = "2025-04-03T20:36:18.447Z" }, - { url = "https://files.pythonhosted.org/packages/5d/77/d9a90b39c16eca20d70fec4ca377fbe9ea4c0d358c6e4736ab0e0e78aaf6/rapidfuzz-3.13.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ed6f416bda1c9133000009d84d9409823eb2358df0950231cc936e4bf784eb97", size = 5310809, upload-time = "2025-04-03T20:36:20.324Z" }, - { url = "https://files.pythonhosted.org/packages/1e/7d/14da291b0d0f22262d19522afaf63bccf39fc027c981233fb2137a57b71f/rapidfuzz-3.13.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1dc82b6ed01acb536b94a43996a94471a218f4d89f3fdd9185ab496de4b2a981", size = 1629394, upload-time = "2025-04-03T20:36:22.256Z" }, - { url = "https://files.pythonhosted.org/packages/b7/e4/79ed7e4fa58f37c0f8b7c0a62361f7089b221fe85738ae2dbcfb815e985a/rapidfuzz-3.13.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e9d824de871daa6e443b39ff495a884931970d567eb0dfa213d234337343835f", size = 1600544, upload-time = "2025-04-03T20:36:24.207Z" }, - { url = "https://files.pythonhosted.org/packages/4e/20/e62b4d13ba851b0f36370060025de50a264d625f6b4c32899085ed51f980/rapidfuzz-3.13.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2d18228a2390375cf45726ce1af9d36ff3dc1f11dce9775eae1f1b13ac6ec50f", size = 3052796, upload-time = "2025-04-03T20:36:26.279Z" }, - { url = "https://files.pythonhosted.org/packages/cd/8d/55fdf4387dec10aa177fe3df8dbb0d5022224d95f48664a21d6b62a5299d/rapidfuzz-3.13.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9f5fe634c9482ec5d4a6692afb8c45d370ae86755e5f57aa6c50bfe4ca2bdd87", size = 2464016, upload-time = "2025-04-03T20:36:28.525Z" }, - { url = "https://files.pythonhosted.org/packages/9b/be/0872f6a56c0f473165d3b47d4170fa75263dc5f46985755aa9bf2bbcdea1/rapidfuzz-3.13.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:694eb531889f71022b2be86f625a4209c4049e74be9ca836919b9e395d5e33b3", size = 7556725, upload-time = "2025-04-03T20:36:30.629Z" }, - { url = "https://files.pythonhosted.org/packages/5d/f3/6c0750e484d885a14840c7a150926f425d524982aca989cdda0bb3bdfa57/rapidfuzz-3.13.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:11b47b40650e06147dee5e51a9c9ad73bb7b86968b6f7d30e503b9f8dd1292db", size = 2859052, upload-time = "2025-04-03T20:36:32.836Z" }, - { url = "https://files.pythonhosted.org/packages/6f/98/5a3a14701b5eb330f444f7883c9840b43fb29c575e292e09c90a270a6e07/rapidfuzz-3.13.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:98b8107ff14f5af0243f27d236bcc6e1ef8e7e3b3c25df114e91e3a99572da73", size = 3390219, upload-time = "2025-04-03T20:36:35.062Z" }, - { url = "https://files.pythonhosted.org/packages/e9/7d/f4642eaaeb474b19974332f2a58471803448be843033e5740965775760a5/rapidfuzz-3.13.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b836f486dba0aceb2551e838ff3f514a38ee72b015364f739e526d720fdb823a", size = 4377924, upload-time = "2025-04-03T20:36:37.363Z" }, { url = "https://files.pythonhosted.org/packages/8e/83/fa33f61796731891c3e045d0cbca4436a5c436a170e7f04d42c2423652c3/rapidfuzz-3.13.0-cp312-cp312-win32.whl", hash = "sha256:4671ee300d1818d7bdfd8fa0608580d7778ba701817216f0c17fb29e6b972514", size = 1823915, upload-time = "2025-04-03T20:36:39.451Z" }, { url = "https://files.pythonhosted.org/packages/03/25/5ee7ab6841ca668567d0897905eebc79c76f6297b73bf05957be887e9c74/rapidfuzz-3.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:6e2065f68fb1d0bf65adc289c1bdc45ba7e464e406b319d67bb54441a1b9da9e", size = 1616985, upload-time = "2025-04-03T20:36:41.631Z" }, { url = "https://files.pythonhosted.org/packages/76/5e/3f0fb88db396cb692aefd631e4805854e02120a2382723b90dcae720bcc6/rapidfuzz-3.13.0-cp312-cp312-win_arm64.whl", hash = "sha256:65cc97c2fc2c2fe23586599686f3b1ceeedeca8e598cfcc1b7e56dc8ca7e2aa7", size = 860116, upload-time = "2025-04-03T20:36:43.915Z" }, - { url = "https://files.pythonhosted.org/packages/88/df/6060c5a9c879b302bd47a73fc012d0db37abf6544c57591bcbc3459673bd/rapidfuzz-3.13.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:1ba007f4d35a45ee68656b2eb83b8715e11d0f90e5b9f02d615a8a321ff00c27", size = 1905935, upload-time = "2025-04-03T20:38:18.07Z" }, - { url = "https://files.pythonhosted.org/packages/a2/6c/a0b819b829e20525ef1bd58fc776fb8d07a0c38d819e63ba2b7c311a2ed4/rapidfuzz-3.13.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d7a217310429b43be95b3b8ad7f8fc41aba341109dc91e978cd7c703f928c58f", size = 1383714, upload-time = "2025-04-03T20:38:20.628Z" }, - { url = "https://files.pythonhosted.org/packages/6a/c1/3da3466cc8a9bfb9cd345ad221fac311143b6a9664b5af4adb95b5e6ce01/rapidfuzz-3.13.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:558bf526bcd777de32b7885790a95a9548ffdcce68f704a81207be4a286c1095", size = 1367329, upload-time = "2025-04-03T20:38:23.01Z" }, - { url = "https://files.pythonhosted.org/packages/da/f0/9f2a9043bfc4e66da256b15d728c5fc2d865edf0028824337f5edac36783/rapidfuzz-3.13.0-pp311-pypy311_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:202a87760f5145140d56153b193a797ae9338f7939eb16652dd7ff96f8faf64c", size = 5251057, upload-time = "2025-04-03T20:38:25.52Z" }, - { url = "https://files.pythonhosted.org/packages/6a/ff/af2cb1d8acf9777d52487af5c6b34ce9d13381a753f991d95ecaca813407/rapidfuzz-3.13.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cfcccc08f671646ccb1e413c773bb92e7bba789e3a1796fd49d23c12539fe2e4", size = 2992401, upload-time = "2025-04-03T20:38:28.196Z" }, { url = "https://files.pythonhosted.org/packages/c1/c5/c243b05a15a27b946180db0d1e4c999bef3f4221505dff9748f1f6c917be/rapidfuzz-3.13.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:1f219f1e3c3194d7a7de222f54450ce12bc907862ff9a8962d83061c1f923c86", size = 1553782, upload-time = "2025-04-03T20:38:30.778Z" }, ] @@ -2274,7 +2245,7 @@ dependencies = [ { name = "numpy" }, { name = "python-iso639" }, { name = "python-magic" }, - { name = "rapidfuzz" }, + { name = "rapidfuzz", marker = "sys_platform == 'win32'" }, { name = "requests" }, { name = "tabulate" }, { name = "typing-extensions" }, From 6f51c5234c4971bbed7713b848235c4518354c60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaqu=C3=ADn=20Cartagena?= Date: Sun, 19 Jul 2026 21:07:56 +0200 Subject: [PATCH 5/5] fix(review): no-retry timeouts and cached CLI resolution MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - LLMTimeoutError (RuntimeError subclass) raised on TimeoutExpired and excluded from the x3 retry loop: a prompt that exhausts the 600s ceiling will almost certainly exhaust it again, so retrying turned one slow call into ~30 minutes of blocked wall clock. - _resolve_cli cached with lru_cache: the binary does not move during the process lifetime, so the per-request PATH scan was waste. (Failures are not cached: lru_cache does not memoize exceptions.) 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- app/utils/llm_client.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/app/utils/llm_client.py b/app/utils/llm_client.py index 78c0ff6..bf08d2e 100644 --- a/app/utils/llm_client.py +++ b/app/utils/llm_client.py @@ -2,6 +2,7 @@ LLM Client — CLI-only providers (Claude Code, Codex). """ +import functools import json import re import shutil @@ -26,6 +27,17 @@ CLI_CALL_TIMEOUT_SECONDS = 600 +class LLMTimeoutError(RuntimeError): + """A CLI call exhausted its wall-clock ceiling. + + Kept distinct from generic RuntimeError so the retry loop can skip it: + a prompt that exhausts the full ceiling will almost certainly exhaust it + again, and retrying only multiplies the wait (3x600s + backoff ~= 30 min + blocked on a single call). + """ + + +@functools.lru_cache(maxsize=None) def _resolve_cli(binary: str) -> str: """Resolve a provider CLI to an absolute path, failing with a clear error. @@ -83,6 +95,9 @@ def chat( if self.provider == "codex-cli": return self._chat_codex_cli(messages, temperature, max_tokens, response_format) return self._chat_claude_cli(messages, temperature, max_tokens, response_format) + except LLMTimeoutError: + # No retry: see LLMTimeoutError docstring + raise except RuntimeError as exc: last_error = exc if attempt < MAX_RETRIES - 1: @@ -139,7 +154,7 @@ def _chat_claude_cli( return self._clean_content(content) except subprocess.TimeoutExpired: - raise RuntimeError(f"Claude CLI timed out after {CLI_CALL_TIMEOUT_SECONDS}s") + raise LLMTimeoutError(f"Claude CLI timed out after {CLI_CALL_TIMEOUT_SECONDS}s") def _chat_codex_cli( self, @@ -196,7 +211,7 @@ def _chat_codex_cli( return self._clean_content(content) except subprocess.TimeoutExpired: - raise RuntimeError(f"Codex CLI timed out after {CLI_CALL_TIMEOUT_SECONDS}s") + raise LLMTimeoutError(f"Codex CLI timed out after {CLI_CALL_TIMEOUT_SECONDS}s") def chat_json( self,