MemPalace/mempalace · error · ValueError
timeout_ms must be a non-negative number
Error message
timeout_ms must be a non-negative number
What it means
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.
Source
Thrown at mempalace/logstream.py:783
def wait_events(
self,
timeout_ms: int = 60_000,
poll_interval_s: float = None,
**filters,
) -> dict:
"""Block until at least one matching event exists or timeout expires.
v1 implementation per RFC 003: a polling loop inside the request,
sleeping 250-1000 ms with jitter. Timeouts are clamped to
``MAX_WAIT_TIMEOUT_MS`` and return ``{"timed_out": True,
"events": []}`` rather than raising.
``filters`` accepts the same keyword filters as :meth:`list_events`.
``poll_interval_s`` pins the sleep (tests); default is jittered.
"""
if not isinstance(timeout_ms, (int, float)) or timeout_ms < 0:
raise ValueError("timeout_ms must be a non-negative number")
timeout_ms = min(timeout_ms, MAX_WAIT_TIMEOUT_MS)
deadline = time.monotonic() + timeout_ms / 1000.0
attempt = 0
while True:
events = self.list_events(**filters)
if events:
return {"timed_out": False, "events": events}
remaining = deadline - time.monotonic()
if remaining <= 0:
return {"timed_out": True, "events": []}
if poll_interval_s is not None:
delay = poll_interval_s
else:
base = min(_POLL_MAX_S, _POLL_BASE_S * (1.5**attempt))
delay = base * (0.75 + random.random() * 0.25)
time.sleep(min(delay, remaining))
attempt += 1View on GitHub (pinned to 06cb6987f0)
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.
Example fix
// before result = ls.wait_events(correlation_id="task_123", type="patch.ready", timeout_ms=None) // after result = ls.wait_events(correlation_id="task_123", type="patch.ready", timeout_ms=300_000)
Defensive patterns
Strategy: validation
Validate before calling
def safe_timeout_ms(value, default=30_000, cap=300_000):
if value is None:
return default
value = float(value)
if value < 0:
raise ValueError("timeout must be >= 0")
return min(int(value), cap)
timeout_ms = safe_timeout_ms(raw_timeout) Type guard
def is_valid_timeout_ms(v) -> bool:
return isinstance(v, (int, float)) and not isinstance(v, bool) and v >= 0 Try / catch
try:
result = ls.wait_events(correlation_id=cid, type=t, timeout_ms=timeout_ms)
except ValueError as e:
if "timeout_ms" in str(e):
result = ls.wait_events(correlation_id=cid, type=t, timeout_ms=30_000)
else:
raise Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Understand the failure class
- Timeouts: ETIMEDOUT, deadlines, and hung requests — what actually expires when a request times out.
Related errors
- {field_name} must be a non-empty string
- {field_name} exceeds maximum length of {_MAX_ROUTING_LENGTH}
- {field_name} contains control characters
- type must be a non-empty string
- type={value!r} is not a valid event type (lowercase letters,
AI-assisted analysis of MemPalace/mempalace@06cb6987f0 (2026-08-15).
Data as JSON: /api/errors/901b6f66662869c0.
Report an issue: GitHub.