---
quick_id: 260726-824
subsystem: network
tags: [discovery, idle-deadline, disc-03, milestone-audit-followup]
requirements_completed: DISC-03
provides:
  - "consumer-time-independent discovery idle window"
key-files:
  created: []
  modified:
    - src/lifx/network/discovery.py
    - tests/test_network/test_discovery_rebroadcast.py
decisions:
  - "Kept the pre-yield mark_response() and ADDED a post-yield one, rather than moving it — the pre-yield call is what stops a duplicate flood expiring the window for serials dedup never yields (DISC-02, Pitfall 1 / D-04)"
  - "Overall deadline deliberately untouched, so resetting on resume cannot let a slow consumer run discovery indefinitely; pinned by a second test"
  - "Rejected buffering serials and constructing devices after the generator completes — it removes the interaction but destroys streaming, which is the point of the async generator API"
metrics:
  duration: ~10 minutes
  completed: 2026-07-26
  tasks: 2/2
status: complete
---

# Quick Task 260726-824: Consumer time no longer expires the discovery idle window

**`_discover_with_packet()` marks a response again once the consumer resumes the
generator, so `api.discover()`'s per-device `create_device()` round trips can no longer
truncate a sweep.**

## Status: COMPLETE

## The defect

The idle timer was reset only *before* a response was yielded. `api.discover()`
(`src/lifx/api.py:792`) awaits `create_device()` inside the loop, and those requests carry
a 16 s wall deadline (`const.py:42`) against a ~4 s idle window. A device that answered
`GetService` and then stopped responding held the generator long enough for
`deadline.idle_expired` to fire before responses from later re-broadcasts were read — the
sweep ended early and silently, reporting fewer devices rather than an error.

Surfaced by the v1.1 milestone audit's cross-phase integration check, which flagged it as
the one fragile link in an otherwise fully wired milestone. It degrades DISC-03's
"median per-round coverage equals the full roster" guarantee on exactly the unhealthy
networks that requirement exists to cover.

## What changed

- `src/lifx/network/discovery.py` — a second `deadline.mark_response()` after
  `yield discovery_resp` returns, with the docstring Note corrected. It previously
  documented the old behaviour and told callers to raise `idle_timeout_multiplier` to
  compensate; that advice is now obsolete.
- `tests/test_network/test_discovery_rebroadcast.py` — new `TestConsumerIdleWindow` class.

**Follow-up (`9afe8df`, PR #177):** a max-effort review of PR #176 found the replacement
Note overstated the guarantee — it claimed a consumer "cannot expire discovery early",
which is false by default because `DEFAULT_REQUEST_TIMEOUT` (16 s) outlasts
`DISCOVERY_TIMEOUT` (15 s), so one unreachable device still ends the sweep via the overall
deadline. The Note now says so and points affected callers at `timeout`. `CLAUDE.md` and
`AGENTS.md` were carrying the pre-fix semantics and now describe the idle window as
measuring network silence. The two regression tests were also hardened: their receive
mocks returned packets without awaiting (so a regression would spin rather than fail),
neither asserted the window was actually reset by consumer time, and the slow-consumer
test did not bound the overall timeout.

## Verification

- `test_consumer_time_does_not_consume_idle_window` — consumer sleeps 0.5 s against a
  0.3 s idle window (max_response_time 0.15 × multiplier 2.0); a second device answers
  afterwards and must still be yielded. **Confirmed to fail without the fix**: the
  post-yield `mark_response()` was temporarily stripped and the test reported
  `['d073d5010203'] == [..., 'd073d50a0b0c']` — the second device stranded.
- `test_overall_deadline_still_bounds_a_slow_consumer` — guards the obvious way to get
  this wrong; the overall timeout must still end a sweep whose consumer sleeps repeatedly.
- `uv run pytest tests/test_network/` → 254 passed
- `uv run --frozen pytest` → 2629 passed
- `uv run ruff format --check` + `ruff check` clean; `uv run pyright` → 0 errors

## Commits

| Commit | Content |
|--------|---------|
| `dca9e39` | fix(discovery): stop consumer time from expiring the discovery idle window (PR #176) |
| `9afe8df` | fix(discovery): correct the idle-window docs and harden its tests (PR #177) |

## Known Stubs

None.

## Threat Flags

None — no new input parsing, no new subprocess or network surface. The change only resets
an existing monotonic timer at one additional point, and the overall deadline still bounds
the loop.
