Summary
Extract a common DetectionSource protocol from DetectionFetcher and refactor TrackerInstance to use it, enabling pluggable detection sources (HTTP polling or TCP push).
Context
TrackerInstance currently creates a DetectionFetcher directly and calls self.fetcher.fetch() in its fetch loop. To support TCP push alongside HTTP polling, the instance needs to accept either source type interchangeably.
See design spec: docs/superpowers/specs/2026-03-11-tcp-push-detection-forwarding-design.md in the retina monorepo.
Changes
New: DetectionSource protocol (tracker_host/detection_source.py)
class DetectionSource(Protocol):
async def receive(self) -> Optional[dict[str, Any]]:
"""Return next detection frame, or None if no new data."""
...
async def close(self) -> None: ...
@property
def is_healthy(self) -> bool: ...
Updated: DetectionFetcher (tracker_host/fetcher.py)
- Rename
fetch() to receive() to match the protocol
- Add
is_healthy property (returns self.state.is_healthy)
- No other behavioral changes
Updated: TrackerInstance (tracker_host/instance.py)
- Constructor accepts a
DetectionSource instead of creating DetectionFetcher internally
_fetch_loop calls self.source.receive() instead of self.fetcher.fetch()
ExtendedOutageError handling stays the same (only DetectionFetcher raises it; TcpReceiver will use a different mechanism)
Updated: TrackerConfig (tracker_host/config.py)
detection_url becomes Optional[str] (currently required)
- Add
mode: Optional[str] — inferred as "http" if detection_url present, otherwise "tcp"
- Add
node_id: Optional[str] — required for mode: "tcp" entries
Updated: TrackerManager (tracker_host/manager.py)
- Creates
DetectionFetcher-backed instances for mode: "http" trackers
- Creates instances without a source for
mode: "tcp" trackers (source will be attached later when the TCP server integration is added in a follow-up issue)
Acceptance criteria
Summary
Extract a common
DetectionSourceprotocol fromDetectionFetcherand refactorTrackerInstanceto use it, enabling pluggable detection sources (HTTP polling or TCP push).Context
TrackerInstancecurrently creates aDetectionFetcherdirectly and callsself.fetcher.fetch()in its fetch loop. To support TCP push alongside HTTP polling, the instance needs to accept either source type interchangeably.See design spec:
docs/superpowers/specs/2026-03-11-tcp-push-detection-forwarding-design.mdin the retina monorepo.Changes
New:
DetectionSourceprotocol (tracker_host/detection_source.py)Updated:
DetectionFetcher(tracker_host/fetcher.py)fetch()toreceive()to match the protocolis_healthyproperty (returnsself.state.is_healthy)Updated:
TrackerInstance(tracker_host/instance.py)DetectionSourceinstead of creatingDetectionFetcherinternally_fetch_loopcallsself.source.receive()instead ofself.fetcher.fetch()ExtendedOutageErrorhandling stays the same (onlyDetectionFetcherraises it;TcpReceiverwill use a different mechanism)Updated:
TrackerConfig(tracker_host/config.py)detection_urlbecomesOptional[str](currently required)mode: Optional[str]— inferred as"http"ifdetection_urlpresent, otherwise"tcp"node_id: Optional[str]— required formode: "tcp"entriesUpdated:
TrackerManager(tracker_host/manager.py)DetectionFetcher-backed instances formode: "http"trackersmode: "tcp"trackers (source will be attached later when the TCP server integration is added in a follow-up issue)Acceptance criteria
DetectionSourceprotocol definedDetectionFetcherconforms toDetectionSourceprotocolTrackerInstanceworks with anyDetectionSourcedetection_urlis optional in configdetection_urlpresent → http, otherwise → tcp