Skip to content

design notes

sinrimin edited this page Oct 26, 2026 · 6 revisions

Notes — capture, organize, annotate, revise (Issue #88)

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.

Context

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.

Domain model (core)

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.

Annotation anchoring

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.

The revision loop (the feature's payoff)

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.

API surface (REST, thin routes → service)

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 bump contentVersion and write a snapshot) / DELETE /api/notes/:id.
  • GET /api/notes/square — every user's public notes, read-only, paginated (20/page).
  • GET /api/notes/categories tree 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).

Web UI (Signal contract)

  • One destination 笔记 in NAV (01) — no separate nav slot for the square; the page carries three underline tabs (ui/tabs.tsx style): 我的 | 广场 | 看板 (en labels Library | 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 stay mine|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's contentMd (downloadNoteMarkdown in vocabulary.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; --signal is never spent on controls). Selection → annotation is TWO-STEP: a compact toolbar pill appears at the selection (its mousedown is 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 pinned left-2 ignored the selection horizontally and its autofocused textarea stole focus, breaking copy). Entry points: pane mouseup (desktop) AND a 350ms-debounced document selectionchange (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.

Out of scope (v1) and follow-ups

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".

AI passes (chat capture, summary, revision)

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.

Agent surfaces (MCP namespace + assistant group + injection)

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:

  1. /mcp virtual namespace notes__ (the #80 api-tools pattern): every profile session lists notes__search / notes__get / notes__annotations / notes__create / notes__update alongside pooled upstreams.
  2. Assistant notes tool group (default ON for new configs): the same five tools (notes_create / notes_update ride 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__create files 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__update is DIFF-FIRST: edits: [{oldText, newText}] hunks where each oldText must 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; contentMd full replacement exists only for whole-document rewrites. expectVersion is REQUIRED and must equal the current contentVersion — 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).
  1. <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 the notes_get tool, and vector retrieval upgrades the matcher later without touching the contract.

Clone this wiki locally