From 3dd0acf2be00c7c6a42a0e43e19dbd17668c53fd Mon Sep 17 00:00:00 2001 From: Daniel Valladares Date: Thu, 21 May 2026 16:47:41 -0300 Subject: [PATCH 1/4] chore(deps): add websocket-client + regenerate lock websocket-client>=1.9 required by local skills/tools. Lock regenerated after merging origin/main; pulls in flask-limiter (rate-limit on public share endpoint, #52), limits, wrapt, ordered-set, deprecated. Co-Authored-By: Claude Opus 4.7 --- pyproject.toml | 1 + uv.lock | 151 ++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 151 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 802fcdc0..d9c0ea8c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,6 +16,7 @@ dependencies = [ "pyyaml>=6.0", "pydantic>=2.0", "flask-sock>=0.7", + "websocket-client>=1.9", "boto3>=1.35", "pywinpty>=3.0.3; sys_platform == 'win32'", "cryptography>=42", diff --git a/uv.lock b/uv.lock index 69360e19..8218e88f 100644 --- a/uv.lock +++ b/uv.lock @@ -560,6 +560,18 @@ nvtx = [ { name = "nvidia-nvtx", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, ] +[[package]] +name = "deprecated" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/49/85/12f0a49a7c4ffb70572b6c2ef13c90c88fd190debda93b23f026b25f9634/deprecated-1.3.1.tar.gz", hash = "sha256:b1b50e0ff0c1fddaa5708a2c6b0a6588bb09b892825ab2b214ac9ea9d92a5223", size = 2932523, upload-time = "2025-10-30T08:19:02.757Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/d0/205d54408c08b13550c733c4b85429e7ead111c7f0014309637425520a9a/deprecated-1.3.1-py2.py3-none-any.whl", hash = "sha256:597bfef186b6f60181535a29fbe44865ce137a5079f295b479886c82729d5f3f", size = 11298, upload-time = "2025-10-30T08:19:00.758Z" }, +] + [[package]] name = "distlib" version = "0.4.0" @@ -589,7 +601,7 @@ wheels = [ [[package]] name = "evo-nexus" -version = "0.32.2" +version = "0.33.0" source = { virtual = "." } dependencies = [ { name = "alembic" }, @@ -599,6 +611,7 @@ dependencies = [ { name = "cryptography" }, { name = "flask" }, { name = "flask-cors" }, + { name = "flask-limiter" }, { name = "flask-login" }, { name = "flask-sock" }, { name = "flask-sqlalchemy" }, @@ -618,6 +631,7 @@ dependencies = [ { name = "sqlparse" }, { name = "tiktoken" }, { name = "watchdog" }, + { name = "websocket-client" }, ] [package.dev-dependencies] @@ -634,6 +648,7 @@ requires-dist = [ { name = "cryptography", specifier = ">=42" }, { name = "flask", specifier = ">=3.0" }, { name = "flask-cors", specifier = ">=4.0" }, + { name = "flask-limiter", specifier = ">=3.5" }, { name = "flask-login", specifier = ">=0.6" }, { name = "flask-sock", specifier = ">=0.7" }, { name = "flask-sqlalchemy", specifier = ">=3.1" }, @@ -653,6 +668,7 @@ requires-dist = [ { name = "sqlparse", specifier = ">=0.4,<1.0" }, { name = "tiktoken", specifier = ">=0.7" }, { name = "watchdog", specifier = ">=4.0" }, + { name = "websocket-client", specifier = ">=1.9" }, ] [package.metadata.requires-dev] @@ -718,6 +734,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/4f/af/72ad54402e599152de6d067324c46fe6a4f531c7c65baf7e96c63db55eaf/flask_cors-6.0.2-py3-none-any.whl", hash = "sha256:e57544d415dfd7da89a9564e1e3a9e515042df76e12130641ca6f3f2f03b699a", size = 13257, upload-time = "2025-12-12T20:31:41.3Z" }, ] +[[package]] +name = "flask-limiter" +version = "4.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "flask" }, + { name = "limits" }, + { name = "ordered-set" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4e/98/71780be5d1afb941219c4b48d241d4e246e3062b017caa4e79c4dc71314c/flask_limiter-4.1.1.tar.gz", hash = "sha256:ca11608fc7eec43dcea606964ca07c3bd4ec1ae89043a0f67f717899a4f48106", size = 403198, upload-time = "2025-12-06T17:39:00.575Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/75/7c/9fe9ffc83be199011bb0c6deb82cdcbc5a355601e380581de9dbc30490dd/flask_limiter-4.1.1-py3-none-any.whl", hash = "sha256:e1ae13e06e6b3e39a4902e7d240b901586b25932c2add7bd5f5eeb4bdc11111b", size = 30554, upload-time = "2025-12-06T17:38:59.162Z" }, +] + [[package]] name = "flask-login" version = "0.6.3" @@ -1155,6 +1186,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" }, ] +[[package]] +name = "limits" +version = "5.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "deprecated" }, + { name = "packaging" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/71/69/826a5d1f45426c68d8f6539f8d275c0e4fcaa57f0c017ec3100986558a41/limits-5.8.0.tar.gz", hash = "sha256:c9e0d74aed837e8f6f50d1fcebcf5fd8130957287206bc3799adaee5092655da", size = 226104, upload-time = "2026-02-05T07:17:35.859Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b9/98/cb5ca20618d205a09d5bec7591fbc4130369c7e6308d9a676a28ff3ab22c/limits-5.8.0-py3-none-any.whl", hash = "sha256:ae1b008a43eb43073c3c579398bd4eb4c795de60952532dc24720ab45e1ac6b8", size = 60954, upload-time = "2026-02-05T07:17:34.425Z" }, +] + [[package]] name = "mako" version = "1.3.11" @@ -1723,6 +1768,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/86/8a/69176a64335aed183529207ba8bc3d329c2999d852b4f3818027203f50e6/opencv_python_headless-4.11.0.86-cp37-abi3-win_amd64.whl", hash = "sha256:6c304df9caa7a6a5710b91709dd4786bf20a74d57672b3c31f7033cc638174ca", size = 39402386, upload-time = "2025-01-16T13:52:56.418Z" }, ] +[[package]] +name = "ordered-set" +version = "4.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4c/ca/bfac8bc689799bcca4157e0e0ced07e70ce125193fc2e166d2e685b7e2fe/ordered-set-4.1.0.tar.gz", hash = "sha256:694a8e44c87657c59292ede72891eb91d34131f6531463aab3009191c77364a8", size = 12826, upload-time = "2022-01-26T14:38:56.6Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/55/af02708f230eb77084a299d7b08175cff006dea4f2721074b92cdb0296c0/ordered_set-4.1.0-py3-none-any.whl", hash = "sha256:046e1132c71fcf3330438a539928932caf51ddbc582496833e23de611de14562", size = 7634, upload-time = "2022-01-26T14:38:48.677Z" }, +] + [[package]] name = "packaging" version = "26.0" @@ -3400,6 +3454,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/68/5a/199c59e0a824a3db2b89c5d2dade7ab5f9624dbf6448dc291b46d5ec94d3/wcwidth-0.6.0-py3-none-any.whl", hash = "sha256:1a3a1e510b553315f8e146c54764f4fb6264ffad731b3d78088cdb1478ffbdad", size = 94189, upload-time = "2026-02-06T19:19:39.646Z" }, ] +[[package]] +name = "websocket-client" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2c/41/aa4bf9664e4cda14c3b39865b12251e8e7d239f4cd0e3cc1b6c2ccde25c1/websocket_client-1.9.0.tar.gz", hash = "sha256:9e813624b6eb619999a97dc7958469217c3176312b3a16a4bd1bc7e08a46ec98", size = 70576, upload-time = "2025-10-07T21:16:36.495Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/34/db/b10e48aa8fff7407e67470363eac595018441cf32d5e1001567a7aeba5d2/websocket_client-1.9.0-py3-none-any.whl", hash = "sha256:af248a825037ef591efbf6ed20cc5faa03d3b47b9e5a2230a529eeee1c1fc3ef", size = 82616, upload-time = "2025-10-07T21:16:34.951Z" }, +] + [[package]] name = "websockets" version = "16.0" @@ -3480,6 +3543,92 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/93/8c/2e650f2afeb7ee576912636c23ddb621c91ac6a98e66dc8d29c3c69446e1/werkzeug-3.1.8-py3-none-any.whl", hash = "sha256:63a77fb8892bf28ebc3178683445222aa500e48ebad5ec77b0ad80f8726b1f50", size = 226459, upload-time = "2026-04-02T18:49:12.72Z" }, ] +[[package]] +name = "wrapt" +version = "2.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e2/f0/5e969d268d59e6035f2f1960da9e82fe6db24a7b8abe8e36a78c27cb3e2b/wrapt-2.2.0.tar.gz", hash = "sha256:b70a0b75b0a5a58d04aad06b3f167d49e729381d3417413656220c0cd7617847", size = 125173, upload-time = "2026-05-21T04:51:39.218Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/03/c6/17263421accbbc27bc4c8535eb9215a18a914d15eab4829a59e93f5ad29d/wrapt-2.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2b3946f0ff079623dc4f117363040433be390bfebce3719de50dfecbf31efdf0", size = 80088, upload-time = "2026-05-21T04:49:10.381Z" }, + { url = "https://files.pythonhosted.org/packages/40/0d/81230469d6a7c6878e0763b7d84ebab6da3625ce62e8fd83086c982b8726/wrapt-2.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a50822bbbefb90b132a780c17356062a2452cd5525bfa4b5b596fd6474cceaa6", size = 81177, upload-time = "2026-05-21T04:49:12.589Z" }, + { url = "https://files.pythonhosted.org/packages/d7/5a/a09c8346f270ab1328ba9e6594d73d86450de22bc4d29a23167ff82d7ec1/wrapt-2.2.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:29c0b2c075f8854b3345be584ab3d84f8968c45605d1914be1c94939cef5d702", size = 152069, upload-time = "2026-05-21T04:49:14.346Z" }, + { url = "https://files.pythonhosted.org/packages/40/5e/79b6d6295733b9fa1bee096120a556366951e3c0140234310080ede40e42/wrapt-2.2.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2f0d4a79d9af893d80caa5b709e024dd2d387f3f047008286036143f118d7010", size = 154319, upload-time = "2026-05-21T04:49:16.097Z" }, + { url = "https://files.pythonhosted.org/packages/1c/4d/a72b95e9389a4f350150d9a3ce9b263bad16f476551004a12de167ae7d0b/wrapt-2.2.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:10e8f78948d13369b770fc17bf72272aac98b4b92d49a38f479abf718f6b615b", size = 148874, upload-time = "2026-05-21T04:49:17.751Z" }, + { url = "https://files.pythonhosted.org/packages/0a/56/ffec9a08beb6fcfc30b259c6b8b36741675c58de69f1c035746f06fa4a07/wrapt-2.2.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a4482d1d4108052827b354850bd6e3d1ed56262cbe4b0e8051876c298fb99280", size = 153250, upload-time = "2026-05-21T04:49:19.413Z" }, + { url = "https://files.pythonhosted.org/packages/3d/c5/7ab2e23d594f28b2fc00bd19e82163bce2f77e2bc916e9dc247e0f886a41/wrapt-2.2.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:43c36019a690b2cb089665eab01a50c92d814553c6e57ff03d2c68e63ce8f00b", size = 147902, upload-time = "2026-05-21T04:49:20.749Z" }, + { url = "https://files.pythonhosted.org/packages/74/61/565965b9613dccf20286880e314cc41b20a85b2f4a7fe275786bb08b330e/wrapt-2.2.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cb9336f2dc99de00c9e58487cae5541ee4d79e859377b6312d98973d4661c584", size = 151334, upload-time = "2026-05-21T04:49:22.695Z" }, + { url = "https://files.pythonhosted.org/packages/32/0e/1890765d97cc3016ba444f8158856a35f8944785660eb88ff73b2d1e2b9b/wrapt-2.2.0-cp310-cp310-win32.whl", hash = "sha256:63a09b40bba3b2482983e2aeba6e45e20e1f567821ac89c8922229ecc1de7f65", size = 77405, upload-time = "2026-05-21T04:49:24.43Z" }, + { url = "https://files.pythonhosted.org/packages/02/02/a943f4d0f9084a354a722468ff2899e9177449f03f4bff8ef234792f27ad/wrapt-2.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:2ff803b3607cd76cb9b853b03d15279c7ffc8ba69e69f76304cd23d2722f2b65", size = 80353, upload-time = "2026-05-21T04:49:25.87Z" }, + { url = "https://files.pythonhosted.org/packages/ea/c2/2c7838cf368c04aebaef93f756f5b76e0eb12bb710c2926111dc96e5aaf9/wrapt-2.2.0-cp310-cp310-win_arm64.whl", hash = "sha256:af17d3ce1e2cc5d22ae8fe8921d7801c980ea3f5d6da4ecbd0f85c4f9e030181", size = 79121, upload-time = "2026-05-21T04:49:27.778Z" }, + { url = "https://files.pythonhosted.org/packages/ba/2e/a3eb4a1ef48fc743c4107e82d5b1144287ef8353b0f6844fee1add28d663/wrapt-2.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b93e1ccddbdf59cec4f7683dc84bc56eb61628eb01b22bdefc15f04cd09f8fae", size = 80324, upload-time = "2026-05-21T04:49:29.402Z" }, + { url = "https://files.pythonhosted.org/packages/0e/23/03248de44165f9c06dc23da981f3d58889ee2600004289c7afd12ef316b1/wrapt-2.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:97fbe7a0df35afe37e7e2f053dee6300a3eed00055cfd907fa51161e22c40236", size = 81201, upload-time = "2026-05-21T04:49:30.691Z" }, + { url = "https://files.pythonhosted.org/packages/39/99/ed8c0f9f0d3c9631259bf5c5d776ec7a70d6d888ce060ad4758f00a29683/wrapt-2.2.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d8f6cf451ec4aab0cdbad128d9be1219e95ceaa9940566d71570b2d820ee50b3", size = 158770, upload-time = "2026-05-21T04:49:32.299Z" }, + { url = "https://files.pythonhosted.org/packages/25/fc/6eed4204b30562f113e40151b94ec1ee565c040d90623a4223742cf5aa68/wrapt-2.2.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3f1dc1d1a2f0b081d8c1eef2203e61717b537a1bcb0d8e4d1405aeb15aa85c34", size = 160322, upload-time = "2026-05-21T04:49:33.959Z" }, + { url = "https://files.pythonhosted.org/packages/3a/3d/cb9d33c140cce69e025d946deac44c636ce16a079cd4410722b552aecb5e/wrapt-2.2.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:952ec99e71d584a0e451795dbd468909c8794727ecddd9ebb4fe9803e2803f1e", size = 153088, upload-time = "2026-05-21T04:49:35.715Z" }, + { url = "https://files.pythonhosted.org/packages/6b/fd/e452de05a75c008acef9055dd9a58fc6a4d08a5e42747394a91030f83169/wrapt-2.2.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:33ff34dc349320dc16ebe0cdf70dddf5ae9328f4a448823a00f37976d0cc2234", size = 159258, upload-time = "2026-05-21T04:49:38.249Z" }, + { url = "https://files.pythonhosted.org/packages/f1/03/c06ee1605a5b11da535b64e26c9f2330de7a8e3a2253afc533f37a5a682f/wrapt-2.2.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:d23ea5a8e4ae99640d027d2fd05c9d03f8d24d561fc26c0462e96affa31bf408", size = 152155, upload-time = "2026-05-21T04:49:39.69Z" }, + { url = "https://files.pythonhosted.org/packages/47/f8/d7cb1d184afe5a1db15515f86758fd08fa795a650f2af18ff221758921d7/wrapt-2.2.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9c95f72d212e1f178f9619b77fd7ee3533e82ded6a5ad119dd88134e185ee3b0", size = 157920, upload-time = "2026-05-21T04:49:41.225Z" }, + { url = "https://files.pythonhosted.org/packages/92/80/5bbdade010313edbb14afbdd916a054c74c99c2f04b0f8358086c728815a/wrapt-2.2.0-cp311-cp311-win32.whl", hash = "sha256:db93eebcf951f9ee41d75dc0423378fa918fc6706db59bc20c02f6563b6b210d", size = 77572, upload-time = "2026-05-21T04:49:42.913Z" }, + { url = "https://files.pythonhosted.org/packages/5f/32/9df5dd381c2d4d9f14d8d442de4efd8ef8fda3df8b25a384e7060a6d91a8/wrapt-2.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:22c7ee3a3737d9656ddf2c9cc1f1548ec963d966251e899561da142697d33a9d", size = 80624, upload-time = "2026-05-21T04:49:44.411Z" }, + { url = "https://files.pythonhosted.org/packages/0a/5c/3c441a01c9e1f072f0a9c062a3aa709b3fe488af649ecb0b74206e5a9754/wrapt-2.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:7e291fa9129d9998ed5035390d4bb9cf429c489f40e5ddaa06a1e83ed52048a7", size = 79003, upload-time = "2026-05-21T04:49:45.687Z" }, + { url = "https://files.pythonhosted.org/packages/83/ac/0d40f7f625b78d698dd8fcaf2df31585d2185dd0c261b82f7cc334c53168/wrapt-2.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8a76b27fe0d600f8a34313e1a528309aa807a16aa3a72000619bc56339020125", size = 80992, upload-time = "2026-05-21T04:49:47.024Z" }, + { url = "https://files.pythonhosted.org/packages/a0/56/bec7ac3b1c40bee400aecf0db3abee9d3461fd8f02eb42fb02693092b3d9/wrapt-2.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:778aa2f59615973f2637d9025a708b69196c4814f38d905647fa1a56d7ff6b79", size = 81648, upload-time = "2026-05-21T04:49:48.382Z" }, + { url = "https://files.pythonhosted.org/packages/a6/a9/6ecf97645bde3fc5faa980516f7007ece0b38d3219e5add54042d3ae8b4e/wrapt-2.2.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5b7f10aa09d1f5abfe3ccd022dec566a5010465b98b3755cc0705a762547101f", size = 168683, upload-time = "2026-05-21T04:49:49.703Z" }, + { url = "https://files.pythonhosted.org/packages/10/69/de03c995ade9b215f2c019be6442fc206b05ddcbec9d2f81bf94157aef47/wrapt-2.2.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d98bf0078736df226e36875aa58a78f9d3b0888bcf585144fb30edbbf7145238", size = 170982, upload-time = "2026-05-21T04:49:51.167Z" }, + { url = "https://files.pythonhosted.org/packages/19/f8/6255eb9827dbd137569de68554b1e9535c3ac79cdbc377af3da415891807/wrapt-2.2.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b62f40eb24ccf05246d203461c8920889fd38dce76978df16fe28e6f0128447d", size = 160002, upload-time = "2026-05-21T04:49:53.598Z" }, + { url = "https://files.pythonhosted.org/packages/e2/dd/962a9281d9c35e21c5a662c7d05c2af0108a3c833d2d6ab2eb546e520f7e/wrapt-2.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a8ce59cad2ee5a4d58ee647c4ed4d9adc4282ffdc31e98cba7f831536776a0f9", size = 168827, upload-time = "2026-05-21T04:49:55.082Z" }, + { url = "https://files.pythonhosted.org/packages/7d/ef/6a10e1200b2238be6da767d1814ab298f20e533a6c210f9ae6423ee3139c/wrapt-2.2.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:bb7c060c3faa78fe066b6b1c65de285d8d61fb6e01ee8195625b9636c3cd9775", size = 158164, upload-time = "2026-05-21T04:49:56.587Z" }, + { url = "https://files.pythonhosted.org/packages/4a/b2/4f5f4c722aa730eb2c0723ee8f32d0d7315d07173cdac0d08b7b92bbab39/wrapt-2.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4297b7338cfa48b5cfefc7416d2ae52b0aad89e9b24da479ec010717b987c07f", size = 167111, upload-time = "2026-05-21T04:49:57.996Z" }, + { url = "https://files.pythonhosted.org/packages/4e/b7/93fabbf2b505b610d019ec537c9dfa785a96920dcfc2ff8f57727aa54625/wrapt-2.2.0-cp312-cp312-win32.whl", hash = "sha256:9b58e2cdbcfe2278a031a12a7d73836d66bc1e9e65f97c63ea0a022f2f9f351b", size = 77867, upload-time = "2026-05-21T04:49:59.339Z" }, + { url = "https://files.pythonhosted.org/packages/70/51/1564bcd9863dbf2cca3a687f53a6eeaaa08850e331948f1c4c7818401e88/wrapt-2.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:199abadf7dcceab4bdc5bfe356275a56b1cb429296e283da2fe90c20b09f8d07", size = 80827, upload-time = "2026-05-21T04:50:01.212Z" }, + { url = "https://files.pythonhosted.org/packages/67/04/354d2fd146936dccf55aced66a606f6e1665435e3119765acb00a8753eb3/wrapt-2.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:8d40f1fb34d600b3eaf812941d6bcf313075728868cad1dafb7021e6a4e77983", size = 79094, upload-time = "2026-05-21T04:50:02.869Z" }, + { url = "https://files.pythonhosted.org/packages/c4/bc/00d23a39b5f002dfa20f7441721bb44198e7c7b4a6b3f3d7b4ff88fe2dc6/wrapt-2.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:49c7ad697d6b13f322a1c3bb22a1c66827d5c0d303a4479e327210ee4d4ad179", size = 80816, upload-time = "2026-05-21T04:50:04.563Z" }, + { url = "https://files.pythonhosted.org/packages/14/ce/d0c5ecb47818be6d1717ea51eec1285f8d53777994fe44deaf9d7299f65c/wrapt-2.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:07dd562ebb774cad070eeedb93c7a29647979e30f0cfd1f5c9b9f803f687b6f4", size = 81346, upload-time = "2026-05-21T04:50:05.882Z" }, + { url = "https://files.pythonhosted.org/packages/3f/19/a68afc8f7b085bc34fa6e17a120a10b2a9e27579369c79fb40f31ba95d69/wrapt-2.2.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5b865e611c186d15366964e3d9500af504920ce7b92a211d61a83d2d3c42a508", size = 166769, upload-time = "2026-05-21T04:50:07.604Z" }, + { url = "https://files.pythonhosted.org/packages/f8/67/b3dadb67dd612223615438ce080be6bd1fee6de12ee16b2ff9725b3169b1/wrapt-2.2.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:12331011cbf76b782d0beec7c7ed880f51454c127ab12012cfaecf56de01a80c", size = 166825, upload-time = "2026-05-21T04:50:09.129Z" }, + { url = "https://files.pythonhosted.org/packages/ab/b3/9cb0277fb0f5c853aa6a91f384784e73db4c3db8ff0f405bc3f71d93daae/wrapt-2.2.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8ae3f4b50a3befa56da0f09d2b71a192454ce48e8887823dbc9228cdbb610f3", size = 157882, upload-time = "2026-05-21T04:50:10.954Z" }, + { url = "https://files.pythonhosted.org/packages/83/f6/e4295b9dadfd73d1db30fced3cdf1d083787d77857257998c5b9dda8b3d9/wrapt-2.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:370b2c36e8fee503c275e39b4588d74412cd0a7792f7f3a7b54c44c4d33d4884", size = 165791, upload-time = "2026-05-21T04:50:12.698Z" }, + { url = "https://files.pythonhosted.org/packages/ac/33/e66764a3aefb45a3a60ac76ea6878417a13f98e67f046f8e78b0a9ca6063/wrapt-2.2.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:9040b15216e07ed68762e44ff231a460036e4bf3543f83988f669e7078847b2c", size = 156574, upload-time = "2026-05-21T04:50:14.28Z" }, + { url = "https://files.pythonhosted.org/packages/cf/90/e3355e82cc765a411283ff4335ab41034d4eab9f5226b3e5840bebcaaf96/wrapt-2.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8062689c0e6faf0c2532f566a492fb48ba60923c2cd6effda7cac9639dbdc1f3", size = 165943, upload-time = "2026-05-21T04:50:16.373Z" }, + { url = "https://files.pythonhosted.org/packages/e4/86/eda5a79813cd9ee86cd7275b9eac5338166886a5ffc9dcf881a3068d03a3/wrapt-2.2.0-cp313-cp313-win32.whl", hash = "sha256:a3848854af260eb4cc33602c685524fff7c8816f033325f750c7fc75c6deccf9", size = 77824, upload-time = "2026-05-21T04:50:18.241Z" }, + { url = "https://files.pythonhosted.org/packages/ba/de/1eadc4caa3797a33d231572435eed9116d24f56dc6c909c43b59092fbb37/wrapt-2.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:76b8111f8f5b8553c066caa26193921dea4185efecf1f9b38473054205137800", size = 80737, upload-time = "2026-05-21T04:50:20.038Z" }, + { url = "https://files.pythonhosted.org/packages/11/34/aeda6d757664a569a19d3e88e89f1c52134bbaa59b053bb316c69c71c459/wrapt-2.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:195db5b92deba6feb818732694ad478abb8a529d97a113cc256e5e49ee2dd80d", size = 79094, upload-time = "2026-05-21T04:50:21.784Z" }, + { url = "https://files.pythonhosted.org/packages/92/c7/3bfdcddd4c0281d104305e473953f1402bcae1898089656b6a9567a1e5cd/wrapt-2.2.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:cf93c441b11c1f3ae2ccf1e8d876939b301b3234ec19f311ab0e7543a9d4427e", size = 82751, upload-time = "2026-05-21T04:50:23.694Z" }, + { url = "https://files.pythonhosted.org/packages/dc/96/37cc2bf299cfbf21f6bb7dfd0ba590e2d29f9e1fe6aa334a97395f4406dc/wrapt-2.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b208a5dd6f9da3d4b17aa2e4f8ca9c5dc6b9a2ed571fdef9ed465102487b445c", size = 83315, upload-time = "2026-05-21T04:50:25.085Z" }, + { url = "https://files.pythonhosted.org/packages/18/b0/bd4b4c51243a38009cc1c96f0503a535a7d8044636626bc7c545e766e73d/wrapt-2.2.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5248171d3cd33f12c144e7aa1222983cb6ab42651e985ce51fec400a876afbfd", size = 203752, upload-time = "2026-05-21T04:50:26.862Z" }, + { url = "https://files.pythonhosted.org/packages/8f/b6/aee7c4fd7f19026d464ca7fd8a83efa5f3168ed33897ca0d1ec83bd15de4/wrapt-2.2.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1f663528d6ea1804d279462671b2bf98a4c0d8a4a8dd319bb3ee0629b743387f", size = 209665, upload-time = "2026-05-21T04:50:28.45Z" }, + { url = "https://files.pythonhosted.org/packages/d9/91/be1181e580cd20a2584260285aa25fa9eb64a27a5921a431008910ea5d70/wrapt-2.2.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fb240700f3b597c1d40d0932bfed2f4130fec2f02b8c2cb0bcdae45d321cb691", size = 194678, upload-time = "2026-05-21T04:50:30.307Z" }, + { url = "https://files.pythonhosted.org/packages/63/f6/cae7b5f26bf1385f562b7904db23b686e66a4f4f4b3496675531b1d0d968/wrapt-2.2.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:1bf3ea62734b24c0241442d8b7684ef53a8de6cad0c2eba1e99fd2297b4a92e4", size = 205364, upload-time = "2026-05-21T04:50:31.899Z" }, + { url = "https://files.pythonhosted.org/packages/81/d4/647312c3fcef95e6c65fd4c11efe4575cd021ac0074f3000cb066fc67c9e/wrapt-2.2.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:ec257eedd8c3988cf76e351e949e3a56a61d90f4bb4e060de2ebfa6603df2a42", size = 192139, upload-time = "2026-05-21T04:50:33.588Z" }, + { url = "https://files.pythonhosted.org/packages/db/c4/b40d8d176979b9397a4cfcc9eaafdd20697fc6e62293d70b1951d422b988/wrapt-2.2.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f58e1aa46c204171a2faa49b1ef2953edebb3913d270bb3bae7e970f254c9293", size = 199221, upload-time = "2026-05-21T04:50:36.591Z" }, + { url = "https://files.pythonhosted.org/packages/f2/cf/71f00a6a0e9f5244c0bcc4e445d1087467d1c80e788637929bea0a1ea637/wrapt-2.2.0-cp313-cp313t-win32.whl", hash = "sha256:615be1d2b21450748e759bed7bf9ba8bc28307e91cb96b6e968f54f39e938ee5", size = 79438, upload-time = "2026-05-21T04:50:38.531Z" }, + { url = "https://files.pythonhosted.org/packages/b7/01/8219ee5e1491fdd880564af04a809eb8866481faff5cce6105174202667d/wrapt-2.2.0-cp313-cp313t-win_amd64.whl", hash = "sha256:0680304db389599691bac06a2f9fb3f0ed06af59f132d35801a38cf6c321ab59", size = 83024, upload-time = "2026-05-21T04:50:39.892Z" }, + { url = "https://files.pythonhosted.org/packages/56/c5/ec61c19ea596299b0f0fca9f5ff82418a5152d933772bac90c61a4b06c30/wrapt-2.2.0-cp313-cp313t-win_arm64.whl", hash = "sha256:60bef9dc4348a76e9c2981ec4b06b779bac02556af4479030e6f62b18545b3cc", size = 80282, upload-time = "2026-05-21T04:50:41.318Z" }, + { url = "https://files.pythonhosted.org/packages/11/b7/dd4278d51621fd5054f840744be1c830b37e9d7b9b22b5590eb69c5039a3/wrapt-2.2.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:5c17982ccfece323bb297a195c9602ef407819199d8dbf99b8041770513fd68f", size = 80861, upload-time = "2026-05-21T04:50:42.755Z" }, + { url = "https://files.pythonhosted.org/packages/65/ec/06efd37278eaee793521aee41091cb29fe20603dc5bd2f5cdc4e73fe9ce8/wrapt-2.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d2aab40474b6adae53d14d1f6a7785f4346a93c072adf1e69ca11a1b6afc789e", size = 81436, upload-time = "2026-05-21T04:50:44.212Z" }, + { url = "https://files.pythonhosted.org/packages/21/95/46922f9415f109506f8bdfd903138dbde8a507a70ca02904b8dcffaac171/wrapt-2.2.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:db48e2623a8aca63dfcfa7e574a5f3a9f760be1c464ee23f6387f70cc9112aa2", size = 166655, upload-time = "2026-05-21T04:50:45.951Z" }, + { url = "https://files.pythonhosted.org/packages/95/57/601af72054c2166e11781a30b0fd6f7d500e9186351e73f8ff5d923afcee/wrapt-2.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f990f1b5c8ee4ff980bdef3f73f50728fd911b9ab8de8c43144e8019dcd845ff", size = 166257, upload-time = "2026-05-21T04:50:47.644Z" }, + { url = "https://files.pythonhosted.org/packages/8d/a0/b2e96a62cd572f186eb94be906d4854dd301b20a3b30b648c8ddab11a2fb/wrapt-2.2.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c990d58100f9ebb8e7a20bd2e7bd3c60838be38c5bbccdd35041bc9f36dc0cea", size = 157694, upload-time = "2026-05-21T04:50:49.215Z" }, + { url = "https://files.pythonhosted.org/packages/72/f8/77fa31bda9344ca76d6a8eb6f5bd274aea1a7e24d6279b21fc2349d41fbe/wrapt-2.2.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:686f1798727bf4a708df015ca782b20abe99b3664e1ee9786b7712b0e2310586", size = 166036, upload-time = "2026-05-21T04:50:50.857Z" }, + { url = "https://files.pythonhosted.org/packages/3e/73/118d00ad41f270128aa94a80b8150c5b720c18e06dc1a2291795c33839ec/wrapt-2.2.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:5b9733ef187cf05e774484ed2f703992a44429050f1cfea2e94dac543da78292", size = 156437, upload-time = "2026-05-21T04:50:52.415Z" }, + { url = "https://files.pythonhosted.org/packages/24/43/16017c26a1eeccbbf8f79f5172095bf9b0cb7183ac9bfc4a3c2c9fc37675/wrapt-2.2.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:231e2728ba04536821d2327ad2b3cb2c20cc79197fe5c30ddf71b12d95febe10", size = 165492, upload-time = "2026-05-21T04:50:54.16Z" }, + { url = "https://files.pythonhosted.org/packages/b3/bd/d5d59f0a074e192f1cdafdeafca3d1aca25c3dd9172e0418fd04a912b864/wrapt-2.2.0-cp314-cp314-win32.whl", hash = "sha256:319720847afa6c58c32f84f9743bdcf34448ae56908c00f409764c627ff2c1fe", size = 78343, upload-time = "2026-05-21T04:50:56.028Z" }, + { url = "https://files.pythonhosted.org/packages/cc/f5/c7fbbcbd8285f1999666115a793890a38e8b88744b8c3630059a0efa88bb/wrapt-2.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:628fbd908649611c8b9293e2e050231f1e230be152e7d38140e3b818ec6aade0", size = 81144, upload-time = "2026-05-21T04:50:57.538Z" }, + { url = "https://files.pythonhosted.org/packages/93/aa/152902a4b85cb55daad6e383a91ca5e23fd8d56132a4aa44987b7154f5e3/wrapt-2.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:b4ce4240a3f095e77cfcc5aed6001bd63af13ea53c35ef496af1a5a972e7eaa9", size = 79573, upload-time = "2026-05-21T04:50:59.307Z" }, + { url = "https://files.pythonhosted.org/packages/af/fe/a25c3eee98417de1caf541c1b234bbc3a8b0ce4817b0c8934ca57bfe3e89/wrapt-2.2.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:f0318a47d23c9407f4f94c06824662499e889ab8c192c1162e4f542a118fd700", size = 82844, upload-time = "2026-05-21T04:51:00.767Z" }, + { url = "https://files.pythonhosted.org/packages/71/bf/31060eb2f475b7798926f46c1779ec93329a48730cbeb8f9c0855162f97b/wrapt-2.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8a094508b7cd6e583378f3cf50f125814961660225bad88f4ecaa691e30b09e1", size = 83321, upload-time = "2026-05-21T04:51:02.253Z" }, + { url = "https://files.pythonhosted.org/packages/54/b9/62702f8bdaf509e444ec38bf142122db8c5ebbdfe6e2ca8e1dd7d43fb574/wrapt-2.2.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:115ff1501c11ac0e267c4afd6f6b3dd24b48afcc77b029e6062f71b12bce1d79", size = 203740, upload-time = "2026-05-21T04:51:03.8Z" }, + { url = "https://files.pythonhosted.org/packages/fe/c6/c9ea3537ea759edcc856a32fc2d16abee41d7474f853bf00089058c0a33e/wrapt-2.2.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:45d4156fd35d0bdab58eac4a6854fbd053a59544fc57eb66e977b3c13c087a1c", size = 209671, upload-time = "2026-05-21T04:51:05.56Z" }, + { url = "https://files.pythonhosted.org/packages/79/c8/5925232cf614c23969b2267d954976e288993ef9e94a74eba4f26ad41232/wrapt-2.2.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4b0aa81f4a3d0203ae8450eae5e794540afbf00a97dd0b81accbe5b4a5362cbb", size = 194717, upload-time = "2026-05-21T04:51:07.227Z" }, + { url = "https://files.pythonhosted.org/packages/10/95/b824ac1e5900f39f80d0d4e97cf59389b078d0fed3551f471911f9b46281/wrapt-2.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:74b7949da2ffcd79869ac1e90946c14ce61a714269403a879ea9ed85a993c81f", size = 205335, upload-time = "2026-05-21T04:51:08.868Z" }, + { url = "https://files.pythonhosted.org/packages/d4/58/623708a153bb1a519260bf61086c5f381196a7d505ac729f7979b0d1a957/wrapt-2.2.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:c7af243871699358ebf34a770205bf2b61ccb17a0b003e8726d2028cc36ce364", size = 192170, upload-time = "2026-05-21T04:51:10.52Z" }, + { url = "https://files.pythonhosted.org/packages/1c/4e/d771a75386676fe08086affe57b0f7cffafe528642ae5ebf95200811248e/wrapt-2.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:eb9d0c3f416e2c7c37498d1716fe323379da8b4e860da3d3818a6ec8fff7b7e5", size = 199200, upload-time = "2026-05-21T04:51:12.269Z" }, + { url = "https://files.pythonhosted.org/packages/2a/42/afd8991950e38f32c73008a3f2cd834cab32e338cc1997b7a39272a22cbc/wrapt-2.2.0-cp314-cp314t-win32.whl", hash = "sha256:4d5b485a6f617825fa7449f5025ebcdad9355acb328cb6d198ba225762219bc0", size = 80206, upload-time = "2026-05-21T04:51:13.837Z" }, + { url = "https://files.pythonhosted.org/packages/b8/34/d10901fb7686ec642e22d75d260f07ba6e05d28d5c83cb1efdc8d5c03e07/wrapt-2.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:cccce5c70a209eb385c82d063f332ed97fc02d1cf7bffb95b2e6995b5a9b8388", size = 83830, upload-time = "2026-05-21T04:51:15.341Z" }, + { url = "https://files.pythonhosted.org/packages/f3/11/d41fd5f17432703783f996fddc475d40baf20fe76f2c6dc217c2dd219b4c/wrapt-2.2.0-cp314-cp314t-win_arm64.whl", hash = "sha256:9ad894d5dc5960ebd546a87a78160a8c645b99899e7e45a538436919bc9be5a6", size = 80711, upload-time = "2026-05-21T04:51:16.859Z" }, + { url = "https://files.pythonhosted.org/packages/33/19/713f33fcd8f7b0aa87c9d068b590dc1e86c51d5e329bf83dd91ee47fe872/wrapt-2.2.0-py3-none-any.whl", hash = "sha256:03b77d3ecab6c38e5da7a5709cee6899083d08fc1bcd648b4fa78b346fc66282", size = 60994, upload-time = "2026-05-21T04:51:37.606Z" }, +] + [[package]] name = "wsproto" version = "1.3.2" From 6d512602cea47e258d002c0cd005629fcc15bd86 Mon Sep 17 00:00:00 2001 From: Daniel Valladares Date: Fri, 8 May 2026 22:35:35 -0300 Subject: [PATCH 2/4] fix(chat): manter WS vivo em idle + auto-reconnect no AgentChat MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit terminal_proxy.py fechava a ponte WS quando client_ws.receive(timeout=30) retornava None — mas None é timeout, não desconexão (disconexão real levanta ConnectionClosed). Em abas em background o ping de 25s do AgentChat é throttled pelo navegador para <1×/min, batendo o timeout e derrubando o WS com o peer ainda vivo. Como o frontend não tinha auto-reconnect, wsRef.current ficava apontando para um socket fechado e sendMessage() retornava em silêncio — sintoma do "clico enviar e nada acontece" relatado pelos usuários. Backend: continue em vez de break no receive timeout. Disconexões reais seguem caindo no except → finally normalmente. Frontend: useEffect refatorado para função connect() reusável, com backoff 1s→30s acionado no onclose. Ping interval virou per-WS (localPing) para que o onclose de uma WS antiga não limpe o ping da nova durante reconnects encadeados. wsRef agora é zerado no onclose para evitar sends em socket morto. Co-Authored-By: Claude Opus 4.7 (1M context) --- dashboard/backend/routes/terminal_proxy.py | 12 ++- .../frontend/src/components/AgentChat.tsx | 74 ++++++++++++++----- 2 files changed, 67 insertions(+), 19 deletions(-) diff --git a/dashboard/backend/routes/terminal_proxy.py b/dashboard/backend/routes/terminal_proxy.py index 17b5a6af..2d631e80 100644 --- a/dashboard/backend/routes/terminal_proxy.py +++ b/dashboard/backend/routes/terminal_proxy.py @@ -192,7 +192,17 @@ def _pump_upstream_to_client(): while not stop.is_set(): msg = client_ws.receive(timeout=30) if msg is None: - break + # simple-websocket returns None on receive() timeout, not + # on disconnect (disconnects raise ConnectionClosed). An + # idle chat session — e.g. user with the tab in the + # background where browsers throttle the 25s ping + # setInterval below 1/min — would otherwise drop the WS + # here, leaving the frontend's wsRef pointing at a CLOSED + # socket. Subsequent sendMessage() then silently no-ops + # because readyState !== OPEN. Continuing the loop + # preserves the connection across idle periods; real + # disconnects still surface via the exception handler. + continue upstream.send(msg) except Exception: pass diff --git a/dashboard/frontend/src/components/AgentChat.tsx b/dashboard/frontend/src/components/AgentChat.tsx index b60ff664..b5462915 100644 --- a/dashboard/frontend/src/components/AgentChat.tsx +++ b/dashboard/frontend/src/components/AgentChat.tsx @@ -104,6 +104,8 @@ export default function AgentChat({ agent, sessionId, accentColor = '#00FFA7', e const inputRef = useRef(null) const fileInputRef = useRef(null) const pingRef = useRef | null>(null) + const reconnectTimerRef = useRef | null>(null) + const reconnectDelayRef = useRef(1000) const dragCounterRef = useRef(0) const subagentToolRef = useRef<{ toolName: string; toolUseId: string; input: string; parentToolUseId: string } | null>(null) @@ -134,12 +136,31 @@ export default function AgentChat({ agent, sessionId, accentColor = '#00FFA7', e useEffect(() => { if (!sessionId) return - setStatus('connecting') - setErrorMsg(null) let cancelled = false - let ws: WebSocket | null = null - ;(async () => { + // Schedule a reconnect with exponential backoff, capped at 30s. Reset to + // 1s on every successful onopen — see the matching reset below. Without + // this, a transient WS drop (proxy idle-timeout, network blip, laptop + // sleep/wake) leaves the chat permanently "open" in the UI but the + // socket closed: sendMessage() then silently no-ops because readyState + // !== OPEN, which is exactly the "I click send and nothing happens" + // failure mode users hit. + const scheduleReconnect = () => { + if (cancelled) return + if (reconnectTimerRef.current) return // already scheduled + const delay = Math.min(reconnectDelayRef.current, 30000) + reconnectDelayRef.current = Math.min(delay * 2, 30000) + reconnectTimerRef.current = setTimeout(() => { + reconnectTimerRef.current = null + if (!cancelled) connect() + }, delay) + } + + const connect = async () => { + if (cancelled) return + setStatus('connecting') + setErrorMsg(null) + // 1) HTTP preflight — fails fast on ECONNREFUSED so we can show a real error // instead of hanging in 'connecting' forever (same pattern as AgentTerminal). try { @@ -149,16 +170,19 @@ export default function AgentChat({ agent, sessionId, accentColor = '#00FFA7', e if (cancelled) return setStatus('error') setErrorMsg(`Could not reach terminal-server at ${TS_HTTP}. Is it running?`) + scheduleReconnect() return } if (cancelled) return - // 2) Open WS - ws = new WebSocket(`${TS_WS}/ws`) + // 2) Open WS — scope-local so each reconnect has its own instance and + // handlers don't race against the next one. + const ws = new WebSocket(`${TS_WS}/ws`) wsRef.current = ws ws.onopen = () => { - ws!.send(JSON.stringify({ type: 'join_session', sessionId })) + reconnectDelayRef.current = 1000 + ws.send(JSON.stringify({ type: 'join_session', sessionId })) setStatus('idle') } @@ -273,26 +297,40 @@ export default function AgentChat({ agent, sessionId, accentColor = '#00FFA7', e } ws.onerror = () => { - if (cancelled) return - setStatus('error') - setErrorMsg('WebSocket error') + // Don't surface "WebSocket error" as a sticky error: most onerror + // events here are paired with an immediate onclose that triggers a + // reconnect, and showing the error disables the input mid-send. Just + // let onclose handle the recovery. } + // Per-ws ping interval; captured locally so onclose of an old ws + // doesn't clear the ping interval belonging to a newer reconnect. + const localPing = setInterval(() => { + if (ws.readyState === WebSocket.OPEN) { + ws.send(JSON.stringify({ type: 'ping' })) + } + }, 25000) + pingRef.current = localPing + ws.onclose = () => { - if (pingRef.current) { clearInterval(pingRef.current); pingRef.current = null } + clearInterval(localPing) + if (pingRef.current === localPing) pingRef.current = null + if (cancelled) return + // The closing ws may not be the current one if a reconnect already + // raced ahead — only clear wsRef if it still points at us. + if (wsRef.current === ws) wsRef.current = null + scheduleReconnect() } + } - pingRef.current = setInterval(() => { - if (ws!.readyState === WebSocket.OPEN) { - ws!.send(JSON.stringify({ type: 'ping' })) - } - }, 25000) - })() + connect() return () => { cancelled = true if (pingRef.current) { clearInterval(pingRef.current); pingRef.current = null } - try { ws?.close() } catch {} + if (reconnectTimerRef.current) { clearTimeout(reconnectTimerRef.current); reconnectTimerRef.current = null } + reconnectDelayRef.current = 1000 + try { wsRef.current?.close() } catch {} wsRef.current = null } }, [sessionId]) From 9c6a9a49aa43fa5590bf78c03b6de2f8942e94ef Mon Sep 17 00:00:00 2001 From: Daniel Valladares Date: Sat, 9 May 2026 20:23:11 -0300 Subject: [PATCH 3/4] =?UTF-8?q?fix(chat):=20respeitar=20scroll=20manual=20?= =?UTF-8?q?do=20usu=C3=A1rio=20(n=C3=A3o=20pular=20pro=20final)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Antes: scrollToBottom() forçava scrollTop = scrollHeight em todo render/ delta, então o usuário não conseguia rolar pra cima pra ler histórico — a próxima mensagem (ou cada chunk de stream) jogava ele de volta no fundo. Agora: isAtBottomRef rastreia se o usuário está perto do fundo (<50px). Se ele rolou pra cima, scrollToBottom() vira no-op e aparece um botão flutuante "Ir para o final" que reativa o follow ao clicar. Mandar nova mensagem ou re-enviar (edit/rewind) força a flag de volta a true porque é sinal claro de "quero ver o resultado". History restore (session_joined, chat_history) também força true — abrindo a sessão você sempre cai no fundo. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../frontend/src/components/AgentChat.tsx | 65 ++++++++++++++++++- 1 file changed, 63 insertions(+), 2 deletions(-) diff --git a/dashboard/frontend/src/components/AgentChat.tsx b/dashboard/frontend/src/components/AgentChat.tsx index b5462915..e5a1293f 100644 --- a/dashboard/frontend/src/components/AgentChat.tsx +++ b/dashboard/frontend/src/components/AgentChat.tsx @@ -108,6 +108,12 @@ export default function AgentChat({ agent, sessionId, accentColor = '#00FFA7', e const reconnectDelayRef = useRef(1000) const dragCounterRef = useRef(0) const subagentToolRef = useRef<{ toolName: string; toolUseId: string; input: string; parentToolUseId: string } | null>(null) + // Auto-scroll só "segue" o stream se o usuário estiver perto do fundo. Se ele + // rolou para cima manualmente para ler histórico, NÃO joga ele de volta a + // cada nova mensagem/delta — UX comum em chat. Volta a seguir quando ele + // rolar de volta ao fundo (ou mandar nova mensagem, ver sendMessage). + const isAtBottomRef = useRef(true) + const [showJumpToBottom, setShowJumpToBottom] = useState(false) // Auto-dismiss global notifications when the user opens this session useEffect(() => { @@ -123,15 +129,41 @@ export default function AgentChat({ agent, sessionId, accentColor = '#00FFA7', e } }, [pendingApprovals.length, sessionId, onPendingCountChange]) - // Auto-scroll to bottom + // Auto-scroll to bottom — respeita scroll manual do usuário. + // Só rola se isAtBottomRef.current === true. Caso contrário (usuário rolou + // para cima), não força nada — assim ele consegue ler mensagens antigas + // sem ser teleportado de volta ao final a cada delta de stream. const scrollToBottom = useCallback(() => { + requestAnimationFrame(() => { + if (scrollRef.current && isAtBottomRef.current) { + scrollRef.current.scrollTop = scrollRef.current.scrollHeight + } + }) + }, []) + + // Scroll forçado, ignora flag — usar quando o usuário pediu explicitamente + // (clique no botão "ir para o final" ou ao mandar nova mensagem). + const forceScrollToBottom = useCallback(() => { requestAnimationFrame(() => { if (scrollRef.current) { scrollRef.current.scrollTop = scrollRef.current.scrollHeight + isAtBottomRef.current = true + setShowJumpToBottom(false) } }) }, []) + // Threshold de 50px: se o usuário está a menos de 50px do fundo, considera + // "no fundo" e mantém auto-scroll ativo. Acima disso, suspende. + const handleScroll = useCallback(() => { + const el = scrollRef.current + if (!el) return + const distanceFromBottom = el.scrollHeight - el.scrollTop - el.clientHeight + const atBottom = distanceFromBottom < 50 + isAtBottomRef.current = atBottom + setShowJumpToBottom(!atBottom) + }, []) + // Connect WebSocket useEffect(() => { if (!sessionId) return @@ -200,6 +232,8 @@ export default function AgentChat({ agent, sessionId, accentColor = '#00FFA7', e uuid: m.uuid, streaming: false, }))) + // Abriu a sessão agora — começa no fundo. + isAtBottomRef.current = true scrollToBottom() } // Restore ticket binding (Feature 1.3) @@ -210,6 +244,7 @@ export default function AgentChat({ agent, sessionId, accentColor = '#00FFA7', e // Fallback history restore if (msg.messages?.length > 0) { setMessages(msg.messages.map((m: any) => ({ ...m, streaming: false }))) + isAtBottomRef.current = true scrollToBottom() } break @@ -759,6 +794,9 @@ export default function AgentChat({ agent, sessionId, accentColor = '#00FFA7', e rewindFromUuid: uuid, })) + // Mesmo motivo do sendMessage: reenviar/editar = quer ver o resultado. + isAtBottomRef.current = true + setShowJumpToBottom(false) scrollToBottom() }, [editingText, editingUuid, scrollToBottom]) @@ -825,6 +863,10 @@ export default function AgentChat({ agent, sessionId, accentColor = '#00FFA7', e files: filesForServer.length > 0 ? filesForServer : undefined, })) + // Mandou nova mensagem = sinal claro de "quero ver minha mensagem no + // fundo". Reativa auto-scroll mesmo se ele estava lendo histórico. + isAtBottomRef.current = true + setShowJumpToBottom(false) scrollToBottom() if (inputRef.current) { inputRef.current.style.height = 'auto' @@ -1073,7 +1115,7 @@ export default function AgentChat({ agent, sessionId, accentColor = '#00FFA7', e )} {/* Messages area */} -
+
{messages.length === 0 && (
+ {/* Botão flutuante "ir para o final" — aparece quando o usuário rolou + para cima e o auto-scroll está suspenso. Clique reativa o follow. */} + {showJumpToBottom && ( + + )} + {/* Input area */}
From 81dcebede6dde1a292ed590e919bbbc5bd9ae92e Mon Sep 17 00:00:00 2001 From: Daniel Valladares Date: Thu, 21 May 2026 16:48:01 -0300 Subject: [PATCH 4/4] fix(chat): scroll race + heartbeat half-open WS + defensive blocks render MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 3 unrelated reliability fixes accumulated in AgentChat.tsx: 1. Scroll race: under heavy streaming (60+ deltas/sec) the scrollToBottom rAF could fire before the user's onScroll propagated, leaving isAtBottomRef stale and teleporting them back to bottom. Now we recompute distance-from-bottom inside the rAF callback and update the flag proactively if the user moved away. 2. Heartbeat timeout: track lastPongAt on every server pong. The ws lib can leave half-open sockets (TCP dead, no close frame), so onclose never fires and chat_event stops silently. The interval now also checks for stale pongs and forces a reconnect. 3. Defensive blocks render: msg.blocks could be undefined for assistant messages mid-stream or from legacy formats, crashing .map/.some/.filter and triggering the ErrorBoundary ("Unable to load dashboard section"). Fixed in 3 sites: getMessageText (line 771), blocks rendering (1278), streaming hasVisibleContent check (1292). Bug 3 was breaking the entire /agents/ page for every agent with chat history — reported today on clawdia-assistant but not specific. Co-Authored-By: Claude Opus 4.7 --- .../frontend/src/components/AgentChat.tsx | 62 +++++++++++++++---- 1 file changed, 49 insertions(+), 13 deletions(-) diff --git a/dashboard/frontend/src/components/AgentChat.tsx b/dashboard/frontend/src/components/AgentChat.tsx index e5a1293f..93fd3402 100644 --- a/dashboard/frontend/src/components/AgentChat.tsx +++ b/dashboard/frontend/src/components/AgentChat.tsx @@ -130,13 +130,31 @@ export default function AgentChat({ agent, sessionId, accentColor = '#00FFA7', e }, [pendingApprovals.length, sessionId, onPendingCountChange]) // Auto-scroll to bottom — respeita scroll manual do usuário. - // Só rola se isAtBottomRef.current === true. Caso contrário (usuário rolou - // para cima), não força nada — assim ele consegue ler mensagens antigas - // sem ser teleportado de volta ao final a cada delta de stream. + // + // Race fix: durante streaming rápido (60+ deltas/seg), o rAF de scrollToBottom + // pode rodar ANTES do onScroll do usuário propagar. Se confiarmos apenas no + // isAtBottomRef (atualizado por handleScroll), a flag estará stale → rolaríamos + // pra baixo mesmo o usuário já tendo rolado pra cima. Solução: recomputar + // distance from bottom no MOMENTO do scroll, dentro do rAF. Se o usuário já + // não está no fundo, atualiza a flag proativamente e não rola. const scrollToBottom = useCallback(() => { requestAnimationFrame(() => { - if (scrollRef.current && isAtBottomRef.current) { - scrollRef.current.scrollTop = scrollRef.current.scrollHeight + const el = scrollRef.current + if (!el) return + const distance = el.scrollHeight - el.scrollTop - el.clientHeight + if (distance < 50) { + el.scrollTop = el.scrollHeight + if (!isAtBottomRef.current) { + isAtBottomRef.current = true + setShowJumpToBottom(false) + } + } else { + // Usuário rolou pra cima entre o agendamento do rAF e este momento. + // Atualiza a flag pra evitar futuros auto-scrolls até ele descer. + if (isAtBottomRef.current) { + isAtBottomRef.current = false + setShowJumpToBottom(true) + } } }) }, []) @@ -327,6 +345,7 @@ export default function AgentChat({ agent, sessionId, accentColor = '#00FFA7', e break case 'pong': + lastPongAt = Date.now() break } } @@ -338,12 +357,23 @@ export default function AgentChat({ agent, sessionId, accentColor = '#00FFA7', e // let onclose handle the recovery. } - // Per-ws ping interval; captured locally so onclose of an old ws - // doesn't clear the ping interval belonging to a newer reconnect. + // Per-ws ping interval + heartbeat-timeout. Captured locally so onclose + // of an old ws doesn't clear the ping interval belonging to a newer + // reconnect. The ws library can leave a socket "half-open" — TCP died + // but no close frame ever arrives — so onclose never fires and the + // frontend silently stops receiving chat_event. Symptom: agent reply + // only shows up after a manual F5 (the reload re-runs session_joined, + // which restores chatHistory from the server). Fix: track last pong; + // if more than 60s elapsed since one was received, force-close the + // socket so onclose → scheduleReconnect kicks in. + let lastPongAt = Date.now() const localPing = setInterval(() => { - if (ws.readyState === WebSocket.OPEN) { - ws.send(JSON.stringify({ type: 'ping' })) + if (ws.readyState !== WebSocket.OPEN) return + if (Date.now() - lastPongAt > 60000) { + try { ws.close() } catch {} + return } + ws.send(JSON.stringify({ type: 'ping' })) }, 25000) pingRef.current = localPing @@ -738,7 +768,7 @@ export default function AgentChat({ agent, sessionId, accentColor = '#00FFA7', e // Extract plain text from a message for copying const getMessageText = (msg: ChatMessage): string => { if (msg.role === 'user' || msg.role === 'system') return msg.text - return msg.blocks + return (msg.blocks ?? []) .filter((b): b is { type: 'text'; text: string } => b.type === 'text') .map(b => b.text) .join('\n\n') @@ -979,7 +1009,13 @@ export default function AgentChat({ agent, sessionId, accentColor = '#00FFA7', e const isConnecting = externalLoading || status === 'connecting' const effectiveError = externalError || (status === 'error' ? errorMsg : null) - const inputDisabled = isConnecting || !!effectiveError + // Don't disable the textarea during transient reconnects — disabling blurs + // the cursor (native behavior of