An automated literature-monitoring agent: a four-stage ETL pipeline that ingests newly published papers and preprints, filters out duplicates and off-topic items, performs LLM-driven structured synthesis, and writes a formatted Markdown digest into a local folder (including an Obsidian vault). Obsidian is optional.
See literature_agent_architecture.md for the full architecture specification.
INGESTION -> DEDUP & FILTERING -> LLM SYNTHESIS -> OBSIDIAN DELIVERY
OpenAlex SQLite ID check Pydantic schema YAML frontmatter
Europe PMC Title matching Field pulse Vault writer
Vector (SPECTER2)
LLM scoring + journal weight
FreshLit's concept, architecture specification, and research profile are the author's own work. The implementation — code, debugging, and documentation — was developed with AI assistance (opencode, an AI coding agent) under human supervision.
Requires Python 3.11+ and macOS or Linux/POSIX. Native Windows is not currently supported by the hardened Codex launcher or secure digest writer. Installation currently uses a source checkout, not a standalone wheel/pipx deployment.
-
From the repository root, create the environment and install FreshLit:
python3 -m venv .venv .venv/bin/python -m pip install -e .The editable install also creates
.venv/bin/freshlitand keeps it pointed at this checkout. The officialopenai-codexSDK is pinned to0.147.0. -
Initialize the missing user files:
.venv/bin/freshlit init
This copies the neutral profile template and
.env.exampleonly if your local files do not exist. It never overwrites them. Editconfig/research_profile.mdwith your chosen search keywords/topics and a research description; see Research profile. An incomplete template cannot run.In
.env, optionally setFRESHLIT_VAULT_PATHto your output directory andOPENALEX_API_KEYfor a larger OpenAlex request budget. A normal Markdown folder works; no Obsidian installation or plugin is required. -
Prepare the configured output directory and dedicated Codex home:
.venv/bin/freshlit init --create-output-dir --prepare-codex-home
These explicit flags create missing directories. The Codex home must be outside this repository, owned by you, and mode
0700; existing unsafe permissions are rejected, not silently changed. Do not usesudoor share this directory with another Codex use. New.envfiles select~/.freshlit-codex; existing configured paths remain supported. -
Install the official Codex CLI and complete ChatGPT OAuth login as described in the official authentication documentation. Initialization prints a shell-quoted login command for your configured path. With the default path:
CODEX_HOME="$HOME/.freshlit-codex" codex loginSelect ChatGPT login, not API-key authentication. FreshLit never initiates login or reads authentication files. Full runs verify the effective safety configuration, account, and availability of the configured model. The explicit gateway alternative does not require Codex setup.
-
Check local setup, then run:
.venv/bin/freshlit doctor .venv/bin/freshlit run
doctoris local and read-only: no requests, downloads, model loading, login, or auth-file inspection. It cannot verify account/model access. The firstruninitializes the database and builds the profile vector automatically; building may download the embedding model. Later profile/model changes trigger a rebuild. Manualinit-dbandbuild-profileremain available and do not require LLM credentials.
Before replacing old configuration files, retain copies of your existing
config/settings.yaml, config/journal_tiers.json, and private profile. The new
public defaults no longer contain the original author's topics or venue weights.
Migrate while your old local research settings are still present (restore them
from your copies if needed):
.venv/bin/freshlit migrate-profile # preview only, printed to stdout
.venv/bin/freshlit migrate-profile --apply # writes profile + exclusive .bakMigration copies effective topics, Europe PMC choices, and venue weights into
the profile front matter without altering the existing Markdown body. It makes
config/research_profile.md.bak before the first write, refuses to overwrite an
existing backup, and does not touch the database, credentials, or other settings.
An already-modern profile is not rewritten. Conflicting settings keyword
overrides require manual resolution; they are not silently merged.
After migration, remove the matching research-only settings from the old YAML and venue JSON. Nonempty conflicting legacy values fail with guidance. Retain your output path, provider/model, thresholds, and other operational choices.
Once the intended source changes are in this checkout, refresh the editable installation and verify it from the project root:
.venv/bin/python -m pip install -e .
.venv/bin/python -m pip check
.venv/bin/python -m unittest discover -s tests -vThis refreshes FreshLit's dependency metadata and console command without a
blanket dependency upgrade. Preserve local source changes, the existing
environment configuration, data, and authenticated Codex profile; there is no
need to recreate them. pyproject.toml is the dependency source of truth;
requirements.txt is now a compatibility entry point for the same editable
installation, not a second list to maintain. No dependency versions were changed.
FreshLit launches the SDK's bundled, version-matched Codex runtime. Updating a
separate global codex command does not update that runtime. Keep the SDK pin
until its private-router compatibility fix has been reviewed against a newer
release.
An existing PATH launcher pointing to this checkout's virtual environment can be
used from any directory: freshlit run. Configuration and data are located from
the source checkout, not the terminal's working directory. Bare freshlit still
requires an explicit subcommand; it does not start the pipeline automatically.
# Full pipeline
.venv/bin/python -m freshlit.main run
# Useful flags
.venv/bin/python -m freshlit.main run --provider codex
.venv/bin/python -m freshlit.main run --model gpt-5.6-luna
.venv/bin/python -m freshlit.main run --lookback 14
.venv/bin/python -m freshlit.main run --limit 50
.venv/bin/python -m freshlit.main run --max-candidates 20
.venv/bin/python -m freshlit.main run --skip-llm
.venv/bin/python -m freshlit.main run --from-cache
.venv/bin/python -m freshlit.main run --reconsider-rejected
.venv/bin/python -m freshlit.main run --dry-run
.venv/bin/python -m freshlit.main run --forceOther supported overrides include --output-name. --from-cache reuses
data/last_ingest.json; --skip-llm stops after vector ranking. CLI provider and
model options override their configured values for that run. --from-cache fails
if no cache exists rather than silently fetching. Legacy caches and caches from
other queries/date windows can be replayed explicitly, with a warning.
Changing the profile does not delete paper history. If FreshLit detects history
from another or unknown profile/selection context, it warns. Use
--reconsider-rejected to re-evaluate rejected papers present in the current
ingestion or replay cache. It retains passed-paper history and within-batch
deduplication; it does not fetch all previously rejected papers. Use a suitable
lookback or an explicit cache replay to supply them. Mixed-history warnings remain
conservative because older rejected rows are not erased.
--dry-run skips paper-disposition updates and digest delivery, but it can still
initialize the database, update ingestion/profile caches, download embeddings,
and submit LLM requests. To avoid ingestion and LLM requests while testing local
ranking with an existing ingestion cache, combine
--from-cache --skip-llm --dry-run; this still needs the embedding model and may
download its weights if they are not cached.
Output: {vault_path}/{digest_folder}/YYYY-Www.md (ISO year + week), with YAML
frontmatter for Dataview, including the LLM provider and model used.
The vault must already exist. Delivery opens it as the trusted filesystem
boundary, securely creates nested digest directories, and uses directory-relative
atomic replacement without following swapped symlinks. In-process and advisory
locks serialize FreshLit rerun merges; the content-free .freshlit-delivery.lock
entry is retained in the digest folder. These guarantees require POSIX
directory-descriptor operations and advisory locking; unsupported platforms fail
closed. Other editors must cooperate with the lock to avoid conflicting edits.
| File | Purpose |
|---|---|
config/settings.yaml |
Output paths, lookback, thresholds, LLM provider/model, timeout/retries, batching/concurrency |
config/research_profile.md |
User-chosen search selectors, optional venue preferences, and research context; gitignored |
config/journal_tiers.json |
Legacy compatibility only; new venue preferences live in the profile |
data/cache.db |
SQLite dedup/disposition history (state) |
data/profile_exemplars.json |
Generated profile vector (from build-profile) |
Retrieval is not AI-generated. You select the keywords and topic IDs. FreshLit constructs API queries deterministically: quoted keywords joined by OR, plus a separate OpenAlex topic query when topics are selected. These result streams are combined, not intersected. Europe PMC is an explicit opt-in for biomedical coverage; it uses the same keywords. Topic-only profiles work with OpenAlex, but Europe PMC needs keywords to search.
The profile's YAML front matter contains structured choices; the Markdown body contains the keywords and research context. For example:
---
version: 1
search:
openalex_topics: []
europe_pmc:
enabled: false
sources: [PPR]
venue_weights: []
---
# Research Profile
## Keywords
- exoplanet atmospheres
- transmission spectroscopy
## Research Description
I study exoplanet atmospheres using observational spectroscopy. Include new
observational constraints and transferable retrieval methods. Exclude papers
concerned only with Solar System missions.Optional topic lookup uses OpenAlex directly, not an LLM, and never edits your profile automatically:
.venv/bin/freshlit topics "exoplanet atmospheres"Copy selected {id, label} entries from its output into search.openalex_topics.
IDs control retrieval; labels are for you. Canonical OpenAlex topic URLs are also
accepted. Optional venue_weights entries have issn, weight, and an optional
name; unlisted venues use filtering.default_journal_weight (normally 1.0).
The Markdown body drives the vector profile and both relevance/synthesis prompts. The AI evaluates retrieved papers against your stated interests and exclusions; it does not choose initial search terms. Front matter is excluded from embedding and LLM text so changing a display label cannot change ranking. For compatibility, embedding inputs retain the complete Markdown body and a separate keyword text; editing that body can therefore change ranking. Never put credentials in it.
Both ## Keywords and ## Research Description are required for new profiles.
Use - keyword bullets; provide at least one keyword or topic and a nonblank
research description. The unfinished example is never used as a fallback.
- Research focus — edit
config/research_profile.md; the next run detects stale vectors and rebuilds them.build-profilecan refresh explicitly. - Selectivity —
filtering.vector_threshold(floor),score_cutoff(qualify at >= 7.0),max_llm_candidates(LLM-scoring budget). - Venue weighting — edit the optional named
venue_weightsin the profile. - LLM latency —
llm.model,filtering.llm_batch_size,llm.workers,llm.timeout_seconds, andllm.max_retries. The CLI can override the model, provider, and candidate count without changing configuration.
FreshLit does not install a background service. After a successful manual run,
schedule the checkout's absolute .venv/bin/freshlit run path using cron on
Linux/macOS or launchd on macOS. For example, a cron entry running Mondays at
08:00 (scheduler-local time) is:
0 8 * * 1 /absolute/path/to/FreshLit/.venv/bin/freshlit run >> /absolute/path/to/freshlit.log 2>&1Quote executable/log paths if they contain spaces. Configuration is located from
the source checkout, not the scheduler's working directory. Run as the same user
who owns the private Codex home; ensure no overlapping runs and arrange log
rotation. Ingestion dates use ingestion.timezone (UTC by default), independently
of the scheduler's timezone. Moving the checkout may require recreating its
virtual environment and updating the scheduled executable path.
codexandgpt-5.6-lunaare the default provider and model. Production startup forces ChatGPT login and the built-in OpenAI provider, then fails closed unless the effective safety configuration, account, and model pass preflight. The app-server starts through an allowlisted, empty environment, so inherited credentials cannot select an API-key-backed billed OpenAI API path.- Each run uses one official Codex runtime and fresh ephemeral threads in fresh temporary working directories outside the repository. Approval is deny-all, the sandbox is read-only, and tools, shell execution, network/web search, MCP, agents, skills, hooks, apps, memories, history persistence, telemetry, feedback, and update checks are explicitly disabled. Temporary request directories are deleted and the shared runtime is closed when the LLM context exits.
- The low-level client explicitly declines approval requests rather than using
the SDK's permissive default. A local compatibility override buffers early
turn-completion notifications and replays pending events atomically. This
override depends on the pinned SDK's private router and must be reviewed and
regression-tested before upgrading
openai-codex. llm.timeout_secondsis a total deadline for each structured Codex request, including startup of its thread and retries. An over-deadline turn is interrupted; if bounded cancellation fails, that runtime is terminated, permanently marked unusable, and the request fails after a short cancellation grace. Successful cancellation fails only that request, without poisoning a healthy shared runtime. Up tollm.max_retriesretries are made only for invalid structured output or errors the SDK classifies as retryable. Other provider errors fail closed. A malformed batch may be retried as individual papers, but FreshLit never changes provider.llm.workersbounds concurrent batch requests through the shared runtime. All responses must satisfy normalized strict JSON schemas and strict Pydantic validation before entering the pipeline.
There is no automatic provider fallback or hidden billed API route. FreshLit will
not start OAuth, switch to an API key, or silently invoke the rollback gateway.
The hardened app-server launcher currently requires a POSIX system with the
standard /usr/bin/env utility (including macOS and mainstream Linux systems).
The legacy opencode-go/Instructor integration remains only as an explicit
rollback. Before using it, rotate any key that was ever exposed and place the
replacement only in .env:
OPENCODE_GO_API_KEY="replacement-key"Select rollback for one run, with a gateway-compatible model:
.venv/bin/python -m freshlit.main run --provider gateway --model qwen3.5-plusAlternatively, set FRESHLIT_LLM_PROVIDER="gateway" in .env, or configure the
rollback explicitly:
llm:
provider: "gateway"
model: "qwen3.5-plus"
base_url: "https://opencode.ai/zen/go/v1"The key is read and required only after gateway is explicitly selected. A
gateway failure does not fall back to Codex, and a Codex failure does not fall
back to the gateway.
Gateway response-mode negotiation accepts Pydantic validation failures or
HTTP 400/404/422 errors whose structured error metadata explicitly identifies a
mode-specific field such as response_format or tool_choice. Generic request,
model, endpoint, authentication, rate-limit, and transport failures stop after
one HTTP attempt. An explicit parse-only retry policy prevents older Instructor
versions from retrying those terminal failures internally.
Creating a gateway client suppresses Instructor's raw retry diagnostics process-wide, including direct library use without CLI logging setup. FreshLit's sanitized errors and application logging remain enabled. Recheck the diagnostic logger names when upgrading Instructor.
- LLM latency varies by model and batch content; bounded concurrency and batching keep weekly runs practical without weakening request isolation.
- OpenAlex anonymous requests have a small daily budget; add an API key for reliable weekly runs.