Summary
The fix in #84 bounds start_notify in claude_usage_daemon.py (Linux/macOS) so a half-open BLE link can't wedge the daemon. The Windows daemon (claude_usage_daemon_windows.py) has the same bug class and is not covered by that PR.
This is a follow-up, not blocking for #84.
Details
setup_refresh_subscription() does an unbounded await self.client.start_notify(...):
https://github.com/HermannBjorgvin/Clawdmeter/blob/main/daemon/claude_usage_daemon_windows.py#L185
It sits on the path between connect and the poll loop (await session.setup_refresh_subscription() ~line 371), identical in structure to the macOS daemon. The existing except (BleakError, ValueError, OSError) does not help: an exception handler only catches a raised error, never an await that hangs forever. If WinRT's CCCD-write confirmation never arrives on a half-open link (same failure shape as the CoreBluetooth hang #84 fixed), the single-process daemon wedges between "Connected" and the first poll — stale data, no error, no recovery until manual restart.
Fix
Mirror #84: wrap the subscribe in asyncio.wait_for(..., timeout=10) and add an except asyncio.TimeoutError that logs and proceeds (the refresh subscription is optional; the 60s poll loop works without it). Keep the existing OSError in the catch list since WinRT can surface failures as raw OSError.
Not affected
The Bash daemon (claude-usage-daemon.sh) is not vulnerable: it subscribes via busctl call ... StartNotify, which is bounded by sd-bus's default ~25s method-call timeout and whose result is discarded, so it can't block indefinitely.
Ref: #84
Summary
The fix in #84 bounds
start_notifyinclaude_usage_daemon.py(Linux/macOS) so a half-open BLE link can't wedge the daemon. The Windows daemon (claude_usage_daemon_windows.py) has the same bug class and is not covered by that PR.This is a follow-up, not blocking for #84.
Details
setup_refresh_subscription()does an unboundedawait self.client.start_notify(...):https://github.com/HermannBjorgvin/Clawdmeter/blob/main/daemon/claude_usage_daemon_windows.py#L185
It sits on the path between connect and the poll loop (
await session.setup_refresh_subscription()~line 371), identical in structure to the macOS daemon. The existingexcept (BleakError, ValueError, OSError)does not help: an exception handler only catches a raised error, never anawaitthat hangs forever. If WinRT's CCCD-write confirmation never arrives on a half-open link (same failure shape as the CoreBluetooth hang #84 fixed), the single-process daemon wedges between "Connected" and the first poll — stale data, no error, no recovery until manual restart.Fix
Mirror #84: wrap the subscribe in
asyncio.wait_for(..., timeout=10)and add anexcept asyncio.TimeoutErrorthat logs and proceeds (the refresh subscription is optional; the 60s poll loop works without it). Keep the existingOSErrorin the catch list since WinRT can surface failures as rawOSError.Not affected
The Bash daemon (
claude-usage-daemon.sh) is not vulnerable: it subscribes viabusctl call ... StartNotify, which is bounded by sd-bus's default ~25s method-call timeout and whose result is discarded, so it can't block indefinitely.Ref: #84