fix: Prevent turbo dev from hanging when daemon file watching fails (#12091)
## Summary
- Add timeouts to prevent `turbo dev` from hanging indefinitely when the
daemon's file watcher fails to initialize
- Fix the daemon's `package_changes` gRPC stream to properly close when
the underlying broadcast channel is closed
- Improve error messages to help users diagnose and recover from file
watching failures
## Context
Users upgrading from 2.8.10 to 2.8.11/2.8.12 reported that `turbo dev`
hangs after printing the version number, showing no output and no error.
The root cause is a chain of missing timeouts and error handling in the
daemon's file watching pipeline.
When the daemon's `FileSystemWatcher` fails to initialize (e.g. FSEvents
on macOS doesn't deliver events, cookie directory permission issues, or
a stale daemon from a previous version), several things go wrong:
1. The `PackageChangesWatcher` subscriber waits forever on
`OptionalWatch::get()` for file events that will never arrive
2. The daemon's gRPC `package_changes` stream handler loops on
`RecvError::Closed` instead of breaking, producing infinite error events
3. The client's `WatchClient::start()` blocks on `events.next().await`
with no timeout, appearing frozen
## Changes
**`crates/turborepo-lib/src/run/watch.rs`** — Add a 10-second timeout
when the client waits for the first event from the daemon's
`package_changes` stream. Fails with an actionable error message instead
of hanging.
**`crates/turborepo-lib/src/package_changes_watcher.rs`** — Add a
5-second timeout to the subscriber's wait for file events to become
ready. Previously waited indefinitely.
**`crates/turborepo-daemon/src/server.rs`** — Fix the `package_changes`
gRPC handler to distinguish `RecvError::Closed` from
`RecvError::Lagged`. On `Closed`, it now sends one error event and
breaks out of the loop instead of spinning.
**`crates/turborepo-filewatch/src/lib.rs`** — Upgrade file watcher
initialization failure logs from `warn!` to `error!` with guidance to
run `turbo daemon clean`.
## Testing
Added 3 new tests:
- `optional_watch::test::get_returns_error_when_sender_dropped` —
Verifies error propagation when the sender drops without sending
- `optional_watch::test::get_with_timeout_returns_elapsed_when_no_value`
— Verifies timeout behavior for the pattern used in the fix
- `server::test::package_changes_stream_closes_on_sender_drop` —
Reproduces the exact bug: broadcast sender drops, verifies the
forwarding loop sends an error event and terminates instead of spinning