Skip to content

2.89.4

Pre-release
Pre-release

Choose a tag to compare

@ttlequals0 ttlequals0 released this 21 Aug 22:33
· 17 commits to main since this release
3e8712d

2.89.4

Fixed

  • Long log lines wrap in the run log viewer instead of pushing the row sideways. On phones the time and level sit above the message, so a long URL gets the full width.
  • Silenced the HTTP transport chatter that openai 3.x reintroduced. That release moved to httpx2 and httpcore2, which log under new names, so the existing suppression missed them and per-request connection dumps went to the server log at DEBUG. They were a quarter of every episode run log.

2.89.3

Changed

  • Chunk extraction now runs ahead of the GPU during chunked transcription. Two ffmpeg workers cut and normalize upcoming chunks while the current one transcribes, instead of the GPU idling through each extraction. On a 46-minute episode with the large-v3 model this takes roughly five minutes off a full run, with the same gain again in the verification pass. A mid-run chunk-size change (GPU out-of-memory, extraction timeout) discards the queued extractions and redoes them at the new size.
  • The log viewer's level pills are now plain filters: none selected shows every line, and selecting pills keeps only those levels. They previously acted as a minimum-severity threshold, which read as a broken filter when Debug was selected and INFO lines still showed.

2.89.2

Added

  • Per-run episode logs. Every processing run writes its pipeline log to disk as JSONL, and the episode page's new Logs card opens any run's log in a full-screen viewer with a minimum-level filter, text search, and a raw download. A log holds whatever that run logged, including the lines its detection and reviewer worker threads wrote, and a failed run keeps its log. A run's log closes when its history row lands, so anything logged after that, like the webhook and the RSS refresh, is not in it. Retention is 30 days by default, editable in Settings > Global Defaults, where 0 keeps nothing and lets the cleanup sweep delete what is already stored. Any feed can opt in or out in its own settings. Files live under data/logs/episodes/<slug>/<episode-id>/ and stop at 20 MB per run with a truncation marker; the cleanup sweep also removes files no run points at. EPISODE_LOG_RETENTION_DAYS and EPISODE_LOG_LEVEL seed the two global settings. New endpoint: GET /feeds/<slug>/episodes/<episode-id>/runs/<n>/log, with format=raw for the file itself. Closes #660.

2.89.1

Added

  • Low-ad-yield response policy. When a run MinusPod started itself removes far less ad time than the feed's recent average, it can rerun detection automatically. Choose the action under Settings > Global Defaults: do nothing (the default), redetect ads from the stored transcript, reprocess, or run a full analysis. Each feed can override that choice or turn the policy off. The episode keeps serving its current audio while the rerun waits its turn, behind any fresh episodes in the queue. A rerun happens at most once per episode. The mark goes down when the rerun is queued, so a rerun that fails still spends it. The policy never fires after a manual reprocess or on a pass-through, skip-detection, or cue-only feed. Degraded runs, which publish on partial detection, are skipped too, since they already queue their own re-detect. Playing an unprocessed episode counts as an automatic run and can trigger the policy. LOW_AD_YIELD_ACTION seeds the global default on a fresh install; the stored setting is editable at runtime.

Changed

  • The Settings page's Processing Queue lists the whole backlog. It showed only the active job plus whatever an enqueue path had registered by hand, so a bulk reprocess or a feed refresh that queued a dozen episodes looked like one item, or like nothing at all. GET /episodes/processing now returns the pending queue rows in the order the worker claims them, and the panel numbers each waiting episode and collapses past ten rows behind a Show all toggle. Cancel works per row, so only the row being canceled reads as canceling. The list is capped at 200 rows while the heading counts the whole backlog.

Fixed

  • The queue drainer dropped an episode re-queued while its own run was still finishing. The queue holds one row per episode, so the drainer's verdict on the finished run overwrote that row and the rerun never happened. A verdict now only lands on a row the drainer still holds, which also repairs the automatic re-detect a degraded run queues for itself.

2.89.0

Security

  • Closed a DNS-rebinding TOCTOU in the SSRF guard. Outbound fetches validated a hostname's resolved addresses, then let the HTTP client resolve the same hostname again to connect, so a record that flipped between the two lookups could pass validation on a public address and connect to a private one. A new transport now resolves each request and redirect hop once, validates every returned address, and connects to the validated addresses in order; the URL, Host header, SNI, and certificate verification stay on the original hostname. A connect failure falls back to the next validated address, each tried at most once, so a multi-homed host stays reachable. SSRF_IP_PINNING=false is the kill switch back to the previous per-request resolution, for isolating a fetch failure the pin might cause.

Added

  • Reviewer verdicts carry a structured is_ad boolean alongside the existing prose, with the prose form kept as a fallback when a model omits the field. Contradiction holds, where a verification finding disagrees with a kept pass-1 span, key off the structured field when present, and the telemetry for a hold now emits once per held span instead of two or three times through the different code paths that inspect the same verdict. On a model that emits is_ad true for a disputed span, the guard no longer holds it, so spans that used to land in Held for Review are now cut. Installs running the shipped reviewer prompt unedited pick this up on upgrade, since that prompt is re-seeded on startup.
  • AD_DETECTION_MAX_FAILED_WINDOW_RATIO (default 0.25). When this fraction of a detection or verification pass's windows fail on an LLM error or timeout, the whole pass is now treated as failed instead of being accepted with the failed windows unexamined. A failed first pass fails the episode and the retry ladder picks it up. A failed verification pass leaves the first-pass markers and audio as they are, and records the second scan as incomplete rather than clean, which also fixes an all-windows-failed verification run reading as a clean scan. Set to 1.0 to restore the previous behavior. The variable seeds a stored setting that is read on every pass, so the threshold can be retuned without a restart. A pass where only some windows failed is retried rather than published from pattern matches alone, since a provider that answered part of a pass usually answers all of it on the retry.
  • Community patterns carry a staleness-based trust tier: active (matched locally in the last 90 days), unproven (no recent local match), or stale (a community pattern with no local match in the last 90 days and no community confirmation within a year). A new community_last_confirmed_at column and optional last_confirmed_at corpus field track community confirmation, and the Patterns page shows an Unproven or Stale badge; an active pattern gets no badge.
  • A reviewer calibration self-test (python -m tools.reviewer_calibration) runs a labeled corpus of ad and non-ad transcripts through the production reviewer stack and reports verdict agreement plus how often responses carry the structured is_ad field. It also runs automatically in a background thread whenever the reviewer model setting changes, including a detection model change while the reviewer is left on same_as_pass, storing its result under the reviewer_calibration_last setting; a failed or slow run never blocks the settings save. Disable the auto-run with reviewer_calibration_on_change (or REVIEWER_CALIBRATION_ON_CHANGE).
  • Gunicorn warns at startup when GUNICORN_WORKERS is greater than 1 and the rate-limit storage is still the default memory://, since limiter counters are per process and the effective limit multiplies by worker count. Login lockout is database-backed and unaffected.
  • Unit tests for the heuristic pre-roll and post-roll detector.

Tooling (benchmark; not in runtime image)

  • Two models added to the sweep roster: qwen/qwen3.8-27b and google/gemini-3.7-flash. Both joined the existing 2026-08 campaign against the frozen prompt, so no earlier model was re-run.
  • google/gemini-3.7-flash lands in the top statistical tier at $0.64 per episode against google/gemini-3.6-flash's $1.44 for a tied score, so 3.6 has no remaining case. docs/llm-providers.md updated to say so.
  • qwen/qwen3.8-27b scores mid-table with brittle JSON and is not a candidate.
  • Fresh pricing snapshot, and the previous report archived to results/archive/2026-08-15/.

Changed

  • openai 2.53.0 to 3.3.0, which migrates the client onto httpx2/httpcore2; anthropic 0.120.2 to 0.124.0; wheel to 0.48.0.
  • Frontend: lucide-react to 1.31.0, swagger-ui-dist to 5.32.13, eslint to 10.8.1, @testing-library/user-event to 14.6.4, @types/react-dom to 19.2.4.
  • Pass-2 verification reconciliation extracted out of processing.py into its own module.
  • Type hints modernized to PEP 585/604 (list[str] over List[str], X | None over Optional[X]), and the ruff lint gate widened from F alone to also cover B904, B905, S608, UP006, UP045, and UP035. The database layer's dynamic SQL is audited and exempted from S608: the interpolated identifiers are hardcoded or allowlist-validated, and values are always bound parameters.
  • zip() calls pairing same-length lists now pass strict=True, which raises on a mismatch instead of silently truncating. One of the sites this caught was a latent length mismatch: chapter span end times could carry one more entry than an empty start-time list, which the strict check now prevents from recurring.
  • Exception re-raises now chain explicitly (raise ... from err or from None), so a traceback shows the real cause instead of only the immediate wrapper.