← All needs

90s idle timeout fires on queue wait with a self-hosted backend, and transient failures spend the paid-send budget and park batches permanently (13.35

## Summary With a **self-hosted OpenAI-compatible backend** (Ollama over Tailscale), 13.35.0 parks batches permanently because the **90 s idle timeout fires on time the request spends queued**, not on a slow or hung model. Two attempts are then spent and `maxPaidSends=2` parks the batch, which is never resent. Result on my machine: **~14.5 h with zero observations written** and 16 batches parked, while the backend was demonstrably healthy. ## Environment - claude-mem **13.35.0** (upgraded from 13.25.1) - `CLAUDE_MEM_PROVIDER=openrouter`, `CLAUDE_MEM_OPENROUTER_BASE_URL=http:// :11434/v1` (local Ollama, 3 instances behind nginx) - `CLAUDE_MEM_LLM_TIMEOUT_MS=120000` - macOS client, Linux inference host ## What happens ``` [WARN ] [SDK] Retrying OpenRouter after 871ms (attempt 1/2) {kind=transient, outcome=ambiguous, message=OpenRouter network error: Request timed out after 90000ms idle (reaching :11434)} [ERROR] [SESSION] Batch parked: its paid-send budget is spent, so it is not resent {parkedMessageIds=[825], paidSendsSpent=2, maxPaidSends=2, pendingCount=395} ``` ## Why the idle timer is the wrong measure here Requests are **non-streaming**, so no bytes arrive until generation completes. Any request that waits in a local load balancer (all backends busy) or simply generates for a while looks "idle" for its entire life. Server-side evidence for the same period — the backend was not slow: | check | value | |---|---| | requests served, last 60 min | 29 / 30 / 29 across three instances | | **requests exceeding 90 s** | **0** (max 53.7 s) | | competing GPU processes | none | | trivial request latency from the client | 1.0 s | So nothing ever generated for 90 s. The time was spent waiting for a free backend, which the client counts as idle and the server never sees. ## Three separate problems 1. **The 90 s idle timeout appears to be a bare constant.** I could not find an env var for it in the shipped bundle. `CLAUDE_MEM_LLM_TIMEOUT_MS=120000` is effectively unreachable, since a shorter idle timer always fires first. 2. **Idle timeout = CLAUDE_MEM_LLM_TIMEOUT_MS`. 2. Don't charge the paid-send budget for `kind=transient` / `outcome=ambiguous` failures, or give parked batches a manual resend path. Today they are unrecoverable. 3. Consider enabling streaming for the observer call. A single streamed token resets the idle timer and makes it measure what it is meant to measure — a hung model, rather than a busy queue. ## Related - #3721 — same watchdog class, different trigger (180 s, Windows, model produced nothing). Closed. - #3606 — adjacent: non-XML replies confirming batches. This report is about the transport path before any reply exists. ## Notes Self-hosted backends make this much easier to hit than hosted APIs, because a local pool queues rather than scaling out. Happy to supply fuller logs.