The reader's bibliography
Date: 2026-09-30
Status: approved in conversation (option 1, engagement + explicit saves),
built on feat/bibliography, held as a draft PR until the AgentMarkit
rehearsal shows an agent saving a paper and reading an uploaded .bib.
Depends on: the library store (2026-09-05-library-store-design.md),
implicit feedback (implicit.py), the Reading desk's last-verdict-wins rule
(2026-09-29-reading-desk-design.md).
What this is for
A researcher's agent reads the feed with them, and what they engage with evaporates: the library knows a paper exists, nothing knows the reader cared. Two asks, one file:
- The feed lands in a
.bib. Papers the reader engaged with -- rated useful (desk, UI, or a verdict the agent extracted), read in full throughfeed.read, or asked about throughfeed.explain-- and papers they explicitly asked to keep ("save this", "I'll cite this") become entries in one generated BibTeX file per persona, ready for\cite{}. - Their own
.bibuploads are read. On a hosted AgentMarkit machine the reader has no Zotero desktop; a.bibexported from Zotero, Mendeley, EndNote or Paperpile, dropped into a folder, is the integration.
Zotero's web API was considered and deferred: an upload covers every reference manager, needs no credential, and the bibliography tools have no measured use yet. It returns when a customer needs live sync (see the conversation record of 2026-09-30).
Decisions
One table, rendered. bibliography(user_id, reference_id, cite_key,
reason, added_at, removed_at) -- migration 013. The .bib is a pure render
of a persona's live rows. Rejected: deriving the file from clicks +
engagement + a saves table at render time, which spreads the rule across
three tables, cannot answer "why is this in my bib" without reconstruction,
and cannot express a removal without inventing a negative signal.
reason is provenance, one of useful, read, explained, saved.
It is what the agent answers "why is this here?" with. Among implicit
reasons the first recorded stands (fold visits useful, then explained,
then read, so the strongest wins on a first fold); an explicit save always
overwrites an implicit reason with saved, because the reader said so.
Removal is a tombstone, removed_at set, never a delete. An implicit
signal never revives a tombstoned entry (re-reading a paper you removed does
not put it back); an explicit save does. A later human "not useful" verdict
tombstones an entry whose reason is implicit, and stops engagement adding the
paper; it never touches a saved entry. This is the desk's
last-explicit-verdict-wins rule, and like the rest of the repo it infers no
negative from silence.
Only human signal adds. Clicks with source in HUMAN_CLICK_SOURCES
(ui, agent); bootstrap, simulated and implicit clicks never reach a
reader's bibliography -- a chat model reacting as the persona is not the
reader choosing what to cite.
Engagement is folded at render, not hooked at write. fold(conn,
user_id) is one idempotent pass (INSERT OR IGNORE) over human clicks and
engagement, run before every render. The four write sites (record_click,
two engagement inserts, desk import) are untouched, so the hot paths keep
their single responsibility and a failure here can never cost a click.
Automatic adds are papers. Engagement adds only items with a DOI or an
arXiv id -- the same rule FeedRecords uses, so an HN thread the reader
opened does not become @misc. An explicit save takes any item, or any
library reference by key, DOI or arXiv id.
A cite key never changes. export_bib suffixes a repeated key by row
order, so removing one entry would rename another and break every \cite
using it. The key is assigned once, at add time (bibtex_key of the row, or
the first free b, c, ... suffix for that persona) and stored;
UNIQUE(user_id, cite_key). A tombstoned entry keeps its key, so a removed
and re-saved paper comes back under the same one.
Where the file goes. ATTEST_BIB_OUT names a directory; each persona
with live entries is written to <dir>/<persona>.bib, atomically, with a
header saying it is generated and edits are overwritten. Unset = inert: the
table still records, nothing is written -- the desk's convention.
Reading uploads. ATTEST_BIB_PATHS entries may now be directories; a
directory contributes every *.bib directly inside it, non-recursively. The
hosted layout puts uploads in ~/.hermes/workspace/bibliography/ and the
generated file in its attestation/ subfolder, so the reader never reads
attestation's own output back as user input, and a newly uploaded file is
picked up without editing .env or reloading the MCP server.
Surface
cite.save(paper, user, remove=False)-- the one new tool, on the knowledge surface (cite.* is 7, the default server 50).paperis a feed item id or a library key/DOI/arXiv id. Folds engagement, saves or tombstones, re-renders the file whenATTEST_BIB_OUTis set, and returns the entry (key,title,reason,bibtex), the live entry count and the file NAME (never a path: a machine string in every reply). An unknown persona refuses (a write never autocreates); an unknown paper refuses.attest library bib [--user NAME]-- fold and write every persona's file (or one), printing counts; inert whenATTEST_BIB_OUTis unset. The hourly refresh script runs it after the desk, degraded and never fatal.- Persona lifecycle.
persona_delete(purge with delete_user) deletes the persona's entries, since users.id is a reused rowid.persona_resetkeeps them: reset is about the ranker, the bibliography is the reader's.mergemoves entries to the keeper where neither the reference nor the key is already taken there.
Skills
The knowledge skill learns "save this / add it to my bib / I'll cite this"
-> cite.save, "take it out" -> cite.save(remove=True), and "why is this
in my bib" -> the reason. The feed skill (which cannot name cite.save,
not on its surface) says that rating, reading and asking about a paper add it
to the reader's bibliography. AgentMarkit's SOUL.md names
mcp__attestation_knowledge__cite_save and the rehearsal asks one save
question and one uploaded-.bib question.
Testing
DB-level tests for fold (human-only, papers-only, tombstones respected, a
not-useful verdict tombstoning implicit entries but never saved ones), key
stability across removal, render determinism, the directory form of
ATTEST_BIB_PATHS, persona delete/merge, and the tool envelope including
both refusals. The live check is the AgentMarkit rehearsal with
ATTESTATION_SRC pointing at this branch -- that, not this suite, decides
when the PR leaves draft.