LP

leonjoeeee/papervault

Developer tools
0 stars 0 forks Quality 68 Trend 20MIT

Industrial-grade, self-hosted literature + knowledge layer for coding agents (Claude Code plugin, MCP underneath)

Overview

A self-hosted , delivered as one MCP server. Point any MCP-capable agent (Claude Code, and others) at it and the agent gains three tools: - — discover literature by describing your research intent in prose; relevant finds are auto-ingested (downloaded, OCR'd, full text on disk). - — look up papers by citation key / DOI / arXiv id / fuzzy title; read verbatim full text from a returned path. - — ask the knowledge base a question, get a cited prose answer synthesized from a knowledge graph distilled from everything the library has ingested. The corpus compounds without babysitting: search → queryable knowledge in tens of minutes, self-healing ingest queues, cited answers grounded in real sources. pre-release (closed beta). See docs/PRD.md for scope and docs/architecture.md for structure. This is industrial-grade infrastructure, not a toy — read the hardware floor before installing. papervault runs its own embedding, reranking, and OCR models locally.

README

papervault

A self-hosted literature + knowledge layer for coding agents, delivered as one MCP server. Point any MCP-capable agent (Claude Code, and others) at it and the agent gains three tools:

  • search_papers — discover literature by describing your research intent in prose; relevant finds are auto-ingested (downloaded, OCR’d, full text on disk).
  • get_paper — look up papers by citation key / DOI / arXiv id / fuzzy title; read verbatim full text from a returned path.
  • query — ask the knowledge base a question, get a cited prose answer synthesized from a knowledge graph distilled from everything the library has ingested.

The corpus compounds without babysitting: search → queryable knowledge in tens of minutes, self-healing ingest queues, cited answers grounded in real sources.

Status: pre-release (closed beta). See docs/PRD.md for scope and docs/architecture.md for structure. This is industrial-grade infrastructure, not a toy — read the hardware floor before installing.

Hardware floor (not optional)

papervault runs its own embedding, reranking, and OCR models locally. There is no GPU-less or degraded mode — below the floor, this product is not for you.

  • 1× NVIDIA GPU, 24 GB (RTX 3090 / 4090 class). A single card runs the full stack (embed + rerank + OCR co-tenant) inside a measured VRAM budget; a second GPU is comfort, never a requirement.
  • Linux + CUDA.
  • An OpenAI-compatible LLM endpoint + key, supplied by you (graph build, search triage, and answer synthesis all route to it). papervault bundles no keys.
  • Docker (for the Neo4j + Postgres backing stores).

Quick start

See docs/INSTALL.md for the full walkthrough. In brief:

cp .env.example .env          # then fill in your LLM endpoint + key + data dir
docker compose --env-file .env -f deploy/docker-compose.yml up -d   # Neo4j + Postgres
uv venv && uv pip install -e ".[mineru,grey]"       # beta standard: + local OCR + grey download tiers
papervault doctor             # verify GPU, databases, LLM config
papervault-mcp                # start the MCP server (== papervault serve)

Then register the server with your agent (Claude Code: install the bundled plugin; other clients: point them at the MCP endpoint — see docs/INSTALL.md).

Domain

papervault ships tuned for space physics + AI4Science as the factory default and worked sample. The domain layer — search-gate rubric, entity ontology, extraction few-shots, instruction text — lives in editable config files (src/papervault/domain/). Adapting to another field means editing those files; quality outside the shipped domain is the adapting operator’s responsibility.

Acquisition state v2

PAPERVAULT_TERMINAL_V2=1 opts into the first acquisition-pipeline v2 slice (default off). Acquisition checks retrieval eligibility, canonical identity, policy permission, and the independent quarantine marker immediately before download dispatch. Ordinary downloads keep their in-memory coordination; campaign re-opens use the durable lease and budget providers described below.

Legacy dimensions are derived without rewriting the index: metadata_only and failed map to exhausted retrieval, while ok and extract_failed map to acquired retrieval (ok becomes verified only with a recorded, labelled extract). All cause-unrecorded terminals retain unknown policy and cannot acquire again until a separate policy ruling. A status reset or identity backfill alone cannot clear that policy. New status writes record dimensions and cause; legacy dimensions are persisted only when their record is next changed. Existing artifacts remain available through the ordinary serving path.

PAPERVAULT_IDENTITY_V2=1 opts into slice 2 (default off). Its pure identity ladder selects DOI, then a namespaced source ID, then a URL; CORE work and output IDs stay distinct. The ladder selects a canonical identity without merging records or changing download order. URL recovery checks correspondence again before adopting a DOI: unresolved evidence stays retryable, and a contradiction records a rejection of that route while retaining the paper and other routes. Redirects retain the final evidence-bearing URL; supplied landing/source bibliography participates in the decision, and raw evidence types survive adoption. Existing identity records remain readable; optional evidence is omitted when absent.

Correspondence uses a minimum normalized title length of 25, title sequence similarity ≥0.92 (≥0.97 without a reference year), title token Jaccard ≥0.90, author surname Jaccard ≥0.50 plus reference first-surname agreement, and a year difference ≤1. Missing or malformed required evidence stays unresolved. DOI resolver forms and supported publisher DOI paths are compared canonically; supplied contradictory bibliography rejects the route even when DOI forms agree.

With the identity flag on, domain_status markers off_domain, non_paper, and bad_extract project to excluded policy with the same reason; unknown markers project to unknown policy. Retrieval state and existing artifacts are retained. Existing scoped policy overrides are retained and must cover the domain reason. This applies to legacy and stored dimensions at read time. Enable terminal-v2 to enforce this projection at ordinary download dispatch. Re-open v2 independently enforces acquisition admission and domain restrictions for campaigns.

PAPERVAULT_REOPEN_V2=1 opts into slice 3 (default off). In a maintenance window with papervault.service stopped, run an explicit bounded campaign:

python -m papervault.library.cli reopen-campaign --cap 20 --dry-run --json
python -m papervault.library.cli reopen-campaign --cap 20 --json

The selector takes exhausted retrieval with allowed policy, canonical resolved identity and no quarantine. It changes only retrieval eligibility; unknown or excluded policy needs a separate ruling. Dry-run reads the index and validates authorization without creating files or sending requests. Apply claims and enqueues the selected cohort through the existing download queue, waits for its results, and refuses a live service, unavailable service probe, or active campaign. An exclusive .reopen-campaign.lock prevents concurrent standalone writers. It does not recover unrelated pending work. There are no automatic campaigns.

Standing authorization permits at most 200 reservations per campaign and three campaign starts in any rolling seven days (UTC wall clock), including interrupted starts. Larger campaigns require the owner and are refused by this CLI. The append-only reopen-budget.jsonl, beside index.json, records version-1 JSON events for authorization, reservation, dispatch, completion, proven-not-sent refunds and fencing. Appends and claim index saves require successful fsync; unreadable accounting refuses acquisition. A reservation precedes dispatch and survives process restart and index rollback. A restored record missing its claim cannot acquire through the ordinary path when the ledger records its generation. Refunds are compensating events, never removal of consumption; uncertainty never refunds or resends.

The optional worker-owned reopen_claim record stores generation, lease ID, expiry, campaign, scope and phase. Every result uses that exact capability; state and PDF/text publication are fenced together under the library write-lock. Attempts download into .reopen-staging// inside the vault, with copied assets and a detached record. Successful publication checks current policy, identity and changed-artifact baselines; it preserves concurrent metadata and applies artifact deletions as well as writes. Completed generations apply once. Normal legacy records keep their serialization when the flag is off. A publication fence failure halts further campaign dispatch; queued attempts refund only when the runner proves they were never sent.

Expiry or uncertain execution sets the independent quarantine marker; expiry never proves thread termination. Dispatched attempts are quarantined on restart, and their resources remain charged. Cancellation quarantines promptly and retains the download permit until the executor actually drains. Turning the flag off stops new campaigns and does not turn persisted claims into ordinary downloads. Uncertain staging must be retained until recovery establishes drain/fencing. The maintenance API ClaimStore.release_quarantine(key, drained_lease_id=...) requires evidence that the exact executor drained, or an explicit human_reason=...; it advances the durable generation and records the evidence without refunding. After every campaign executor is drained/fenced, BudgetLedger.finish_campaign(..., drained=True) records closure. These are single-writer operations, matching the existing process model in services/concurrency.py; they are not scheduler tasks.

PAPERVAULT_MERGE_V2=1 opts into slice 5 (default off). Duplicate identity consolidation and preprint/journal promotion persist a paired merge intent, refuse new claims on both keys, and wait for queued work, live executors and durable campaign claims to drain. Each checkpoint uses the existing library write lock; it releases the lock between checkpoints so executors can finish. The merge intent projects quarantine for new admission without marking existing execution as uncertain. Dispatched campaign results still pass the normal generation, lease, policy, identity and budget publication checks and settle their claims. Lease-expiry or execution-uncertainty quarantine continues to drop results. The default wait is 60 seconds (MergeStore.quiesce(..., wait_seconds=...)). An expired or uncertain wait stays quarantined and blocked after restart; expiry never permits a merge or refunds uncertain consumption. The operator or orchestrator must decide recovery explicitly.

The canonical record retains the union of restrictions, intersection of grants, and source observations. The absorbed record remains an alias; resolve_alias returns its canonical record. Existing artifacts remain in this vault and are read through retained paths. reopen-budget.jsonl receives an explicit merge event with both keys, generations before/after and drain evidence, before the index is saved. Spent budgets sum, ceilings take the minimum of the affected keys’ campaign caps (the standing 200-paper cap for keys with no history), and generations re-sequence to their sum plus one. A ledger/index disagreement fails closed and requires operator repair; an uncertain commit is never replayed. Disabling the flag retains fences on persisted intents and merged records. Plain metadata fills use their existing path; untouched legacy rows gain no merge fields. Standalone synchronous consolidations use the same write lock; runtime callers must own it on the server’s event loop.

PAPERVAULT_POLITE_V2=1 opts into slice 4 (default off). API sends for Wiley, Crossref, Unpaywall, Semantic Scholar, OpenAlex and arXiv share one process ledger per service, combining credentials, IPs and endpoints conservatively. Every configured sliding window must admit the send, with concurrency accounted separately. A full window, occupied permit, cooldown or unavailable accounting refuses the send; the wrapper does not add retries or waits. With the flag off, the original transport receives the same arguments and returns the same response.

Wiley starts at 1 request/second and 30/600 seconds, below the issue’s documented 3/second and 60/600 seconds. Semantic Scholar starts at 1/2 seconds (the batch module documents 1/second); arXiv’s API retains 1/3 seconds. Each service has concurrency 1. Crossref, Unpaywall and OpenAlex have no numeric provider limit documented in the current code and remain disabled until an explicit envelope is configured and authorized. Retry delays, sweep caps and the existing network semaphore are not provider windows.

PAPERVAULT_POLITE_ENVELOPES accepts a JSON object keyed by crossref, unpaywall or openalex. Each entry needs analogue (wiley, semantic_scholar or arxiv), initial and hard_cap arrays of [requests, seconds], and a nonempty authorization reference. An initial window must correspond to each analogue window, in increasing duration order; its duration may grow, its burst cannot exceed the analogue, and its rate must be at most half the analogue. Caps specify maximum counts and minimum durations for those corresponding windows. Missing fields disable that scope; malformed or changed live configuration fails closed. For example, the shape for a separately authorized envelope based on Wiley is:

{"crossref":{"analogue":"wiley","initial":[[1,1],[30,600]],"hard_cap":[[1,1],[60,600]],"authorization":"owner approval reference"}}

This example records an assumption, not a claim about Crossref’s permission. The pure Ledger.increase(scope, windows) API takes explicitly chosen values: at most +25% in rate per window after 24 hours and fresh, completed 2xx evidence. Counts may increase or durations shorten, within the configured caps. There is no automatic ramp. Configurations, increases, cap freezes, cooldowns and resumes are inspectable through Ledger.snapshot(). A 429, or three timeouts in a rolling 60 seconds, starts a 60-second scope cooldown and stops further speed-up. A late timeout extends the quiet window. After it expires, requests resume at the same limits. Unread/partially closed streams and unproven raw streams without a content length earn no ramp evidence. The ledger is in memory under the existing single-process model; envelope state and request windows do not survive a restart, so restart is not a supported way to clear a live quota or cooldown. Browser/stealth, publisher content, redirects and arXiv page/PDF paths remain for later slices; existing semaphore and cascade behavior is retained.

PAPERVAULT_STOPRULES_V2=1 opts into slice 6 (default off). Reconcile evaluates the rules before routing; download queue rounds and campaign settlement also evaluate them. The detectors consume durable outstanding claims and merge redirects, actual scope send timestamps, and explicit publication evidence. They never interpret an admission refusal or a rejected PDF tier as a violation. Named thresholds in library/stop_rules.py are zero: duplicate in-flight identity halts the campaign; an exceeded quota window cools the scope and halts active campaigns; identity-mismatched publication halts its campaign (or active campaigns for unclaimed work) and quarantines the current record. Publication comparison uses the adopted DOI-first canonical identity and resolves merge aliases. Clearing or repairing a secondary identifier or route leaves a matching canonical identity intact and does not trigger a stop. Verified blank identifier fills keep their existing promotion behavior; the comparison advances only for fills acknowledged by the existing writers. SIGNIFICANT_REGRESSION_FRACTION=0.20 defines the measurement term; it does not enforce a fourth rule without measurements.

Campaign halt is an idempotent append-only ledger event, distinct from verified finish/drain. Replay denies new reservations and dispatches, while existing dispatched work keeps its completion and accounting semantics. Uncertain work stays charged; proven-unsent queued work can settle normally. Scope cooldown uses the same lock and configured quiet window as 429/timeout cooldown; repeated rounds do not extend an active cooldown, and resume never increases the limits. Disabling the flag stops evaluation and new measurement fields; persisted campaign halts continue to fence admission.

Enabled manifest events add identity_version (v2 identity definition), attempt_generation, lease_id and an opaque request_scope_id, preserving existing fields. Unknown correlation values are null; request scope is distinct from the campaign’s logical scope. Wait, executor/request service, and actual executor drain have separate duration fields. Incomplete attempts are recorded as censored, with null unobserved durations, and never back-filled with an estimated latency or outcome. admission-denied records refused sends; verified-success requires positive identity verification and live publication, so verification fail-open and temporary campaign stages do not count as success.

URL identity backfill

For records with a URL but no DOI or arXiv ID, inspect a bounded sample without writing to the vault:

python -m papervault.library.cli identity-backfill --cap 20 --json

Recovery checks DOI/publisher and arXiv URL patterns, then one citation-metadata landing fetch (including its redirect target), then exact ADS, CORE, OpenAlex or INSPIRE metadata. DOI registry metadata or the current arXiv page must pass the existing title/author consistency screen before an identifier is filled. Missing, blocked, ambiguous or conflicting evidence causes abstention. Existing identifiers and collisions are left intact; successful fills retain route, URL, verified title and UTC observation time in identity_recovery.

In an authorized maintenance window after deployment, add --apply --acquire to persist identities and immediately run the ordinary current-only cascade on only the newly recovered rows without existing assets. --keys FILE restricts selection to listed citation keys (one per line). --cap is required, accepts 1–50, and bounds both inspected and acquired records; unrelated terminal sets are never reset. Writes refuse while papervault.service is running. The JSON result and library manifest record recovery counts by route, acquisition outcomes, and successful sources (including the winning member of aggregator groups). Report that post-deploy pass’s numbers on issue #197.

The standing correction pipeline uses that same verification/application path with acquisition disabled. Notify it explicitly after a credential becomes available or an affected source is repaired/deployed, in an authorized maintenance window:

python -m papervault.library.cli correction-event credential_arrived --source ads --cap 20 --apply
python -m papervault.library.cli correction-event source_repaired --source core --cap 10 --apply
python -m papervault.library.cli correction-event identifiers_arrived --source ads --keys affected.keys --cap 10 --apply

These commands are the operator notification points; they do not watch credentials or poll source health. Source adapters can use the same async run_event interface with the running library. identifiers_arrived requires explicit stored citation keys whose source identifiers or locator URLs gained evidence. Each event intersects keys with its source cohort, then caps selection at the required --cap (1–50). Without keys, only identifierless rows and unrecorded verified routes consume the cap. Known identifiers remain intact. The standing pipeline applies DOI recoveries; non-DOI claims are recorded as deferred because #198’s lower identity ladder is TBD. Requests are serial, with at least one second between inspected records and sweep cohorts; the reviewed source helpers retain their timeouts, limits and cooldowns.

Library background boot owns a monthly correction task. Its durable due time has a 30-day floor, survives restart, and starts 30 days after first activation. An operator may run the first sweep earlier with correction-sweep --apply; subsequent sweeps respect that floor. Each cohort defaults to 20 selected papers (hard maximum 50); the whole sweep defaults to and cannot exceed 100. Both citation-key and cohort cursors rotate so capped misses and later cohorts progress. CORE starts last per its measured ceiling. Override the defaults downward with --per-cohort-cap and --total-cap. The constants and validation live in services/correction_pipeline.py; the reviewed backfill’s record ceiling remains in services/identity_backfill.py.

correction-ledger.json, beside the vault’s index.json, keeps conflicts, immutable batch manifests, adjudications, route findings, scheduling and monthly aggregates. The library manifest records every pass and monthly report. Merge/enrichment and verified-identity claim hooks stage conflicting fields with both original sides and provenance; they retain existing fill-blanks behavior. The backfill still never merges or purges a collision (the older title-based DOI resolver keeps its existing merge behavior, with field conflicts staged before merging).

At ten distinct pending subject papers, one atomic ledger write emits and assigns a manifest for an orchestrator-dispatched Codex CLI gpt-6-luna / max agent. Each manifest contains ten papers and at most fifty conflicts. No agent is launched by the library. Export its actual evidence with correction-report --kind manifests --manifest ID. Import an investigation using correction-adjudicate results.json --apply, where JSON contains manifest_id and results entries with conflict_id, status (resolved or needs_human), decision, and evidence. Resolved answers require evidence; omitted/unadjudicable answers become retained needs_human. Identical replay is idempotent; foreign/duplicate answers fail atomically. Adjudication records a decision and does not apply field changes. Those require separate scoped authorization. correction-report --month YYYY-MM reports aggregate counts and current unresolved dispositions; --kind conflicts exports the retained evidence. All report exports are read-only; maintenance writes refuse while the service is running.

Verified stored ADS full-text observations are copied into the route ledger. A #205 survey packet may also be supplied to a key-scoped ADS event using --routes FILE: a JSON list of {key, url, source_identifier, provenance} objects, with the exact stored ADS bibcode and provenance containing timezone-aware checked_at, verification: "remote_pdf", and the survey’s evidence (for example issue: 205 and status_code: 200). Imports require selected keys and at most three routes per paper. correction-report --kind routes is the #174 scoped re-open’s selection interface. The correction pipeline downloads no assets and changes no terminal states.

Download telemetry is appended to the same manifest by ordinary acquisitions. Existing outcomes retain their fields and gain timing: a 32-character run_id, UTC at, and, inside a tier slot, slot_id, slot_started_at, slot_ended_at, and monotonic slot_duration_s. These slot endpoints describe elapsed time at outcome emission; use the corresponding completed tier span for the full slot including final bookkeeping.

Additional download_telemetry records share run_id, paper key, actor (download or competing search), UTC at, process/thread IDs, and phase/mark. Spans have start and end marks with span_id, parent_span_id, started_at, ended_at, monotonic duration_s, and control-flow status (ok, error, cancelled). Status ok means the operation returned; tier success remains the existing outcome event. Phases cover queue_worker, network_wait, network_hold, actual executor work, cascade, arXiv preflight, tier, strategy, verification, pdf_save, firecrawl, parallel group/member, and browser_call. enqueue/dequeue points pair by queue sequence and run ID; dequeue includes enqueued_at, queue_wait_s, worker ID and priority. queue_drop points close stale priority-promoted tuples with their sequence and dequeue time. Cascade end carries paper_status, outcome_source and pdf_returned; group winner points precede executor drain. Mirror members use indices, never target URLs. Telemetry stores scalar fields only, bounds labels to 64 characters and keys to 128, and retains only one admission stamp per queued tuple. New-record failures emit a warning and do not change acquisition results. Manifest writes serialize complete JSONL records.

The semaphore spans describe whole cascade/search admission, including browser, verification and executor drain; member fan-out can issue multiple requests per permit. Browser-call spans observe the exposed fetch API, not process creation/exit, PSS/RSS or isolated remote latency. Cancellation can release a permit before the executor thread ends; measure both timelines. Partial spans at capture boundaries must be censored. Request counts/status/rate headers and same-window host process memory measurements remain necessary to validate upstream limits and resource costs. This instrumentation changes neither the four workers nor the four semaphore permits.

Releases

papervault is consumed by pin (ADR-0001): deployments track an annotated git tag (vX.Y.Z[-stage]), never main.

The repo carries one version, and every change PR bumps it per the semver call stated in its description. The declared version fields move together in that PR: pyproject.toml [project].version plus the papervault entry in uv.lock (refresh with uv lock), plugin/.claude-plugin/plugin.json version, and the papervault entry’s version in .claude-plugin/marketplace.json; tests/test_plugin_version_sync.py fails when they drift.

A release IS a tag — CI’s build job produces the wheel/sdist for every commit, so tagging is the whole ceremony: the tag names the version already on main (vX.Y.Z, plus an optional -stage suffix), and a release commit changes no version field. Latest tag: v0.1.6-beta (closed beta), cut before the one-version rule, so its number predates the package version.

License

MIT — see LICENSE.

View this README on GitHub

Install

This server does not publish a one-line install command.

Open the repository installation guide