{"record":{"id":"901b6f66662869c0","repo":"MemPalace/mempalace","slug":"timeout-ms-must-be-a-non-negative-number","errorCode":null,"errorMessage":"timeout_ms must be a non-negative number","messagePattern":"timeout_ms must be a non-negative number","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"mempalace/logstream.py","lineNumber":783,"sourceCode":"\n    def wait_events(\n        self,\n        timeout_ms: int = 60_000,\n        poll_interval_s: float = None,\n        **filters,\n    ) -> dict:\n        \"\"\"Block until at least one matching event exists or timeout expires.\n\n        v1 implementation per RFC 003: a polling loop inside the request,\n        sleeping 250-1000 ms with jitter. Timeouts are clamped to\n        ``MAX_WAIT_TIMEOUT_MS`` and return ``{\"timed_out\": True,\n        \"events\": []}`` rather than raising.\n\n        ``filters`` accepts the same keyword filters as :meth:`list_events`.\n        ``poll_interval_s`` pins the sleep (tests); default is jittered.\n        \"\"\"\n        if not isinstance(timeout_ms, (int, float)) or timeout_ms < 0:\n            raise ValueError(\"timeout_ms must be a non-negative number\")\n        timeout_ms = min(timeout_ms, MAX_WAIT_TIMEOUT_MS)\n        deadline = time.monotonic() + timeout_ms / 1000.0\n\n        attempt = 0\n        while True:\n            events = self.list_events(**filters)\n            if events:\n                return {\"timed_out\": False, \"events\": events}\n            remaining = deadline - time.monotonic()\n            if remaining <= 0:\n                return {\"timed_out\": True, \"events\": []}\n            if poll_interval_s is not None:\n                delay = poll_interval_s\n            else:\n                base = min(_POLL_MAX_S, _POLL_BASE_S * (1.5**attempt))\n                delay = base * (0.75 + random.random() * 0.25)\n            time.sleep(min(delay, remaining))\n            attempt += 1","sourceCodeStart":765,"sourceCodeEnd":801,"githubUrl":"https://github.com/MemPalace/mempalace/blob/06cb6987f02610784fefbad4b2bd5d026d164ba6/mempalace/logstream.py#L765-L801","documentation":"wait_events requires timeout_ms to be an int or float >= 0. Negative timeouts, strings like '30000', and None all fail. Valid values are clamped to MAX_WAIT_TIMEOUT_MS = 300,000 (5 minutes); timeout_ms=0 is legal and degenerates to a single immediate poll that returns timed_out=True when nothing matches. Note the function returns {'timed_out': True, 'events': []} on expiry instead of raising.","triggerScenarios":"wait_events(..., timeout_ms=-1); timeout_ms=None; timeout_ms='5m' from user input; passing seconds (0.3) where milliseconds (300) was intended, producing near-instant timeouts.","commonSituations":"Config values parsed as strings; unit confusion between seconds and milliseconds; callers treating None as 'wait forever' — the API has no forever mode, it caps at 5 minutes.","solutions":["Pass milliseconds as a number: timeout_ms=300_000 for the 5-minute cap.","For longer waits, loop over wait_events results and accumulate, since anything above 300,000 is clamped anyway.","Coerce external input: timeout_ms=max(0, int(float(raw))) and reject non-numeric strings at the boundary."],"exampleFix":"// before\nresult = ls.wait_events(correlation_id=\"task_123\", type=\"patch.ready\", timeout_ms=None)\n// after\nresult = ls.wait_events(correlation_id=\"task_123\", type=\"patch.ready\", timeout_ms=300_000)","handlingStrategy":"validation","validationCode":"def safe_timeout_ms(value, default=30_000, cap=300_000):\n    if value is None:\n        return default\n    value = float(value)\n    if value < 0:\n        raise ValueError(\"timeout must be >= 0\")\n    return min(int(value), cap)\n\ntimeout_ms = safe_timeout_ms(raw_timeout)","typeGuard":"def is_valid_timeout_ms(v) -> bool:\n    return isinstance(v, (int, float)) and not isinstance(v, bool) and v >= 0","tryCatchPattern":"try:\n    result = ls.wait_events(correlation_id=cid, type=t, timeout_ms=timeout_ms)\nexcept ValueError as e:\n    if \"timeout_ms\" in str(e):\n        result = ls.wait_events(correlation_id=cid, type=t, timeout_ms=30_000)\n    else:\n        raise","preventionTips":["Always pass milliseconds as a number; there is no 'wait forever' — the cap is 300,000 ms.","For longer waits, loop wait_events calls and check result['timed_out'] between rounds.","Coerce string config values to int at the config layer, not at the call site."],"tags":["validation","logstream","timeout","wait-events"],"backgroundTag":null,"analyzedSha":"06cb6987f02610784fefbad4b2bd5d026d164ba6","analyzedAt":"2026-08-15T03:03:36.213Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}