Repository navigation
design notes
Issue-driven era. Anchor: issue #88 (scope, open questions, implementation slices). Review prototypes live locally in the repo's git-ignored
docs/dev/notes-feature/; this doc is the durable design.
The platform grows three content services for agents on one storage substrate:
| pillar | content | writer | notes |
|---|---|---|---|
| knowledge base | long-lived knowledge, norms, detailed facts | LLM, wiki-entry style (llm-wiki) | most complex; later |
| notes | day-to-day captures: articles, AI reports, write-ups | human (promotable into the KB later) | this design |
| memory | short facts | agent | prior art (mem0 et al.) underwhelming; re-research later |
All three eventually surface to client agents as native tools on the aggregated
/mcp endpoint and to the platform assistant via direct service calls. Notes
ship first and deliberately exclude the other pillars.
Note
id, ownerId // notes are always personal-owned
title, summary?, contentMd // summary is first-class: feeds + injection prefer it
contentVersion // monotonic, bumped on every content save
categoryId?, tags[], status, visibility, source
createdAt, updatedAt
NoteVersion // immutable snapshot per (noteId, version)
noteId, version, contentMd, createdAt
NoteAnnotation // "inline comment" = revision instruction for an agent
id, noteId, noteVersion // pinned to the version it was drawn on
quote, contextPrefix, contextSuffix // exact span + surrounding context
comment, state ('open'|'done'), authorId, createdAt, resolvedAt?
NoteCategory // tree, one parent, per owner
id, ownerId, parentId?, name, createdAt
Enums (English on the wire, translated in the UI): status: inbox | to_read | reading | read, visibility: private | public, source: manual | chat | url.
New ports on UnitOfWork: notes / noteAnnotations / noteCategories
(packages/core/src/ports/repository.ts); drivers under
server/src/infra/storage/{memory,sqlite} implement them like every other repo.
Anchors are quote + context, pinned to an immutable version — never raw
character offsets (edits invalidate them) and never block ids (would require a
block editor). When content moves vN → vN+1 (typically an AI revision), open
annotations re-anchor by searching the quote (disambiguated by prefix/suffix);
a span that no longer matches surfaces as lost rather than silently dropping.
AI emits a report → saved as a note → the user marks it up in one pass (inline annotations) → "AI revise by annotations" hands every open annotation (quote + comment) plus the note to an agent → the revision lands as vN+1, open annotations re-anchor or surface as lost, the annotator marks them done. This replaces copy-pasting snippets into a chat one by one.
Service NotesService follows the #53 contract (Fastify-free, principal
explicit, single authorization point, 404 existence-hiding):
-
GET /api/notes— own notes; filters: category/status/tag/search/visibility; paginated. -
POST /api/notes/PATCH /api/notes/:id(content changes bumpcontentVersionand write a snapshot) /DELETE /api/notes/:id. -
GET /api/notes/square— every user's public notes, read-only, paginated (20/page). -
GET /api/notes/categoriestree CRUD;GET /api/notes/:id/annotations+ annotation create/update(state)/delete. - Summary generation (chat capture + editor button) calls the assistant path in-process; it is a convenience, not a separate authorization surface.
Authorization: notes are owner-only for read/write (#36 personal rules);
visibility=public grants other users read of the note row itself (the square)
— nothing else (no annotations, no board).
- One destination
笔记in NAV (01) — no separate nav slot for the square; the page carries three underline tabs (ui/tabs.tsx style):我的 | 广场 | 看板(en labelsLibrary | Explore | Kanban— "Mine" is 我的's literal translation and no English product tabs it; the personal-collection tab is "Library" (YouTube's Library|Explore pair), discovery is "Explore", status columns are "Kanban". Internal ids staymine|square|board). - Feed tab: card list (title/summary 2-line clamp/category/tags/status/
visibility/annotation count/date) + sidebar
搜索 → 目录 → 筛选(status rows and tag cloud merged in one collapsible panel; last panel stretches to the feed's height). Square tab: same cards with an author line + hot-tags sidebar. Board tab: four status columns, personal notes only, per-column cap. Card action menus (feed + board) carry下载 .md— pure client-side blob download from the view'scontentMd(downloadNoteMarkdowninvocabulary.ts), no server surface. - Editor (create/edit shared page): card with header underline tabs 编辑/预览, title, mono markdown body, footer status line (format · word count · autosave); right meta panel (summary w/ AI-generate, category, tags, status, visibility, source well).
- Detail page: summary lead block, meta chips, source well, markdown body with
mark highlights (open = signal underline, done = muted dashed) and the
annotation pane; chrome actions
编辑(outline) +让 AI 按批注修订(primary ink — the system has NO signal-colored button;--signalis never spent on controls). Selection → annotation is TWO-STEP: a compact toolbar pill appears at the selection (itsmousedownis prevented, so the selection — and copy — survives while it is up); clicking批注opens the comment popover. Both popover anchors derive from the selection rect, centered and clamped into the pane (a v1 box pinnedleft-2ignored the selection horizontally and its autofocused textarea stole focus, breaking copy). Entry points: panemouseup(desktop) AND a 350ms-debounced documentselectionchange(mobile long-press never fires mouseup; a collapse closes only the toolbar — caret moves inside the comment textarea land there too — and pane scrolls keep the draft: soft keyboards scroll the pane mid-typing). - Chat capture:
保存为笔记on assistant replies opens a FormDialog (title, AI-pregenerated editable summary, tags, category/状态/visibility row, source hint) — capture stays light, full organization happens in the editor. - i18n en/zh for every string (
strings/notes.ts, side-by-side objects); mobile (<900px plate hides): sidebar collapses into an expandable panel, board scrolls horizontally, editor meta panel moves below the body.
Calendar/timeline views, todos, relations/rollups/formulas, block editor, collaboration comments, URL body fetching (third outbound HTTP surface — needs its own allowlist/TTL design), vector search (sqlite-vec — upgrades the injection's retrieval; the contract below stays), public-note "save a copy".
One-shot LLM completions over note content (services/notes-ai.ts): a BARE
pi Agent — no tools, no session — built on the same provider+credential
resolution an assistant turn uses. Default model = the owner's personal
providers first, then global, first chat entry; none configured is an honest
409 NOTE_AI_NO_PROVIDER. Endpoints: POST /api/notes/summarize (works on
UNSAVED content — the capture dialog and the editor's 生成 button both seed
from the current draft) and POST /api/notes/:id/revise — every OPEN
annotation + the body goes to the model, the rewritten markdown saves as the
NEXT immutable version (the service bumps it), and the addressed annotations
flip to done; quotes the rewrite consumed surface as lost in the pane.
The library reaches agents on three surfaces, all through the same dispatcher
(mcp/notes-tools.ts) over the #53 services — the principal is the PAT owner
(/mcp) or the session owner (assistant), so owner-only access and 404 hiding
apply everywhere:
-
/mcpvirtual namespacenotes__(the #80 api-tools pattern): every profile session listsnotes__search/notes__get/notes__annotations/notes__create/notes__updatealongside pooled upstreams. -
Assistant
notestool group (default ON for new configs): the same five tools (notes_create/notes_updateride the WRITE class — the #79 approval gate prompts per call unless the session pre-authorized writes).
Writes are read-gated by design (client-agent write-tool lessons):
-
notes__createfiles agent output into the library; the source is PINNED to{kind:'agent'}regardless of what the caller passes (visible in the UI, never masquerading as a human capture). -
notes__updateis DIFF-FIRST:edits: [{oldText, newText}]hunks where eacholdTextmust occur EXACTLY once (0 or >1 is an error telling the agent to add context or re-read — never a guess). A one-line change costs one hunk, not a full-body re-emit;contentMdfull replacement exists only for whole-document rewrites.expectVersionis REQUIRED and must equal the currentcontentVersion— a stale/missing version is refused with "re-read first" (409 NOTE_VERSION_MISMATCH), so a blind write can never clobber content the agent never read. Body changes ride the existing version machinery (bump + immutable snapshot + quote re-anchoring).
-
<system_context>injection: when the group is on, each turn's prompt keyword-matches the owner's notes (ASCII words + CJK bigrams, score ≥2 on title+summary+tags, top 3) and appends a marker-wrapped summaries-only block to the system prompt — the v0 of the user's validated injection mechanism; full bodies stay behind thenotes_gettool, and vector retrieval upgrades the matcher later without touching the contract.