agentscope-ai / agentscope-ai/QwenPaw
# [Bug]: Workspace file browser (SSE `/api/workspace/watch`) freezes the entire server when the workspace contains large repos — `watchfiles.awatch` RustNotify sync init blocks the event loop
- 主要言語
- Python
- スター
- 34.9k
- フォーク
- 3.1k
- 平均マージ
- 1日 15時間
- マージ済み PR(30日)
- 225
説明
## QwenPaw Version
2.2.1 (`qwenpaw --version`), running inside a Docker container
## Description
Opening the Console file browser (Workspace → Files) hangs the **entire** QwenPaw server: the WebUI stops responding, all channels (Feishu/QQ/OneBot) stop processing messages, and the process cannot even handle SIGTERM (it fails to shut down within Docker's 10 s grace window and is force-killed with SIGKILL, exit code 137).
Root cause: the file browser opens an SSE connection to `GET /api/workspace/watch`, which is served by `workspace_watch_events` in `src/qwenpaw/app/routers/workspace.py`. That handler builds the watcher **synchronously on the event-loop thread**:
```python
async def workspace_watch_events(request, watch_dir):
yield 'data: {"type": "connected"}\n\n'
watcher = awatch(watch_dir, rust_timeout=1000, yield_on_timeout=True) # <-- sync, on the loop
while True:
raw_changes = await watcher.__anext__() # first call runs RustNotify.__enter__
...
```
`watchfiles.awatch` (watchfiles 1.2.0, `main.py:257`) does:
```python
with RustNotify(...) as watcher: # __enter__ = recursive initial scan, NOT offloaded
...
raw_changes = await anyio.to_thread.run_sync(watcher.watch, ...) # only polling is offloaded
```
`RustNotify.__enter__` builds the initial file snapshot by recursively scanning the whole watched tree. In a workspace containing large repos (in our case 192,898 files / 6.2 GB under `workspace/coding_projects/`, including `deepseek-harness` 1.9 GB with 64,230 files in `node_modules`), this initial scan blocks the event loop for **25 s+** (measured), so every request in the process — HTTP, channel WebSockets, and the SIGTERM handler — is starved. Only the periodic `watcher.watch` polling is offloaded to a thread; the initialization scan is not.
**Related PR(s):** none (no existing issue/PR on this). Closely related symptom-class issue: **#7363** (synchronous calls freeze the event loop and timeout never fires) — but that one is the embedding health-check path, a different root cause. **#7261** (SSE serialization loop, 100% CPU) is a different, already-fixed bug.
**Security considerations:** No sensitive data. Paths shown are the reporter's local container layout only.
## Component(s) Affected
- [x] Core / Backend (app, agents, config, providers, utils, local_models)
- [x] Console (frontend web UI)
- [ ] Channels (DingTalk, Feishu, QQ, Discord, iMessage, etc.)
- [ ] Skills
- [ ] CLI
- [ ] Documentation (website)
- [ ] Tests
- [ ] CI/CD
- [ ] Scripts / Deploy
## Environment
- **QwenPaw version:** 2.2.1
- **OS:** Debian 12 (bookworm) inside Docker container (host: Ubuntu 24.04)
- **Install method:** Docker (custom image based on `agentscope/qwenpaw`)
- **Python version (if applicable):** 3.11.2
- **watchfiles:** 1.2.0
## Steps to Reproduce
1. Make the agent workspace large: clone/copy several big repos into `workspace/coding_projects/` so the tree contains ~190k+ files / >6 GB (e.g. a repo with `node_modules`). Smaller workspaces work fine.
2. Start QwenPaw, open the Console WebUI.
3. Open the **file browser** (Workspace → Files). The frontend issues `GET /api/workspace/watch?root=workspace` (SSE).
4. Observe: the WebUI becomes unresponsive, Feishu/QQ channels stop answering, `/api/*` requests no longer return.
5. Restart the container: SIGTERM is sent but the process cannot exit within 10 s, so Docker sends SIGKILL (exit code 137).
## Actual vs Expected
- **Actual:** After the file browser opens the watch SSE, the entire server freezes; all HTTP/channel processing halts; the process cannot shut down gracefully on SIGTERM and is SIGKILLed.
- **Expected:** The watch SSE establishes without blocking the event loop; file-change events stream asynchronously; the server keeps serving other requests.
## Logs / Screenshots
Measured `awatch` initialization latency on the reporter's workspace (Python 3.11, watchfiles 1.2.0, `rust_timeout=1000`, `yield_on_timeout=True`):
```
/tmp (small dir): first __anext__ returned in 1.02 s ✅
/app/.../silver-wolf (192,898 files): first __anext__ did NOT return in 25 s (timed out) ❌
```
SSE handler is where the freeze starts — the last successfully served request before the hang:
```
INFO: 172.20.0.1:51782 - "GET /api/workspace/watch?root=workspace HTTP/1.1" 200 OK
(no further requests ever served until forced restart)
```
Supervisor/container level (SIGTERM → 10 s → SIGKILL, exit 137):
```
2026-09-12 13:53:38 WARN received SIGTERM indicating exit request
2026-09-12 13:53:48 container die exitCode=137 (SIGKILL after grace timeout)
```
## Additional Notes
**Suggested fix:** move the watcher construction (and the first `__anext__`) off the event loop, e.g. build the `awatch`/`RustNotify` inside `asyncio.to_thread`, or set an explicit timeout so the SSE stream degrades (e.g. fall back to a polling implementation or a "workspace too large to watch" response) instead of blocking the process. Passing `ignore_dirs`/a `watch_filter` that excludes `node_modules`, `.venv`, `target`, etc. would also cut the scan cost, though the blocking itself should be fixed regardless of scale.
**Workaround (immediate):** avoid opening the file browser while the workspace contains very large directories; moving large repos out of the watched workspace restores normal behavior.
コントリビューションガイド
評価
この issue はまだ評価されていません。