Build & Deploy ResearchOwl / build-and-push (push) Successful in 9s
Añade /generate short_en y /short_spec. El pipeline genera un shot spec con Haiku, verifica cada cifra, fecha y cita contra los chunks de la sesión, lo renderiza en shortsmith y entrega el MP4 por Telegram junto a un informe de claims. - ShortsmithClient con sondeo y fallback al spec JSON si el render falla - Contrato de plantillas obtenido de GET /templates, no codificado - Comprobación de fundamento determinista, sin LLM - outputs.published_url para enlazar el artículo de Ghost - Normalización de comillas rectas a tipográficas (ver KNOWN-ISSUES.md) Lo que no aparece en los chunks se contrasta contra el ejemplo del prompt: si casa ahí es fuga, no invención, y se informa como tal. El purgado de sesiones se lleva también su MP4. La subida a YouTube queda fuera a propósito: fase 3. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
128 lines
6.8 KiB
Markdown
128 lines
6.8 KiB
Markdown
# ResearchOwl — Known Issues & Operational Gotchas
|
|
|
|
## Brotli / aiohttp incompatibility
|
|
|
|
Do NOT install any brotli backend (`Brotli`, `brotlicffi`) while on aiohttp 3.14.x.
|
|
aiohttp's incremental brotli decompressor is broken and fails intermittently
|
|
depending on stream chunking ("Can not decode content-encoding: br" on VALID
|
|
streams — the same bytes decompress fine offline; httpx is unaffected).
|
|
|
|
Extra trap: merely *installing* a backend makes aiohttp advertise `br` in the
|
|
default `Accept-Encoding` of every session that doesn't set one explicitly.
|
|
On 2026-07-04 a scraper fix installed brotlicffi and inadvertently enabled br
|
|
project-wide: Ghost/Cloudflare responded in brotli, the decode blew up AFTER
|
|
Ghost had already accepted the POST, and the publish fallback re-published →
|
|
duplicate drafts + silently lost SEO notices.
|
|
|
|
Fix applied (commits `397546a` + `76c927f` + `7d07375`, guard relocated
|
|
2026-07-05):
|
|
- no brotli backend in requirements.txt (comment there explains why),
|
|
- explicit `Accept-Encoding: gzip, deflate` on all Ghost/autofill aiohttp
|
|
sessions and on the scraper HEADERS (`57f341f`),
|
|
- duplicate guard INSIDE `GhostPublisher.publish_draft` (covers every caller,
|
|
including `/publish`): if the POST is accepted (2xx) but reading the response
|
|
fails, it recovers the just-created draft via `find_draft_by_title(title,
|
|
since=attempt_start)` instead of raising. The guard only fires after an
|
|
accepted POST — a pre-POST failure (Ollama down, menu fetch, links) never
|
|
triggers it, so a stale same-title draft from a previous run can no longer
|
|
swallow freshly generated content.
|
|
|
|
Note: with no backend installed there is NO brotli fallback at all — a server
|
|
that responds `Content-Encoding: br` without it being advertised (misbehaving
|
|
CDN) fails undecodable and that source is lost. Accepted trade-off.
|
|
|
|
The header value lives in one place: `SAFE_ACCEPT_ENCODING` in `src/config.py`.
|
|
Every aiohttp session/request must use it explicitly — never rely on aiohttp's
|
|
default Accept-Encoding, which silently grows `br` if a backend appears.
|
|
|
|
Re-test with disclosure.org before ever re-enabling br (e.g. after an aiohttp
|
|
upgrade).
|
|
|
|
## SQLite WAL mode + read-only mounts
|
|
|
|
The database runs in WAL mode, so even read-only access needs the `-shm` file
|
|
writable, or must open with `sqlite3 "file:...?immutable=1"`. Backups (daily
|
|
CronJob `researchowl-db-backup`, 03:00 Europe/Madrid, PVC `researchowl-backups`,
|
|
7-day retention) inherit WAL mode: to inspect one from a read-only mount use
|
|
`?immutable=1`; to restore, copy it to a writable location first.
|
|
|
|
## DDG (duckduckgo_search) blocks the event loop
|
|
|
|
`DDGS()` is synchronous (blocking requests inside). Never call it directly from
|
|
async code — always wrap in `loop.run_in_executor()` (see `_ddg_text_sync` /
|
|
`_ddg_videos_sync` in `src/scraper/exhaustive.py`). Direct calls froze the
|
|
entire Telegram bot during searches until fixed on 2026-07-04 (`8dfd011`).
|
|
|
|
## Google News RSS is a dead end from this infrastructure
|
|
|
|
`news.google.com/rss` entry links point to `/rss/articles/CBMi…` redirects that
|
|
hit a consent wall from EU IPs, and the inner `AU_yqL` id is only resolvable via
|
|
Google's private batchexecute API. Do not retry. The news seed uses Bing News
|
|
RSS instead (`ENABLE_NEWS_SEED`, real publisher URL in the `?url=` param of
|
|
apiclick.aspx — unwrapped by `_unwrap_news_link`).
|
|
|
|
## Large sources can OOM-kill the pod
|
|
|
|
On 2026-07-10 the pod was OOMKilled (memory limit was 1Gi) mid-research: a
|
|
batch of 20 concurrent sources hit a 98k-word document plus several large PDFs
|
|
at once, and pdfplumber's parse spiked RAM past the limit. The in-memory
|
|
research task died with the pod and its session sat in `running` forever.
|
|
|
|
Mitigations now in place:
|
|
|
|
- Memory limit raised to 2Gi (`k8s-manifests/researchowl/deployment.yaml`).
|
|
- PDFs capped at 15MB (was 50MB), checked both via Content-Length and actual
|
|
body size; pdfplumber runs in `run_in_executor` (it is sync + CPU-heavy and
|
|
also froze the event loop, same class of bug as DDGS) and flushes its page
|
|
cache per page.
|
|
- Extracted content is truncated to `max_content_length` (300k chars) before
|
|
hitting `source_contents`.
|
|
- On startup the bot marks orphaned `running` sessions as `interrupted`.
|
|
|
|
If a research still dies, the scraped sources survive in the DB: `/process`
|
|
re-chunks and scores them without re-scraping.
|
|
|
|
## Un shot spec escrito por Haiku falla de dos maneras concretas
|
|
|
|
Medido el 2026-08-01 generando Shorts de verdad contra sesiones reales (JAL
|
|
1628 #153, Bélgica #158). Las dos están mitigadas, pero conviene saber que
|
|
existen porque las dos son silenciosas si nadie mira.
|
|
|
|
**1. Comillas rectas dentro de una cadena JSON.** El modelo escribe
|
|
`"quote_a": ""CREDIBLE PEOPLE. THEY TOLD CLEARLY WHAT THEY SAW.""` y la cadena
|
|
se cierra en la segunda comilla: el JSON entero deja de parsear. Se repitió en
|
|
los tres intentos aunque el prompt lo prohíbe explícitamente y aunque el error
|
|
se le devolvía con el fragmento exacto. **No se arregla insistiendo**: lo
|
|
arregla `_typographic_inner_quotes()` en `src/generator/shortspec.py`, que
|
|
convierte esas comillas en `“ ”` recorriendo el texto con estado de cadena.
|
|
Sólo se ejecuta después de un fallo de parseo, así que un JSON correcto no pasa
|
|
por ahí. Además es lo que se quiere dibujar: las citas del canal van con
|
|
tipográficas.
|
|
|
|
**2. Se copian cifras del ejemplo del prompt.** El `examples/jal1628.json` que
|
|
va en el prompt como referencia de formato es también una fuente de datos muy
|
|
tentadora. En la primera eval dorada, tres claims del spec generado venían del
|
|
ejemplo y no de las fuentes: `232 FT` (largo de un 747) y `40 YEARS` no
|
|
aparecían en NINGUNO de los 126 chunks de la sesión, y `RARELY, IF EVER` estaba
|
|
en 1 chunk que no entró en el top-40 que vio el modelo. La sección 5 del prompt
|
|
lo dice ahora en mayúsculas ("FORMAT ONLY … a number copied from here is a
|
|
fabrication") y eso bajó de 3 a 1.
|
|
|
|
El ejemplo se queda con cifras reales a propósito — uno sintético enseña peor
|
|
la forma —, así que la defensa es estructural: `check_grounding()` contrasta lo
|
|
que no encuentra en los chunks **contra el propio ejemplo**, y lo que casa ahí
|
|
sale en el informe como `🧪 copiado del EJEMPLO del prompt (fuga, no
|
|
invención)`. Son dos diagnósticos y piden dos acciones: una invención hay que
|
|
verificarla, una fuga hay que borrarla.
|
|
|
|
Corolario: **el informe de claims no es decorativo**. Si algún día se manda el
|
|
vídeo sin él, se estará publicando lo que el modelo recuerde del ejemplo.
|
|
|
|
## El comprobador de fundamento sólo mira los chunks que vio el modelo
|
|
|
|
`check_grounding()` compara contra los mismos ~40 chunks que se metieron en el
|
|
prompt, no contra los 126 de la sesión. Es deliberado — la pregunta es "¿lo
|
|
sacó de lo que le dimos?" — pero produce falsos positivos cuando el dato existe
|
|
en la sesión y no entró en el top-k. Un falso positivo cuesta un vistazo; un
|
|
falso negativo cuesta la credibilidad del canal.
|