Branch: 2.3.0-dev · Repo: F:\Indicragv2\IndicRAG · Plan: F:\Indicragv2\implementation_plan.md
Handoff doc for resuming after compaction. Everything shipped this session is committed; working tree carries only the local .env change (gitignored).
Added Phase 8 to implementation_plan.md: OpenRouter as a first-class secondary LLM provider + user-selectable models. Design settled:
- Provider abstraction behind
generate_with_failover/llm_generate_stream; OpenRouter backend returns a Gemini-shaped response shim so the ~6 agent call sites stay unchanged. - Curated
.envallowlistLLM_SELECTABLE_MODELS(e.g.gemini-3.5-flash,anthropic/claude-haiku,openai/gpt-5.4-nano) drives the UI dropdown; live OpenRouter catalog only enriches capability metadata. LLM_PROVIDER/LLM_FALLBACK_PROVIDERmake either vendor primary or fallback.- Scoped 6 d → 8 d after folding 6 gaps (re-export constraint at
rag.py:577, per-backend error classifiers,(provider,model)circuit key, net-new streaming cross-provider failover,google-genaistays a hard dep,SAFETY_SETTINGSdropped on OpenRouter, pre-commit capability gating). No code written — deferred behind Phases 5–7.
Ran the real mDeBERTa NLI model (offline) against the plan's stated "over-firing on paraphrase" risk. Result: opposing claims 0.724, paraphrase 0.019, unrelated 0.126 — threshold 0.6 well-placed, wide separation, no false positives. Ships as-is, no code change. Memory updated. (Real-model check kept in scratchpad; repo tests deliberately mock NLI for offline CI.)
Decisions: manual-trigger first (schedule loop deferred) + full-PDF arXiv ingest (no-PDF → abstract-only). Single-worker, no Redis/Celery.
- ✅ Increment 1 —
f70ae57—watchestable + CRUD inpersistence.py(save_watch/get_watch/list_watches/due_watches/delete_watch). Mirrors job/prefs pattern. Tests:tests/test_watch_persistence.py(6). - ✅ Increment 2 —
b5eb6dd—routes/watch.pyCRUD (POST/GET/GET{id}/DELETE /watch), gated byconfig.WATCH_ENABLE(404 when off). Router wired inapi_server.py. KnobsWATCH_ENABLE+WATCH_DEFAULT_CADENCEinconfig.py+.env.example. Tests:tests/test_watch_routes.py(8).
18 watch tests green; no regressions.
Design (contracts already confirmed):
execute_arxiv_search(topic, max_results)(+execute_open_access_searchfallback) → hits{text,title,source,pdf_url,arxiv_id}.- Filter hits whose
arxiv_id ∉ watch.seen_ids. - Per new hit: download
pdf_url→ temp file →ingest.ingest_pdf(path, paper_id=arxiv_id, metadata={title,source})→ returns(n_chunks, title);n_chunks > 0= genuinely new (built-in paper_id/content-hash dedup). Nopdf_url→ abstract-only chunk. - Summarize the newly-ingested papers into a cited digest (LLM).
- Update
seen_ids += new arxiv_ids,latest_digest,last_run=now, advancenext_run = now + cadence;save_watch. - Endpoint:
POST /watch/{id}/run— sync viarun_in_threadpool(ingest blocks the event loop otherwise). - Tests: mock
execute_arxiv_search,ingest_pdf, and the LLM summarizer; assert dedup,seen_idsgrowth, digest storage,next_runadvance.
Then Increment 4 (asyncio lifespan loop over due_watches) and Increment 5 (GET /watch/{id}/digest + UI).
- Restart the server after
.envchanges —config.pyreads env only at import. .envhasWATCH_ENABLE=truelocally (gitignored, NOT committed)./watchreturns{"detail":"Topic watches are not enabled"}(404) when off — that's the gate working.- Run tests:
python -m pytest tests/test_watch_routes.py tests/test_watch_persistence.py -q(needsPYTHONPATH= repo root for standalone scripts; pytest handles it via conftest which also redirects the SQLite DB to a temp file). - GateGuard fact-forcing hook fires on first-touch of each file + first Bash. Disable with
export ECC_GATEGUARD=offorECC_DISABLED_HOOKS += pre:edit-write:gateguard-fact-force. - No Co-Authored-By Claude lines in commits (user preference).
{id, user_id, topic, language, cadence(daily|weekly|monthly), seen_ids:[], latest_digest, next_run(ISO-8601 UTC|null), last_run, created_at} — stored as json in watches.data; user_id/next_run/last_run/created_at denormalized as columns.