Changelog¶
All notable changes to the papio daemon and CLI are documented here, keyed
to v* release tags. The browser extension is versioned and released
independently (ext-v* tags): from extension 0.3.1 onward its changes live in
extension/CHANGELOG.md. Through [0.3.0] the two shared a version stream,
so older sections below include extension entries. The initial release entry
is synthesized from the complete papio and zotio Git histories and the
execution records in notes/acquisition-stack-plan.md.
[0.10.1] - 2026-07-23¶
Fixed¶
- Direct-distribution release manifests now record the bundled zotio version, rather than incorrectly repeating the papio release version.
[0.10.0] - 2026-07-23¶
Added¶
- Zotero exception-tag ledger (
zotio.exception_tags, off by default): the daemon now answers "is this item coming, or is it mine now?" inside Zotero itself by maintaining two reconciled automatic tags on linked items —papio:needs-action(acquisition parked on a human action) andpapio:unavailable(OA and institutional routes exhausted, as of the last attempt). Desired state combines each item's newest job with a live exact-key attachment check, so a PDF attached manually clears the marker. Schema v14 rolls the v13 applied-state ledger forward with personal-library provenance plus pending/owned/foreign/missing-target state; per-item mutations survive partial failures, serialize concurrent passes, and never claim or remove a same-name manual tag.papio zotio tags reconcileruns one pass on demand. Requires zotio ≥ 0.13.0 (items tags add --automatic,items tags remove --automatic-only); personal library only. Turning the feature off cleans up papio-owned tags. Design record: zotiodev/adr/0004. - Backfill re-checks
unavailableitems after a cool-down (zotio.unavailable_recheck_days, default 14) instead of retrying them on every watch cadence — availability drifts upward (green-OA deposits, holdings changes, new adapters), and apapio:unavailablesaved search doubles as an ILL/manual worklist only if it neither rots nor flaps.
Changed¶
papio initautodiscovers more of its own answers: the zotio executable is resolved from PATH and labeled with its source, prompt defaults name where they came from (keep currentvsfrom Zotero), the Browser step names the supported browsers and lists those detected on the machine, the Chrome extension ID prompt and--extension-idaccept an unpacked extension folder path and compute the ID from it (macOS/Linux; Windows requires the literal ID), and the browser setup instructions are suppressed when doctor reports a healthy, current extension connection.
Fixed¶
- Upgraded
golang.org/x/textto 0.39.0, fixing GO-2026-5970 (an infinite loop on invalid normalization input reachable through PDF inspection).
[0.9.2] - 2026-07-22¶
Fixed¶
papio doctorno longer fails against a healthy daemon whose daily update check has results: the strict ping decode now accepts the optionalupdate_available/latest_versionfields (and their zotio counterparts).
[0.9.1] - 2026-07-22¶
Changed¶
- Doctor's extension check is floor-aware: a connected extension below the
daemon's minimum version now WARNs with the exact skew and remediation
instead of reporting a bare
connectedPASS while every handoff refuses. papio init's guided flow was redesigned: numbered step sections with one-line explanations, one question per prompt, a single[Y/n]grammar, middle-ellipsized long defaults, per-browser setup instructions, a doctor summary that shows only checks needing attention, and a closingNext:action. Behavior and flags are unchanged.
[0.9.0] - 2026-07-22¶
Added¶
- Institution discovery in
papio init: paste a library discovery URL or use the resolver configured in Zotero to derive the OpenURL base; scripted setup supports--institution-url. papio adapter diagnose <job-id>— sanitized, paste-safe support report for provider/adapter failures (job policy, actions, redacted event timeline, extension connectivity).- Library-completeness line in
papio status: when zotio is configured, status leads withLibrary: N item(s) missing PDFs — papio acquire --from-zotio fills them(orLibrary: complete), backed by a newzotio.missing_countRPC over zotio's local mirror. papio doctor --start: opts doctor into the same daemon autostart every ordinary command uses, so a first run can bring the stack up instead of reporting it down. Default behavior still diagnoses a stopped daemon.papio initdefaults the Chrome extension ID to the Chrome Web Store package (npccengdhjmpojpjmjoeeclpdhcjelhf) — a store install needs no ID copying; only unpacked development builds still paste theirs.- Triage snapshot schema 2 exposes institutional sign-in/open-access guidance only for actions classified by the acquisition waterfall; schema 1 remains available unchanged for older browser extensions.
- Standard bibliographic ingestion for
acquire --batch: RIS, BibTeX, CSL-JSON, and MEDLINE/NBIB use one robust interchange pipeline for reference managers, database exports, and systematic-review tools. - Provider compatibility matrix: a hand-maintained page records the individually live-verified provider routes, adapter status, and limitations without inventing aggregate success rates.
Changed¶
- Doctor's daemon-unreachable report collapses the four dependent SKIP lines
into one
integrationsline and names the actual problem ("not running or unreachable") with the--startremediation, instead of a six-line cascade for a stopped daemon. - Opener failures in
papio actions opennow name the failing command even when it produced no output (no more bareexit status 1). - Getting-started, README, and troubleshooting now document the Chrome Web Store install as the default path; unpacked loading moved under a development-install tab. Firefox remains a temporary add-on until the AMO listing clears review.
- BREAKING: Access mode
maximalis nowdelegatedin the config value,--access-mode, and wire enum. Existing configs must be updated by hand; stored job policies migrate automatically.
[0.8.0] - 2026-07-21¶
Added¶
[hooks] on_ready— a generic post-acquisition hand-off: when a job reachesready, the daemon runs a user-configured shell command once with the job's metadata inPAPIO_*environment variables (PAPIO_DOI,PAPIO_PDF,PAPIO_TITLE, …). Fire-and-forget with a durablehook.on_readyjob event as the audit trail; file acquisitions into papis, Calibre, a plain folder, or any script without a bespoke integration (ADR-0004). zotio remains the deep Zotero path.- zotio is now optional: an empty
zotio.executabledisables the deep Zotero integration instead of rejecting the config. Ownership lookup degrades to not-owned with a staleness warning sobatch submitand watches keep working,doctorreports zotio as "not configured (optional)", andzotio.auto_import = truestill requires the executable. - Browser session arbitration (
papio browser sessions/papio browser use <id>|--latest, +browser.sessions/browser.claimRPCs): with papio installed in more than one browser, exactly one session now holds the offer/handoff flow. Later hellos wait as pending (session_busy) instead of silently stealing the session; a holder silent for 10 s yields to a live pending session; a cleanly closing browser releases immediately. Session identity rides the daemon↔native-host envelope, so extensions of every version participate.papio statusanddoctorreport waiting browsers and denied hellos, and the extension-version flap between competing browsers is gone (ADR-0003). - Handoff actions say whether a login is needed: human actions carry
requires_authandblocked_by(anti_bot,paywall,landing_page),papio actions listprints "open access — no login needed" vs "sign in to your institution first", and batch reports classify handoffs from the structured fields (schema v11 migrates and backfills existing parked actions). Thejob_offerprotocol message carriesrequires_authfor the extension. - Open-access sibling fallback: when a DOI's own record yields no legal candidate — or every candidate it did yield fails to fetch — the OpenAlex resolver searches for open-access sibling versions (preprints or repository copies under a different DOI) with strict title/year/author matching and tries those before parking the job — a paywalled paper with a free SSRN/arXiv copy no longer requires a manual re-search.
- OpenAlex works without an API key: the resolver now runs in OpenAlex's
free polite pool with just the configured contact email — matching the
search/discovery client — and treats
sources.openalex.api_keyas optional premium capacity.doctorreports the polite-pool state instead of failing. - Stale-SSO handoff recovery: the daemon records
browser.handoff_offeredandbrowser.handoff_failedjob events (newhandoff_outcomeprotocol message), and institutional handoff guidance now says to sign in first and simply re-open on a stale-session error — every open mints a fresh link. importedterminal job state: a successfulzotio apply(manual or auto-import) advances the jobready → importedwith the Zotero item keys on the transition, sojobs listandstatusstop presenting filed work as actionable; batch reports read the keys from the job itself.--labelnow works on singlepapio acquire(not just--batch), seeding the target collection when--collectionis unset.
Fixed¶
papio actions openfailures now name the URL and underlying error (with apapio doctorhint) instead of a bareexit status 1, and explain when open actions exist but none are openable from the CLI.
[0.7.2] - 2026-07-20¶
Fixed¶
- Scheduled backfills now scan the complete zotio missing-PDF queue and skip already-live request IDs without consuming the per-run cap, preventing one persistent entry from starving later work.
- Alert-watch digests now retain consumed identities as a durable seen set, merge title, arXiv, OpenAlex, and DOI aliases, preserve author names losslessly, recheck zotio ownership before acquisition, and serialize clear and acquire operations.
- Long failure reasons remain separate groups until after display truncation, so unrelated failures with a common prefix no longer collapse together.
- Semantic Scholar citation and reference snowballs now enforce year and open-access filters and reject ambiguous query-plus-snowball requests.
[0.7.1] - 2026-07-20¶
Fixed¶
- Browser-download adoption now waits for Firefox
.partwrites and their zero-byte target placeholders to settle before validating the final file, preventing valid PDFs from being quarantined mid-download. - Accepting an identity review for a browser-adopted PDF now binds the override
to that file's SHA-256 candidate key, so the scheduler's safe re-resolution
path reaches
readyfor the same bytes instead of parking the same PDF for review again.
[0.7.0] - 2026-07-20¶
Added¶
papio jobs failures(+jobs.failuresRPC): groups jobs that did not complete on their own (failed, unavailable, needs-review, awaiting-human) by state, provider host, and terminal reason, with--since(Go durations,Nddays, or RFC3339) — so adapter and resolver work can be prioritized from real failure data.papio acquire --from-digest <watch> [--keys ...](+watch.digest_acquireRPC): queues pending alert-watch digest entries as acquisition jobs through the same submission path watches use, preserving the recorded identifiers so deduplication holds; entries are removed only after their manifest is persisted and submission succeeded.papio watch digest clear <id>(+watch.digest_clear) empties a digest.- Structured webhook notifications: webhook payloads now carry
event,watch_id,watch_label, andcountalongsidemessagefor routable receivers; plain notices keep the exact previous{source, message, sent_at}shape. - Click-to-acquire support: the daemon accepts the new
page_acquirebrowser-protocol message (advertised via thepage_acquirehello_ackfeature) and queues the page's DOI through the normal acquisition pipeline, answering withpage_acquire_ack(job id or duplicate marker).
Fixed¶
papio acquire --from-digest --keysnow treats every--keysvalue as one opaque work key: keys containing commas (title-derived digest keys) are no longer split into fragments that miss valid entries. Repeat the flag for multiple keys.papio jobs failures --sincerejects negative durations (-1h,-1d) instead of silently computing a future cutoff that hid every failure.
[0.6.0] - 2026-07-20¶
Added¶
- Backfill watches:
papio watch add --kind backfillschedules the existingacquire --from-zotioqueue on a cadence, so a growing Zotero library steadily self-completes its missing PDFs. Runs are bounded by--limit-per-runand idempotent (deterministic per-item request IDs), and the watch is force-runnable withpapio watch runlike any other. - Alert-only watches:
papio watch add --mode alertruns the scheduled discovery search and library-ownership filter but reports new works instead of acquiring them. New finds are recorded once per watch (re-runs never re-report) and browsable with the newpapio watch digest <id>command; notifications point at the digest. - Webhook notifications: a new
notify.webhook_urlconfig field delivers every daemon notification (watch results, human-action handoffs, imports) as a JSON POST — Slack/Discord/ntfy-style receivers work out of the box — in addition to the local desktop channel. Optionalnotify.webhook_secretis sent as a bearer token. Delivery is best-effort and never fails the work that triggered it. - Semantic Scholar discovery backend: discovery is now pluggable behind a
source seam.
discovery.sources = ["openalex", "semanticscholar"]in config fans searches (and watches) across both backends with DOI/title deduplication in preference order;papio search --sourceselects one explicitly. Citation snowball (--cites,--cited-by) is supported on both; arXiv-only Semantic Scholar results now carry their identifier through to acquisition. API key (optional) lives atsources.semanticscholar.api_key.
Changed¶
- The MCP read resources (
papio://jobs,papio://artifacts,papio://exports, …) now return{"<name>": [...], "truncated": bool}envelopes instead of bare arrays, making the 100-row cap honest. Filtered and paginated access remains available through the command facade (jobs list --state --limit).
[0.5.0] - 2026-07-19¶
Added¶
- Broader browser reach for the native-messaging connector.
papio native-host installnow registers the host with every installed Chromium browser it detects — Chrome, Edge, Vivaldi, Brave, Opera (Chromium too) — plus Firefox, each at its own per-user location (directory on macOS/Linux, registry key on Windows), so the same extension works across them. A newbrowser.extension_idsconfig field lists additional Chrome-family extension IDs (e.g. an Edge Add-ons build) alongsideextension_id; the daemon accepts any of them and the manifest'sallowed_originslists them all.
Fixed¶
A triaged audit pass (33 confirmed findings fixed, each with a regression test where behavior changed):
- OA resolver identity verification: CORE, Europe PMC, and OpenAlex title searches now verify the normalized title — plus publication year and the full author list whenever the request supplies them — before trusting a result; Unpaywall requires the returned DOI to match the requested one; arXiv compares exact version-stripped IDs instead of substring matching. Cuts wrong-paper acquisition risk across the discovery plane.
- PDF identity matching scopes DOI and supplementary-material signals to the document's front matter, so a bibliography citing other DOIs — or a body mention of "supplementary material" — no longer rejects a correct article.
- Download safety: caller-supplied headers (Authorization, API keys) are stripped on cross-origin and HTTPS→HTTP redirects, and the body-reader goroutine no longer leaks when a response ignores cancellation.
- Storage integrity: bundle export and Zotero plan staging copy artifacts instead of hard-linking the immutable store (a consumer mutating the copy could corrupt it); concurrent same-hash promotions converge atomically; failed exports roll back the files they created; failed SQLite backups no longer strand a partial destination file; promotion and backup fall back gracefully on filesystems without hard-link support.
- Concurrency: RPC calls on separate IPC connections no longer serialize daemon-wide behind one slow call; the browser bridge releases its session lock during PDF validation on download adoption; the serial auto-importer releases its lock during retry backoff; concurrent zotio plan applies of the same plan are now mutually exclusive, and a claim abandoned by a crash or cancellation heals after a 15-minute lease instead of wedging the plan.
- Job lifecycle: context cancellation during auto-import stays retryable
instead of recording a permanent failure; crash recovery clears abandoned
quarantine files and the quarantine sweep continues past individual cleanup
failures; a validation-persistence failure can no longer orphan a
just-promoted artifact; watch-discovered works keep their OpenAlex
identifier; pending notifications flush on a timer instead of waiting for
the next event;
pinganswers from cache instead of blocking on the daily update check. - Protocol strictness: strict JSON decoding rejects trailing documents; batch submissions reject unknown fields; Zotero item keys are validated fail-closed at the zotio integration boundary (the published v1 protocol contract is unchanged); batch identity hashes widened from 32 to 128 bits, with manifests from earlier releases still readable.
[0.4.0] - 2026-07-19¶
Added¶
- First-class Windows support. The daemon's local RPC runs over a named pipe on
Windows — restricted to the current user via an explicit SDDL, the analog of
the Unix socket's
0600— while macOS and Linux keep their Unix-domain socket; the transport is chosen at build time.papio init,papio native-host install/uninstall/status, andpapio doctorregister the browser connector through the per-user registry (HKCU\Software\...\NativeMessagingHosts) instead of a manifest directory, and — because Windows has no unprivileged symlinks — install a copy of thepapiobinary as the native host (rerunpapio initafter upgrading to refresh it). Configuration lives at%APPDATA%\papioand data at%LOCALAPPDATA%\papio, and the update hint recognizes Scoop (scoop update papio). macOS and Linux behavior is unchanged.
[0.3.0] - 2026-07-18¶
Added¶
-
Automatic, institution-agnostic library-resolver access. When a library's OpenURL resolver shows a "full text options" menu instead of direct-linking to the provider, papio follows the institution's top-ranked electronic service link itself — gated on a host permission for that resolver origin. The daemon advertises its configured resolver origins in the
hello_ackhandshake (new optionalresolver_origins, backward compatible withinpapio-browser/1); the extension requests exactly those origins, so the popup surfaces a one-click "Allow library access" prompt (and the toolbar badge counts them) whenever a configured resolver isn't granted yet, and the options page lists the user's own resolvers under "Your library". Custom resolver domains outside the built-in Ex Libris hosts are reached through an optionalhttps://*/*pattern that is never granted in bulk — only the exact configured origin is ever requested. Institution identity lives only inconfig.toml, never in extension code. -
Update discovery, without auto-install and without silent network calls. Store-delivered extension builds are stamped with the daemon version they shipped with, so the popup can show a calm "papio X.Y is available" line when the connected daemon is older — papio itself performs no network activity for this. Separately, an opt-in
[updates] check = truesetting (offered by thepapio initprompt, default yes) has the daemon consult the papio and zotio GitHub releases APIs independently at most once a day. papio status appears in daemon status; both targets surface inpapio doctorand a once-daily stderr hint. Configurations without the setting never check. -
Version-skew awareness across every surface. The
hello_ackhandshake now carries the daemon's version and a feature list (optional, backward compatible withinpapio-browser/1), so the auto-updating extension can degrade gracefully against an older daemon instead of failing opaquely. The popup reports daemon health directly: a quiet version line when healthy, and actionable states for daemon-unreachable, daemon-out-of-date, and extension-out-of-date; the toolbar badge shows!when attention is needed and stays clear otherwise. The options page footer shows extension and daemon versions at a glance. The daemon records the connected extension's version and rejects extensions below a minimum floor with a clear update-the-extension message. -
papio doctornow walks the whole integration chain in one report: the Phase-1 readiness checks (config paths, database, PDF tooling, credentials) followed by integration checks — daemon reachability and version match, browser-extension connectivity, native-messaging-host manifests for Chrome and Firefox, and the zotio preflight — each failure with a concretefix:line. The same diagnostics are exposed to agents as a read-onlypapio_doctorMCP tool. -
Every CLI command now warns on stderr (once per invocation, never on stdout) when the running daemon's version differs from the CLI binary, with the exact recovery command.
-
Release engineering:
release_metadata.py compatmechanically verifies the cross-artifact compatibility floors (daemon↔extension minimums, zotio minimum version, extension manifest/package version agreement) as arelease.shstep and a source-only CI check;release.shnow also packages the Firefox extension archive alongside the Chrome one. A shared release runbook lives at.agents/skills/papio-release/SKILL.mdand is cross-referenced from zotio. -
Extension store submission path for Chrome Web Store and Firefox Add-ons (AMO).
extension/scripts/submit-firefox.shsigns and submits the built Firefox package viaweb-ext(AMO API credentials fromextension/.env), andextension/scripts/submit-chrome.shuploads the Chrome package viachrome-webstore-upload-cli; both are exposed asbun run submit:firefoxandbun run submit:chrome. Paste-ready store listing kits (name, summary, full description, per-permission rationale, data-collection disclosure, and reviewer build instructions for the bundled source) live atextension/docs/amo-listing.mdandextension/docs/chrome-web-store-listing.md. -
The bundled-zotio compatibility floor (
internal/zotio/client.goMinimumVersion) now targets a released zotio line (0.10.0) instead of an unreleased1.0.0; a built zotio 0.10.0 satisfies every capability, operation, and write-target papio's preflight requires.release.shnow stamps the bundled zotio binary with zotio's own version rather than papio's, so the cross-artifact compatibility check reflects the real zotio being shipped. -
Documentation: a
Version skew and updatestroubleshooting section (update flow, popup states, config-newer-than-binary errors), sister-project cross-references between papio and zotio in both READMEs and docs, and regenerated command reference. -
The MCP
papio_statustool now surfaces the same actionablecategoryandguidanceas the CLI for parked and no-file jobs (including the config-awareinstitution_not_configured), so agents driving papio over MCP get the same diagnosis and next step as a human. The category catalog moved to a sharedinternal/errcatpackage consumed by both the CLI and the MCP server, so the two surfaces cannot drift. -
papio initnow captures the browser extension IDs during first-run setup, so the native messaging host installs on the first run instead of failing withbrowser.extension_id is not setand forcing a config hand-edit and re-run. The Firefox add-on ID defaults to the built extension's fixed gecko id (papio@orgmentem.com) so Firefox works out of the box; the Chrome ID is prompted (paste the value fromchrome://extensions). New--extension-idand--firefox-extension-idflags cover non-interactive setup. Unit-tested (flag and interactive paths, including that the captured Chrome ID reaches the native-host install) and smoke-verified end to end. -
Actionable error categories in
papio status. Every parked or settled-without- a-file job now shows a short, stable category and a one-line next step instead of a raw internal reason (or nothing, for failed/unavailable). The catalog is config-aware: a job that found no copy under assisted/maximal mode with no institution configured surfaces asinstitution_not_configuredpointing atpapio init, rather than a silentunavailable. Categories/guidance are added to the status JSON (category,guidance) for agents. The same category and next-step now print underpapio acquire --waitwhen a job parks or settles without a file, and the desktop human-action notification tells the user torun papio status to see whyinstead of a bare count. Unit-tested; the status view andacquire --waitguidance are smoke-verified against the live daemon. -
Per-institution access profiles and guided institution onboarding. Named resolver profiles under
[browser.resolvers.<name>]are now full institution tables (openurl_base_urlplus optionalshibboleth_entity_idandproquest_account_id), so a multi-institution user routes each job's login to the right library. This lifts the earlier "default profile only" limitation on federated login-routing and the ProQuest account-id unlock: the daemon now wireslogin_entity_id/proquest_account_idper selected profile, and a named institution never inherits the default institution's identity.papio initgained an "Institution" step (and--openurl-base,--shibboleth-entity-id,--proquest-account-idflags); the ProQuest prompt accepts a pasted resolver URL and extractsaccountid=for users who don't know their numeric id. Config validation, per-profile offer wiring, and the account-id extractor are unit-tested; the interactive flow is smoke-verified end to end. Older single-base configs keep loading: a resolver profile may still be a barename = "https://…"string (shorthand foropenurl_base_url), so no config migration is required. -
ProQuest account-id unlock: on ProQuest's "Find your institution" wall, papio appends
?accountid=<id>to the current URL, which unlocks Example University's institutional access with no sign-in at all (verified live 2026-07-18 — resolves the wall cold, "Access provided by EXAMPLE UNIVERSITY"). New per-institution config[browser] proquest_account_id(digits); the daemon passes it as the optional job-offer fieldproquest_account_id(default profile only); the ProQuest adapter gainsaccountIdParam: "accountid", and on aloginverdict papio appends it (latched, once) — preferred over the federated route since it needs no credentials. This is the fix for the ProQuest openurl-handler blocker (the Shibboleth-DS route authenticated only ProQuest's main context, not the link-resolver handler). Config + protocol + adapter + bridge are unit-tested; the full download still needs a live pass on a ProQuest-held title. -
Institution auto-selection ("login routing"): on a provider login wall, papio navigates the handoff tab straight to the institution's federated login, skipping the provider's institution picker — selection is deterministic config (which institution you're at), not a secret, so only credential entry stays with you. New per-institution config
[browser] shibboleth_entity_id(the Shibboleth IdP entityID, e.g. Example University'shttps://idp.example.edu/entity); the daemon passes it to the extension as the optional job-offer fieldlogin_entity_id(default resolver profile only, to avoid mis-routing another institution's job); and an adapter gains an optionalfederatedLogintemplate ({entityID}placeholder). On aloginverdict papio navigates once (latched) to<federated-login>?entityID=<configured>. ProQuest ships the route (verified live 2026-07-17: Example University's entityID via ProQuest's Shibboleth DS URL routes straight toidp.example.edulogin, skipping the WAYF picker). Config + protocol + adapter classify + bridge routing are unit-tested; the full post-sign-in download on a ProQuest-held title still needs a live pass. -
ProQuest institution-wall handling (
proquestadapter v0.2.0): aloginclassify rule (ordered beforearticle) now recognizes ProQuest's "Find your institution" wall (form#institutionForm+input#institutionName, fixture-backedfixtures/proquest/login-return.htmlcaptured live via CDP). papio surfaces it as a human sign-in step (login→auth_pending) instead of silently staying assisted/unknown. Matters disproportionately because Example University's OpenURL resolver routes many titles (incl. SAGE/T&F journals) to ProQuest rather than the publisher. Classify verified by fixtures; the full post-sign-in download recovery (authenticate ProQuest → re-drive → entitled docview → download) still needs a live pass. -
SAGE Journals adapter (
journals.sagepub.com), fixture-backed (fixtures/sage/success.html, captured live via CDP from a Example University-authenticated article). SAGE emits no Highwire metas; classifies onpublication_doi+ thedownloadPdfUrlanchor (same shape as ACM) and downloads that anchor's/doi/pdf/<doi>?download=truehref. Classify is fixture-verified; the end-to-end download is not yet live-exercised because Example University's resolver routed the SAGE test title to ProQuest rather than sagepub (the adapter fires when a title routes to journals.sagepub.com). -
Wiley Online Library adapter (
onlinelibrary.wiley.com), fixture-backed (fixtures/wiley/success.html, captured from a Example University-authenticated article). Classifies via the Highwirecitation_pdf_url/citation_titlemetas, then builds and fetches Wiley's direct/doi/pdfdirect/<doi>?download=truefile through the privileged downloads API —citation_pdf_url(/doi/pdf/) and the/doi/epdf/link both return an HTML viewer wrapper, onlypdfdirectreturns the file (verified live end-to-end: 1.15 MB PDF →ready). Closes the gap where Wiley pages classifiedunknownand stayed assisted (browser- agnostic; affected Chrome too). tandfonline/psycnet remain unimplemented — permissioned but not yet fixture-backed (both paywalled in the dev session; psycnet also emits no standard metadata). -
Firefox dev loop:
bun run devrunsbuild.ts --watch(rebuildsfirefox/on anysrc/,icons/, ormanifest.jsonchange) alongsideweb-ext run, which hot-reloads the add-on in a dedicated Firefox Developer Edition instance.web-ext-config.mjspins an absolute, gitignored dev profile (.ff-dev-profile) so permissions and institutional logins persist across reloads — and, being path-based, boots straight in without Firefox's profile-chooser modal. web-ext installs and hot-reloads over the devtools RDP (not WebDriver/Marionette), so it does not setnavigator.webdriver— but that live RDP connection makes Firefox show its remote-control indicator and is itself an automation surface a bot wall could fingerprint. Two modes:bun run devfor fast iteration and fixture testing; for real Cloudflare- walled providers,bun run buildthen loadfirefox/manually viaabout:debugging(one-shot install, no persistent connection, no indicator,navigator.webdriverfalse). -
Brand: a papio logo — an oblique lowercase p (coral
#E85D4A) inside a broken ink ring (#2B2D42); the p's descender becomes a download arrow that exits through the ring's bottom gap. Structural sibling of the zotio badge with its own palette. Vector sources live indocs/assets/(logo.svg,logo-dark.svgfor dark surfaces,logo-tile.svgfor theme-agnostic toolbar icons,logo-wordmark.svg,logo-wordmark-dark.svg) and are used in the README wordmark header, the docs site logo/favicon (mkdocs.yml), the Chrome extension toolbar/action icons (extension/icons/, wired inmanifest.json), and the extension popup header. - Brand: the README header wordmark (
logo-wordmark.svg,logo-wordmark-dark.svg) is now an animated SVG. The mark builds in on a calm ~10s loop — the broken ring draws on, the coral p and download arrow drop into place, the wordmark rises in — then a cheeky little papio (baboon) head peeks over the wordmark to blink, tilt, and wave before ducking away, leaving a long clean hold on the finished logo. Pure CSS (no script/SMIL, self-contained for GitHub's<img>rendering); the resting state is byte-for-byte the prior static logo andprefers-reduced-motion: reduceshows it with no animation. - Background work window: papio now does its browsing in one dedicated
minimized, unfocused Chrome window instead of the user's tab strip. Every
broker handoff tab (first, queued, and download-fallback) and the keepalive
tab route there; provider-spawned viewer tabs inherit it via their opener.
A tab surfaces (window restored + focused, tab activated) only when the
human is needed: on the IdP transition (
auth_pending), on keepalive reauth, and from the popup's Focus button — which now also restores a minimized window. Opt out anytime via the options page ("Keep papio tabs in a background window"); disabling restores the legacy visible-handoff behavior, as does any runtime withoutchrome.windows. - Firefox support, day one:
bun run buildnow emits a second complete extension atextension/firefox/(MV3 event-page background as a classic iife bundle,browser_specific_settings.gecko.id = papio@orgmentem.com,strict_min_version 128) generated from the samemanifest.jsonsource of truth. The native-host installer registers a Firefox manifest (allowed_extensions, MozillaNativeMessagingHostsdir) alongside Chrome's when the new[browser] firefox_extension_idconfig is set, and the host accepts Firefox's bare-ID invocation with the same exact-match, fail-closed validation as Chrome's origin. The options page gained a "Library resolver access" grant section because Firefox treats MV3host_permissionsas runtime-optional; on Chrome it simply shows the install-time grants. No behavior change for Chrome users. The provider section also gained "Grant all providers" / "Revoke all" — one click issues a singlepermissions.requestfor every publisher origin (one Firefox doorhanger) instead of ten separate grants.
Changed¶
- Rewrote the README on the zotio template: centered wordmark + tagline +
badges + docs nav, a "Why papio" section with the hard boundaries, a
hand-drawn two-row serpentine architecture diagram in the brand palette
(
docs/assets/architecture.svg+-dark.svg, theme-switched via a<picture>element; replacing the mermaid flowchart, which rendered poorly on GitHub) with the access-mode table, the research loop, validation/provenance and zotio-boundary sections, the MCP tool surface, and install paths (brew, scoop, signed releases, source). Brand style: papio italic in prose, zotio plain. - Redesigned the wordmark's baboon cameo: the abstract head is now a recognizable hamadryas baboon (cape mantle, long muzzle, heavy brow) that peeks up holding a stack of papers instead of waving. Light and dark wordmark variants stay in sync; in dark mode the paper stack renders navy against the cream mantle for contrast, and the face details (eyes, brows, muzzle) stay navy on the coral face in both modes.
- Config unknown-field errors now explain that the config was likely written for a newer papio and name the offending fields, instead of surfacing a raw TOML parse error. zotio preflight failures name the installed version, the configured executable path, and the action that fixes the mismatch.
- MCP tool surface now derives from the papio CLI command tree instead of a
parallel set of hand-maintained typed tools, so the CLI is the single source
of truth and the two can no longer drift. The default surface is a command
facade —
papio_command_searchto discover commands andpapio_command_runto execute one (JSON output, command-local flags only, inherited globals rejected);PAPIO_MCP_SURFACE=mirrorinstead exposes onepapio_<command>tool apiece. Setup and lifecycle commands (init,config,daemon,native-host,mcp) are hidden viamcp:hiddenannotations. Two composite tools with no single-command equivalent stay first-class —papio_acquire_batch(bulk work input) andpapio_batch_wait(bounded polling) — alongside the five read resources. Migrated the server library frommodelcontextprotocol/go-sdktomark3labs/mcp-gofor parity with zotio.
Fixed¶
- Reliability: overlapping extension state writes are now persisted through a
serialized save chain, so a reordered
chrome.storagewrite can no longer resurrect a stale snapshot after a service-worker restart. - Reliability: concurrent queued-handoff fallback timers no longer drop each
other's forced releases; a single drain loop consumes every pending release,
so queued jobs can no longer be stranded invisibly with
tab_id -1. - Reliability: a failed native-host idle-poll write now tears the bridge down instead of leaving the process alive but no longer polling (which starved the extension of offers and cancels).
- Reliability:
fetchCandidatespropagates theOpenHumanActionwrite error before parking a landing-page-only job, matchingexhaustedCandidates, so a transient write failure can no longer strand a job with no human-action row. - Concurrency: removed a redundant drain goroutine in
readBodyWithContextthat doubled leaked goroutines when a response body read hung. - MCP
acquire.reportnow classifies failures — missing batch asnot_found, malformed batch ID asinvalid_argument, and other failures asinternal— instead of collapsing every error intonot_found. - Batch settlement is now a single source of truth (
batch.Report.Settled), removing a stale duplicate outcome list inpapio_batch_waitthat carried legacy outcome spellings. - Docs/schema for
papio_batch_waittimeout_secondsnow state that0or an omitted value defaults to 300, matching the implementation.
[0.2.0] - 2026-07-15¶
Phase 0 — contracts and prerequisite¶
- Established the papio Go/Bun workspace, fail-closed shared protocol fixtures, and draft work-request, acquisition-bundle, and browser contracts.
- Added zotio's stored-attachment upload path with reconciliation and retry-safe Web API registration, which is the import prerequisite for papio exports.
Phase 1 — durable open-access acquisition¶
- Added private configuration, SQLite migrations, daemon IPC, durable job and lease recovery, source budgets, redacted events, quarantine, and content-hash artifact storage.
- Added normalized work identity, deterministic candidate ranking, bounded HTTPS acquisition, PDF validation, OCR fallback, and review/rejection paths.
Phase 2 — institutional browser handoff¶
- Added the native-host bridge, versioned bounded browser protocol, native-host install/status commands, and a least-privilege MV3 extension for one requested institutional download per job.
- Added adoption confinement and validation for browser downloads, with restart-safe daemon and extension lifecycle handling.
Phase 3 — provider adapters and protocol lock¶
- Added declarative, permission-gated adapter execution and sanitized fixture capture for ProQuest, JSTOR, EBSCO, and Springer flows.
- Locked
work-request/1,acquisition-bundle/1, andpapio-browser/1with strict cross-runtime fixtures; retained Go as the core after the reversal review.
Phase 4 — zotio, MCP, and human resolution¶
- Added zotio capability/version preflight, preview/apply plans, confirmation hashes, import-ledger idempotency, missing-PDF intake, and stored attachments.
- Added MCP tools and resources over the same application service, plus bounded human identity-review resolution and action lifecycle cleanup.
- Added extension session recovery across daemon restarts and startup wake-up.
Post-Phase 4 — autonomous acquisition¶
- Added OpenAlex discovery, batch acquisition, serialized retry-safe auto-import, session keepalive, observed-provider fixture capture, library-aware batches, OA browser fallback, snowball search, status/reporting, notifications, watchlists, MCP loop closure, and first-run onboarding.
- Updated zotio integration with collection-aware missing-PDF scopes, item-type valid container-title mapping, exact-key enrichment, and transactional workflow execution.
Phase 5 — release preparation¶
- Added local release artifacts for papio and zotio binaries, the extension ZIP, dependency inventories, license reports, hashes, and a machine-readable release manifest.