GO

gsmlg-opt/agent-note

API development
0 stars 0 forks 품질 55 트렌드 20

Agent note: Rust note server with MCP, embedding, and Yew frontend

개요

Hybrid-search notes app with local inference by default. See docs/design.md for the full design. A Rust workspace: note-core (pure types/validation/RRF fusion), note-storage (backend-neutral repository contracts), note-storage-turso (embedded Rust Turso Database adapter with title FTS and exact dense retrieval), note-storage-pg (external PostgreSQL adapter with title FTS and exact pgvector retrieval), note-embedding (BGE-M3 via local ONNX or a self-hosted OpenAI-compatible service, with a deterministic stub for offline dev), note-org (pure Org source projection/editing, workflow policy, dependency validation, and readiness), note-pipelines (save/search workflows), note-mcp (MCP Streamable HTTP), note-server (Axum REST + /mcp + /org/mcp), and note-frontend (Yew/Wasm UI built with the yew-duskmoon component library). The frontend styling comes from the @duskmoon-dev/core design system, vendored as a prebuilt stylesheet at crates/note-frontend/duskmoon-core.

README

agent-note

Hybrid-search notes app with local inference by default. See docs/design.md for the full design.

A Rust workspace: note-core (pure types/validation/RRF fusion), note-storage (backend-neutral repository contracts), note-storage-turso (embedded Rust Turso Database adapter with title FTS and exact dense retrieval), note-storage-pg (external PostgreSQL adapter with title FTS and exact pgvector retrieval), note-embedding (BGE-M3 via local ONNX or a self-hosted OpenAI-compatible service, with a deterministic stub for offline dev), note-org (pure Org source projection/editing, workflow policy, dependency validation, and readiness), note-pipelines (save/search workflows), note-mcp (MCP Streamable HTTP), note-server (Axum REST + /mcp + /org/mcp), and note-frontend (Yew/Wasm UI built with the yew-duskmoon component library).

The frontend styling comes from the @duskmoon-dev/core design system, vendored as a prebuilt stylesheet at crates/note-frontend/duskmoon-core.css (Trunk links it; no JS build step needed). To refresh it: bun add @duskmoon-dev/core in crates/note-frontend, copy node_modules/@duskmoon-dev/core/dist/index.css over duskmoon-core.css, then remove node_modules/package.json. With no data-theme on ``, DuskMoon automatically uses sunshine in light mode and moonlight when the operating system prefers dark mode.

Architecture

Pure core, effectful shell. Business logic is a pure, I/O-free core; all side effects (DB, inference) live at explicit boundaries and are threaded through one Context value rather than global state. The crates form a strict dependency stack — each depends only on those above it:

note-org         pure Org source projection/editing, workflow policy,
                 dependency validation, and readiness (no I/O)

note-core        pure: Note/Label/LabelKey types, input validation, RRF rank-fusion (no I/O)
   ▲
   ├── note-storage     backend-neutral repository/session/transaction contracts
   ├── note-storage-turso
   │                    Rust Turso Database: notes + labels, title FTS/BM25 + exact dense cosine
   ├── note-storage-pg  PostgreSQL: notes + labels, title FTS/GIN + exact pgvector cosine
   ├── note-embedding   Embedder trait → StubEmbedder | local ONNX | OpenAI-compatible BGE-M3
   ▲
note-pipelines   Context + workflows that compose core/storage/embedding:
                   • save_note   — validate → atomically persist note/chunks/jobs/labels → return Note
                   • embedding worker — embed queued body chunks → atomically persist dense vectors only
                   • search_notes — title FTS + exact dense content → weighted RRF → minimum score → hydrate top-k
                   • org — transport-free, revision-safe workspace/document/item commands,
                     append-only audit reads, and recovery-context assembly
   ▲
   ├── note-mcp     12 Markdown-note tools at /mcp + 40 Org tools at /org/mcp (Streamable HTTP)
   └── note-server  Axum REST (/api/notes, /api/labels, /api/org) + /mcp + /org/mcp
                        ▲
                   note-frontend   Yew MVU (AppState + pure reducer) → talks to note-server over REST

Org canonical persistence. note-org supplies the pure Org domain and workspace-time behavior. Storage schema v7 persists canonical Org workspaces and documents, durable execution attempts, exclusive leases, richer append-only events, idempotent operation results, and derived, rebuildable work-item projections. Documents also carry nullable archived_at lifecycle state. The embedded Turso adapter upgrades supported marked databases through v7, including the v6-to-v7 document lifecycle migration; PostgreSQL applies the ordered Org migrations through 0006_org_document_lifecycle.sql (with 0005_attachment_operations.sql preceding it). Markdown notes and their storage remain unchanged. Projection rebuilds preserve attempts, events, leases, and operation results.

The transport-free note_pipelines::org boundary provides atomic, revision-safe commands for workspace and document management—including empty create, rename, reversible archive, and restore—and for item creation, follow-ups, movement, reparenting, assignment, scheduling, dependencies, and Markdown-note links. It also provides workspace-sequenced audit pages and recovery context assembled from canonical document revisions, projections, dependencies, weak-note availability, attempts, artifacts, origins, and cross-workspace event lineage. It also provides deterministic, cursor-paged ready, assigned, running, blocked, review, scheduled, upcoming-deadline, failed, expired-lease, and completed operational views. Archived workspaces remain readable and auditable while rejecting new mutations.

Org execution and review ownership use exclusive leases with opaque fencing tokens. A claim creates the token. Heartbeats, releases, progress, results, review actions, and ownership-sensitive state transitions validate the current unexpired token. Reclaim atomically expires the old ownership, records recovery, creates a new attempt and lease, and permanently invalidates stale tokens. Retry can claim only closed failed work within its retry budget. The shared workspace concurrency limit counts active execution and review leases and is checked in the same transaction as its effects; idempotent operation replay returns the original result without duplicate attempts, events, state changes, or tokens. Actor IDs are client-asserted audit data, not trusted identities.

Delivery Slice 6 originally exposed the same 36 Org operations through REST/OpenAPI, MCP, and four storage-only offline commands where applicable. REST and MCP call the same pipeline boundary and share the same Arc; no transport duplicates policy, revision, idempotency, or lease logic. Delivery Slice 7 adds a read-only Org-content and operations console. The workspace management slice adds only create, complete structured update, and reversible archive over REST; the document lifecycle slice adds four matching operations for empty create, rename, reversible archive, and restore. Raw Org source editing and workflow operations remain Web-read-only. The browser adds no polling or application authentication layer. Agent Note implements no inbound authentication, authorization, proxy-identity-header, or workspace ACL behavior. A front proxy owns TLS, authentication, authorization, and network access; direct exposure to an untrusted network is unsupported.

One core, two front doors. REST and both MCP HTTP endpoints call the exact same note-pipelines functions — no business logic is duplicated per transport (design.md §1–2). Hybrid retrieval runs Turso full-text ranking over note titles and exact dense-vector ranking over body chunks, then fuses the two ranked lists with deterministic weighted Reciprocal Rank Fusion. Title FTS has weight 3.0; dense body retrieval has weight 1.0. The persisted global System setting search.minimum_score defaults to 0.01; the shared pipeline keeps fused scores greater than or equal to that cutoff before hydration and the requested result limit. The resulting score is a fused weighted-RRF score, so the UI labels the action as retrieval. Labels are Kubernetes-style: each key is registered once in a catalog with a description, and notes attach known keys with a value (at most one value per key); save auto-creates a missing key with an empty description.

See docs/design.md for the full contracts and docs/superpowers/ for the spec and build plan.

Setup

  1. Install Rust (stable), Trunk (cargo install trunk), and the wasm32-unknown-unknown target (rustup target add wasm32-unknown-unknown).
    • Note: if Rust is managed by rustup installed via Homebrew, rustup is keg-only — put /usr/local/opt/rustup/bin (Apple Silicon: /opt/homebrew/opt/rustup/bin) ahead of the Homebrew rust formula on your PATH, otherwise cargo resolves to a single-target rust that can’t build wasm and trunk build fails.
  2. (Optional, for real local embeddings) Download the BGE-M3 int8-quantized ONNX model:
    hf download gpahal/bge-m3-onnx-int8 --local-dir ./models/bge-m3-int8.onnx
    
    The whole app runs without it using a deterministic stub embedder. To use the real model, edit the generated dev-data/config.toml so its embedding table is:
    [embedding]
    engine = "local"
    model_path = "../models/bge-m3-int8.onnx/model_quantized.onnx"
    
    NOTE_MODEL_PATH is the fallback when model_path is omitted. Embedding inference uses all detected physical CPU cores by default; set NOTE_EMBEDDING_THREADS to a positive integer to override it, or explicitly set it to auto to retain the default.

Build & test

  • cargo build --workspace — builds all native crates (everything except the wasm frontend).
  • cargo test --workspace — runs the backend test suite.
  • cd crates/note-frontend && cargo test — runs the frontend’s native logic tests (the frontend is excluded from the workspace because it targets wasm; test it from inside its directory).

Run the PostgreSQL suite against a real pgvector-capable server with:

TEST_DATABASE_URL=postgresql://postgres:[email protected]:5432/postgres cargo test -p note-storage-pg

Without TEST_DATABASE_URL, PostgreSQL integration tests emit visible skip notices instead of accessing an external database.

Run (end to end, dev)

  1. Install Trunk once:
    cargo install --locked trunk
    
  2. From the repo root, start the backend and hot-reloading frontend together:
    cargo run
    
    Then open http://0.0.0.0:6221. Trunk proxies /api, /mcp, and /org/mcp to note-server on 127.0.0.1:6222. A debug build atomically creates ./dev-data/config.toml on first start, without replacing an existing file. Its generated settings store the embedded Turso database at ./dev-data/notes.db and attachments under ./dev-data/attachments. A note’s unrendered Markdown is available at GET /api/notes/{id}/raw and GET /notes/{id}/content. During local development, use the backend URL http://127.0.0.1:6222/notes/{id}/content for the latter because Trunk owns the frontend /notes/* routes on port 6221. Add ?type=html to the latter URL for a standalone, styled HTML document suitable for iframe embedding.

HTTP API documentation

With the HTTP server running, open the interactive Swagger UI directly at http://127.0.0.1:6222/api/docs or fetch the generated OpenAPI document from http://127.0.0.1:6222/api/openapi.json. During frontend development, Trunk proxies the same paths on port 6221, so /api/docs and /api/openapi.json are also available there.

The OpenAPI document covers the REST API only. /mcp and /org/mcp remain outside the document and retain their own MCP protocol discovery and schemas. Swagger UI has Try it out enabled, including for destructive operations, and the HTTP API is unauthenticated. Use it only on a trusted network or behind an authenticating reverse proxy.

Bulk note label updates

The MCP tool bulk_update_note_labels and REST endpoint POST /api/notes/bulk-labels call the same pipeline. A set-only request adds or replaces a label:

{"selector":"type=ietf-rfc","set":[["project","ietf-rfc"]]}

A remove-only request removes a label:

{"selector":"type=bear_note","remove":["type"]}

They return only bounded aggregate counts, never a per-note result list:

{"matched":42,"updated":40,"unchanged":2}

matched is the number of matching active notes. updated counts matched notes where at least one requested mutation changed, and unchanged is matched - updated. A note with one no-op mutation and one changed mutation counts once as updated.

The operation fixes the unpaginated set of matching active note IDs before any label changes, then applies the entire set/replace/remove update atomically (all-or-nothing). set and remove are individually optional, but at least one mutation is required. A blank selector, invalid or duplicate keys, duplicate remove entries, and a key present in both set and remove are rejected. Each assignment expresses desired state: a missing label is added, a different value is replaced, an exact existing value is a no-op, and removing an absent label is a no-op. Missing set target catalog keys are created as text only when at least one note matches, while values for existing typed catalog keys are validated. remove never creates or deletes catalog keys.

Absent intervening changes to the matching set or its requested label state, exact replays are no-ops. Matched notes for which no requested mutation changes state contribute to unchanged. If a request removes the selector label, replay returns zero only absent intervening changes that make notes match the selector again. Unrelated labels, content, attachments, revision, chunks, and embeddings are preserved. Only changed notes advance updated_at; no-op and zero-match requests do not change timestamps or create catalog keys.

Org Web operations console and workspace management

The Org console is available at six client-side routes:

  • /org — active/archived workspace directory with counts for all operational views;
  • /org/new — create a workspace from the structured engineering-default policy;
  • /org/:workspace_id — one workspace’s server-computed operational ledger;
  • /org/:workspace_id/files — status-filtered document lifecycle management;
  • /org/:workspace_id/settings — edit the complete structured workspace policy;
  • /org/:workspace_id/items/:item_id — recovery context and subject-filtered event history.

The workspace ledger exposes exactly ten views: ready, assigned, running, blocked, review, scheduled, upcoming_deadline (shown as Due soon), failed, expired_lease, and completed. The selected view, filters (item_type, state, priority, tags, assignee, from, and to), opaque cursor, and page limit live in the URL. Item links carry that complete typed state with return_ parameters, so Back restores the exact originating ledger URL. Workspace-directory archive selection, cursor, and limit are also URL-backed. The files route defaults to status=active and exposes Active and Archived filters plus opaque cursor pagination and limits of 10, 25, 50, 100, or 200. It never renders or edits raw Org source.

Data loads once on navigation and changes only on an explicit Refresh action; there is no automatic polling. Server-provided workspace time is the primary timestamp and browser-local time is secondary. Invalid timestamps or timezones render as Unavailable, and weak Markdown-note links whose target is missing remain visible as unavailable rather than disappearing. Loading, empty, and structured error states are explicit.

The browser uses same-origin REST. Its workspace mutations are POST /api/org/workspaces, PATCH /api/org/workspaces/:workspace_id, and POST /api/org/workspaces/:workspace_id/archive. Its approved document-container exception is POST /api/org/workspaces/:workspace_id/documents, PATCH /api/org/documents/:document_id/path, and POST /api/org/documents/:document_id/archive|restore. These requests use actor_id: "web-ui" and generated idempotent operation IDs; revision-bearing operations use the loaded document or workspace revision. Raw Org source updates, work items, claims, transitions, reviews, import/move, and hard delete remain unavailable in the browser. The browser never calls MCP or stores a fencing token. Agent Note itself has no authentication, session, trusted proxy-identity-header, or workspace ACL feature; the front proxy must own TLS, authentication, authorization, Host/origin policy, and network access.

For local development, Trunk proxies /api/ to 127.0.0.1:6222 while retaining /org routes for the Yew router. Packaged builds set NOTE_STATIC_DIR; any unclaimed direct load or refresh of the six routes returns the frontend index.html with HTTP 200, while /api/org remains claimed by the REST router.

Org REST API

The Org REST surface has exact one-to-one parity with the 40 org_* MCP tools. Each generated OpenAPI operationId is the matching MCP tool name:

Method Path operationId
GET /api/org/workspaces org_list_workspaces
POST /api/org/workspaces org_create_workspace
GET /api/org/workspaces/{workspace_id} org_get_workspace
PATCH /api/org/workspaces/{workspace_id} org_update_workspace
POST /api/org/workspaces/{workspace_id}/archive org_archive_workspace
GET /api/org/workspaces/{workspace_id}/documents org_list_documents
POST /api/org/workspaces/{workspace_id}/documents org_create_document
GET /api/org/documents/{document_id} org_get_document
PUT /api/org/documents/{document_id} org_put_document
PATCH /api/org/documents/{document_id}/path org_rename_document
POST /api/org/documents/{document_id}/archive org_archive_document
POST /api/org/documents/{document_id}/restore org_restore_document
POST /api/org/documents/{document_id}/move org_move_document
POST /api/org/items/{item_id}/move org_move_item
POST /api/org/workspaces/{workspace_id}/import org_import_workspace
GET /api/org/workspaces/{workspace_id}/export org_export_workspace
POST /api/org/workspaces/{workspace_id}/items org_create_item
GET /api/org/items/{item_id} org_get_item
GET /api/org/items/{item_id}/context org_get_item_context
POST /api/org/items/{item_id}/follow-ups org_create_follow_up
POST /api/org/items/{item_id}/assignment org_assign_item
POST /api/org/items/{item_id}/schedule org_schedule_item
GET /api/org/queue org_query_queue
GET /api/org/agenda org_query_agenda
POST /api/org/items/{item_id}/claim org_claim_item
POST /api/org/items/{item_id}/claim/heartbeat org_heartbeat_claim
POST /api/org/items/{item_id}/claim/release org_release_claim
POST /api/org/items/{item_id}/progress org_report_progress
POST /api/org/items/{item_id}/result org_submit_result
POST /api/org/items/{item_id}/transition org_transition_item
POST /api/org/items/{item_id}/retry org_retry_item
POST /api/org/items/{item_id}/review/request org_request_review
POST /api/org/items/{item_id}/review/approve org_approve_item
POST /api/org/items/{item_id}/review/reject org_reject_item
POST /api/org/items/{item_id}/dependencies org_add_dependency
DELETE /api/org/items/{item_id}/dependencies/{dependency_item_id} org_remove_dependency
POST /api/org/items/{item_id}/note-links org_link_note
DELETE /api/org/items/{item_id}/note-links org_unlink_note
GET /api/org/notes/{note_id}/work-items org_list_note_work_items
GET /api/org/workspaces/{workspace_id}/events org_list_events

Read inputs use path and query parameters. Mutations, including both DELETE operations, use a JSON envelope containing schema_version, workspace_id when it is not supplied by the path, client-asserted actor_id, idempotent operation_id, and operation-specific revision, lease, and action fields. A path ID is authoritative; any duplicated ID in a body must match it. actor_id is audit attribution supplied by the caller, not a verified identity or authorization decision. An identical operation replay returns its original result, including after restart; reusing the operation ID for different input returns idempotency_conflict.

Document list reads accept status=active|archived|all and default to active. The legacy include_archived=false|true alias means active|all; combining it with status is invalid. Filtering happens before cursor pagination, and status is bound into the opaque cursor. List, raw-source, export, and snapshot document records include required-but-nullable archived_at. Empty create returns revision 1 and stable caller-supplied identity. Rename is revision-safe for active or archived documents and preserves ID, source, content hash, projection identity, and archive state. Archive is reversible, rejects active execution or review leases, reserves the current path, and removes the document’s items from operational views without satisfying unfinished dependencies. Restore reactivates the same document and projections. There is no document hard delete.

REST success bodies serialize the same pipeline DTOs returned as MCP structured content. REST errors are always JSON with this common shape:

{
  "code": "active_lease",
  "message": "work item already has an active lease",
  "details": {},
  "retryable": false
}

invalid_input and unsupported_semantic_edit map to HTTP 400; missing requested resources map to 404; archived_document, document_path_conflict, workflow, revision, idempotency, dependency, review, lease, and retry conflicts map to 409; concurrency_limit maps to 429 with retryable: true; and storage failures map to 500 with a safe generic message. Protocol framing and HTTP status are transport metadata; result fields and the four error fields have MCP/REST parity.

Raw fencing tokens are sensitive ownership proofs. Only successful claim and retry/reclaim responses return them, and only lease-bound mutation requests accept them. They must not be logged or copied into errors, events, queues, context, source documents, exports, or general application state. OpenAPI marks token-bearing request fields as sensitive and contains no token-bearing read schemas or examples.

Agent Note deliberately implements no authentication, authorization, session, or workspace ACL behavior. It does not consume a trusted proxy-identity header and the OpenAPI document defines no security scheme. A front proxy must own TLS, authentication, and access control, as well as the deployment’s Host/origin and network restrictions. Direct exposure to an untrusted network is unsupported.

Runtime configuration

Every normal entrypoint—HTTP server, MCP over HTTP, legacy --import/--export, and the four org offline modes—loads a configuration file before opening storage. NOTE_CONFIG_PATH selects it; a relative selector is resolved from the process working directory. Without that variable, the path is ./dev-data/config.toml.

The server atomically creates the implicit ./dev-data/config.toml when it is missing. An explicitly selected missing file is always an error. Existing configuration files that do not contain server.bind_addr are atomically updated with the resolved value while preserving their other settings. An existing bind address is never overwritten. This is the generated configuration:

[server]
bind_addr = "0.0.0.0:6222"

[database]
engine = "embed"
path = "notes.db"

[embedding]
engine = "local"

[attachments]
engine = "filesystem"
path = "attachments"

See config.example.toml for every supported option, environment fallback, default, and adapter-specific example.

The HTTP bind address independently uses server.bind_addr, then NOTE_BIND_ADDR, then 0.0.0.0:6222. The default exposes the REST API, /mcp, and /org/mcp on every network interface; run it only on a trusted network or protect it with an authenticating reverse proxy.

All relative path values—including values supplied by environment fallbacks—are resolved from the configuration file’s parent directory. Each field independently uses configuration file, then its supported environment variable, then its built-in default. The active schemas and their fallbacks are:

  • [database]: engine (NOTE_DB_ENGINE, default embed); for embed, path (NOTE_DB_PATH, default notes.db); for pg, url (DATABASE_URL) and max_connections (NOTE_DB_MAX_CONNECTIONS, default 10). The embedded adapter is Rust Turso Database.
  • [embedding]: engine (NOTE_EMBEDDING_ENGINE, default local); for local, optional model_path (NOTE_MODEL_PATH). No model path selects the deterministic stub; a path selects local BGE-M3 ONNX inference. For openai, base_url (NOTE_EMBEDDING_BASE_URL), model (NOTE_EMBEDDING_MODEL, default bge-m3), optional api_key_env (NOTE_EMBEDDING_API_KEY_ENV), timeout_secs (NOTE_EMBEDDING_TIMEOUT_SECS, default 30), and max_retries (NOTE_EMBEDDING_MAX_RETRIES, default 3).
  • [attachments]: engine (NOTE_ATTACHMENTS_ENGINE, default filesystem); for filesystem, path (NOTE_ATTACHMENTS_DIR, default attachments). For s3, bucket (NOTE_S3_BUCKET), prefix (NOTE_S3_PREFIX, default empty), optional region (AWS_REGION), optional endpoint (NOTE_S3_ENDPOINT), and force_path_style (NOTE_S3_FORCE_PATH_STYLE, default false).

GaoNote synchronization

The System page can synchronize notes selected by labels, such as project=bear_note, with one GaoNote connection. Multiple enabled selectors form an OR union. Synchronization includes full Markdown, title, typed labels, attachments, trash/restore and permanent deletion. Initial merge uses note UUIDs; the latest complete version wins. Removing a matching label sends a final snapshot and preserves both copies as independent notes. Pausing keeps capturing changes for later delivery.

In System → GaoNote sync, choose Add connection, select a connection method, and enter the GaoNote WebSocket address for the desired environment. The production address is a default; you can replace it with a test server address.

Method Address and configuration
Direct connection A ws:// or wss:// GaoNote address, without a client certificate. WSS verifies the server certificate using native roots or a selected CA file.
SSH tunnel A ws:// GaoNote address reachable from the SSH host, plus SSH host, port, username, private-key file and known_hosts file on the Agent Note server. Agent Note establishes and maintains the tunnel automatically.
mTLS A wss:// GaoNote address, client certificate/key files, and optional server CA file on the Agent Note server.

All socket addresses include the Phoenix path and vsn=2.0.0, for example ws://127.0.0.1:4114/note-sync/websocket?vsn=2.0.0 for GaoNote’s private SSH endpoint. In SSH mode, that loopback address refers to the SSH host. Agent Note initiates the connection; both peers send note data through it. GaoNote does not connect back.

Save the connection, select it, add selectors such as project=bear_note, keep sync paused, and choose Save settings. System then shows the local node ID for receiver registration. Once the target receiver is configured for that node and connection method, enable sync and choose Save settings again. Edit connection updates its address, method, credentials and transfer limit without restarting the server. Connection changes are saved separately from label rules and the enable/pause switch.

Certificate, SSH key and known_hosts files must already be provisioned on the server (and mounted into the container when applicable). The UI accepts file paths; it does not upload private keys. Existing paths are never returned to the browser. When editing, blank credential fields keep their current paths for that method. Switching methods drops credentials that do not apply. For WSS, the CA setting offers keeping the existing CA, using native roots, or replacing it. SSH uses strict host-key checking and key authentication; it does not prompt for passwords. The container image includes the OpenSSH client. See SSH setup for the receiver listener and peer registration.

Runtime TOML can also seed the initial connection catalog:

[[sync.connections]]
id = "gao_note"
mode = "mtls" # Omitted mode also means mTLS for existing configurations.
url = "wss://admin.gsmlg.org/note-sync/websocket?vsn=2.0.0"
client_cert = "/run/credentials/agent-note/client-chain.pem"
client_key = "/run/credentials/agent-note/client-key.pem"
# Omit ca_cert to use native roots; a supplied CA replaces native trust.
ca_cert = "/run/credentials/agent-note/server-ca.pem"
max_blob_bytes = 536870912

Credential paths resolve from the runtime config directory. UI-managed profiles are persisted in a separate server-owned sync-connections.json, outside the note database and note exports. Once that file exists, it is the authoritative catalog and takes precedence over TOML profiles. Its default location is beside the embedded database, or in the parent of the filesystem attachments directory for PostgreSQL, falling back to the runtime config directory. Override it with [sync].profiles_path or NOTE_SYNC_PROFILES_PATH (file configuration takes precedence); relative paths resolve from the runtime config directory. The catalog directory must be writable and persistent. A read-only runtime TOML is supported.

Connection saves validate the method, address and relevant local credential files before replacing the catalog. They reconnect the worker automatically and retain pending work and the paired peer identity. Saving a connection does not establish a successful remote connection; System shows connection state, pending count, last confirmation and errors, with manual retry. A rejected note remains pending and identifies the affected note in System while other notes continue syncing. Correct it and save again, or use manual retry to attempt the same version again. You can correct the selected profile before pairing; after a peer is registered, this database retains its profile and peer identity. Transfer errors preserve pending work and normal note use. The aggregate limit includes event JSON and attachment bytes; larger notes remain editable locally but cannot transfer until they fit the negotiated limit. Older attachments without stored size/checksum metadata need a bounded read to seed synchronization. A legacy file larger than 512 MiB stops that scan or capture until it is migrated to immutable storage, replaced or removed.

This implements the Agent Note client. The target GaoNote environment must expose the sync receiver described in the v1 protocol. Saving a profile does not verify the receiver or its deployed raw-port configuration. See the approved design for the phase boundary and config.example.toml for all options.

PostgreSQL

PostgreSQL is a complete external storage adapter. A mandatory configuration file selecting it can use:

[database]
engine = "pg"
url = "postgresql://user:password@db/agent_note"
max_connections = 10

[embedding]
engine = "local"

[attachments]
engine = "filesystem"
path = "attachments"

Before starting Agent Note, a PostgreSQL operator with extension-management privileges must install pgvector in the target database:

CREATE EXTENSION vector;

Agent Note verifies that vector is already installed and never runs CREATE EXTENSION itself. The configured application role needs the normal schema and migration privileges required to create and update Agent Note’s tables and indexes. Title retrieval generates a tsvector with the PostgreSQL simple text-search configuration and indexes it with GIN. Dense retrieval performs an exact cosine-distance scan using `` over vector(1024) body-chunk embeddings; it creates no ANN index. System information reports the pg engine but never the PostgreSQL URL or credentials.

OpenAI-compatible BGE-M3

Use a self-hosted OpenAI-compatible embeddings service with:

[embedding]
engine = "openai"
base_url = "http://embedding.internal:8000"
model = "bge-m3"
api_key_env = "EMBEDDING_API_KEY"
timeout_secs = 30
max_retries = 3

api_key_env names an environment variable that contains the bearer token; it never contains the secret itself. Set EMBEDDING_API_KEY in the server process environment for the example above. Omit api_key_env for a service that requires no authentication. Normal remote HTTP or MCP server startup resolves the named variable and fails when its value is missing or blank. Legacy --import/--export and all four org offline modes do not resolve that value and remain embedding-service offline even when the named variable is absent.

The adapter preserves any non-root path in base_url, normalizes its trailing slash, and appends the v1/embeddings suffix. The root example above therefore posts to http://embedding.internal:8000/v1/embeddings; a base URL ending in /gateway or /gateway/ would post to /gateway/v1/embeddings. Requests use encoding_format = "float" and do not send dimensions. Each response must contain exactly one uniquely indexed, finite 1,024-component vector per input. Connection failures, timeouts, HTTP 429, and HTTP 5xx responses are retried with bounded backoff; other transport errors and HTTP statuses fail immediately. max_retries is the number of retries after the initial request and must be at most 10. A successful response body is limited to 1 MiB. Unauthenticated HTTP error bodies are limited to 4 KiB, while authenticated response bodies are redacted.

The local engine without model_path uses the deterministic stub and reports fingerprint stub:1024. A local engine with model_path and an OpenAI-compatible engine using model bge-m3 share the real vector-space fingerprint bge-m3:1024. Reconciliation has three states. A missing stored fingerprint, including in a legacy database, adopts the configured fingerprint without deleting existing vectors or jobs. An equal stored fingerprint leaves them unchanged. A present but different fingerprint—including switching from the stub to BGE-M3—atomically deletes old vectors, replaces existing embedding jobs, and queues one pending job for every active body chunk before the scheduler starts. Import and export still load the mandatory configuration and storage adapters, but use the deterministic stub internally and never contact the configured embedding service; import queues missing chunks for later processing by a normal server run.

A blank retrieval query returns no results before embedding or storage access. Normal note saves reject blank content; if an import contains blank note content, it produces no chunks or embedding jobs. The System API exposes only embedding_engine, embedding_model, and embedding_fingerprint for the embedding backend—never the base URL, API-key environment-variable name, or secret.

Attachment durability

Generated attachment bytes are stored as immutable object generations. Agent Note chooses opaque object keys and storage-generation IDs that are unrelated to the user-visible attachment path and do not contain raw note or attachment IDs. Each generated attachment persists its object_key, storage_generation, size_bytes, and SHA-256 checksum_sha256 with the normal attachment metadata. Records created before these fields existed remain readable through the legacy note-ID/path layout; new writes never publish to that mutable layout.

Create, update, attachment-put, and import operations publish and verify every new immutable object before opening the database transaction. Only then does the note CAS make those object generations authoritative for an update; creates use the same object-first transaction boundary. A pre-commit failure deletes newly published objects when possible. If the database commit result is ambiguous, Agent Note retains them as safe orphans rather than risk deleting bytes that a successful commit may reference.

Replacement, attachment deletion, and permanent note deletion enqueue cleanup intent in the durable attachment_operations table in the same database transaction as the note mutation. After a successful commit, the request path makes one best-effort cleanup pass. Cleanup failure never rolls back or changes the result of the committed user mutation; the durable intent remains claimable for a later attempt. The recurring cleanup worker, startup reconciliation, orphan doctor, and cleanup metrics are intentionally deferred to PR3 and do not exist yet.

Legacy archive import preserves its existing candidate filtering. It decodes and validates each attachment before publishing that attachment’s generated object, cleans up earlier uploads if a later decode or publication fails, and opens the database transaction only after all objects are published and verified. It also removes objects for other pre-commit failures and race-skipped notes, while retaining them when commit outcome is uncertain. Import never trusts source archives’ internal storage coordinates. Export hydration supports both legacy attachments and generated object metadata.

Filesystem attachments

The filesystem adapter publishes each generated object with an atomic no-replace operation and durably syncs the file before publication and, on Unix, syncs each containing directory entry before verifying its size and checksum. Windows has no supported directory-entry fsync equivalent, so directory-entry persistence is best-effort there while file syncing remains mandatory. The adapter refuses every pre-existing object key, even when the existing bytes match. Reads and cleanup use the persisted object key; legacy reads and deletes use the old note-ID/path layout explicitly. The configured root must be exclusively owned by this agent-note process: external mutation, symlinks, and multiple writers are unsupported.

S3 attachments

The S3 adapter supports AWS S3 and compatible object stores such as MinIO. A local MinIO configuration can use:

[attachments]
engine = "s3"
bucket = "agent-note"
prefix = "attachments"
region = "us-east-1"
endpoint = "http://127.0.0.1:9000"
force_path_style = true

For AWS S3, normally omit endpoint and leave force_path_style = false. MinIO and similar local services use their service endpoint and commonly require path-style addressing. region is optional; omitting it uses the standard AWS region provider chain. The bucket must already exist.

Credentials never belong in TOML. They come from the standard AWS credential provider chain, including AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, optional AWS_SESSION_TOKEN, shared AWS profiles, web identity, container credentials, and instance roles. The adapter allows four total SDK attempts—the initial request and at most three retries—for failures the SDK classifies as transient.

The configured prefix is prepended to Agent Note’s opaque generated object keys. S3 publication is a conditional PutObject: it never overwrites a different existing generation. Agent Note verifies the resulting object’s size and SHA-256 checksum with object metadata and reads when required; an idempotent retry is accepted only when the existing object has the same identity. A single attachment read issues one GetObject, and note/export hydration reads every object declared by the persisted metadata. Cleanup deletes exact object keys recorded by durable operations.

The System API reports backend-neutral database_engine, optional database_path and database_size_bytes, attachments_engine and optional attachments_location, plus the three safe embedding fields described above. It does not expose database credentials or embedding authentication. For S3 it reports only engine s3 and s3://bucket/prefix; it never exposes endpoint user information, profiles, access keys, secret keys, or session tokens.

Exact hybrid retrieval

The two retrieval channels cover different note fields. Rust Turso Database FTS ranks title matches with BM25, while dense retrieval uses an exact linear cosine-distance scan over stored 1024-component body-chunk embeddings and ranks each note by its closest matching chunk. The title ranking has weight 3.0 and the dense body ranking has weight 1.0; deterministic weighted RRF combines them using weight / (60 + rank) and breaks equal fused scores by note ID.

The pipeline requests clamp(requested_limit × 32, 128, 4096) ranked IDs from each channel before fusion, label filtering, hydration, and the requested top-k limit are applied. Dense retrieval uses that overfetch length directly. Embedded Turso title FTS physically materializes a fixed pool of up to 4,096 hits, batch-filters deleted notes, orders equal BM25 scores by note ID, and returns only the requested overfetch length. Its larger fixed pool bounds physical FTS work and makes ties deterministic within that pool.

PostgreSQL title FTS queries the generated simple-configuration tsvector through its GIN index, filters active notes in SQL, ranks matches with ts_rank_cd and a note-ID tie-break, and applies the requested overfetch length directly as its SQL LIMIT. Both adapters feed the resulting title and content rankings into the same weighted RRF pipeline.

The shared pipeline reads the persisted global System setting search.minimum_score, which defaults to 0.01 and must be finite and non-negative. After fusion, it retains results inclusively (score >= minimum_score) before hydrating note summaries and before applying the requested result limit. REST, the web frontend, and MCP all use this same cutoff; search requests have no per-request override. This setting does not change the retrieval weights, RRF constant, or score normalization. The System page provides the editor for this persisted value.

POST /api/notes/search and MCP semantic_search return the same search-summary fields: id, title, revision, score, labels, created_at, and updated_at. They do not return content, attachments, or deleted_at. The Notes frontend renders these results as a full table with Score | Title | Labels | Created | Updated | Actions, formatting Score to four decimal places.

Exact dense scanning avoids an approximate-index lifecycle and gives deterministic results; the accepted trade-off is linear dense-search cost, which is appropriate for the current personal-notes corpus. The PostgreSQL adapter keeps the same logical channels using title FTS and exact cosine pgvector retrieval without changing pipeline or transport code.

The adapter intentionally makes a clean break from databases created by the retired storage implementation, and no in-place legacy-data migration is guaranteed. Delete and recreate only disposable test/development databases. Export or back up any non-disposable database before upgrading so its data can be recovered or imported deliberately.

Ordinary-note revisions and concurrent mutations

Every ordinary-note read returns a positive revision. Detail, list, search, and Trash responses expose that value; clients must keep it with the data they loaded. Every mutation of an existing note is an atomic compare-and-swap and requires that caller-held value as expected_revision. A successful logical mutation increments the revision exactly once. A rejected stale mutation changes neither the note nor its labels, chunks, attachment metadata, or embedding jobs.

REST full updates include expected_revision in the PUT /api/notes/{id} JSON body. Soft delete and permanent delete send it as the expected_revision query parameter. Trash restore posts {"notes":[{"id":"...","expected_revision":5}]}; each selected note has its own token. Missing tokens are invalid requests. A stale token returns HTTP 409 using the common JSON error envelope:

{
  "code": "stale_revision",
  "message": "the note changed after it was read",
  "details": {
    "note_id": "note-id",
    "expected_revision": 5,
    "current_revision": 6
  },
  "retryable": false
}

Refetch and deliberately merge or reapply local work after a conflict; never blindly retry the stale payload. The bundled editor preserves its local draft and provides an explicit refetch and reapply action. Note and Trash delete dialogs remain open on conflict and require a reload before retrying.

MCP

Agent Note supports stateless MCP 2026-07-28 over Streamable HTTP only. /mcp exposes exactly these twelve Markdown-note tools: bulk_update_note_labels, save_note, get_note, read_note_lines, patch_note, replace_note, delete_note, list_notes, semantic_search, put_note_attachment, get_note_attachment_content, and delete_note_attachment. /org/mcp exposes the separate forty Org tools; a tool is unavailable on the other endpoint. Explicit label-key catalog management remains REST/UI-only.

Read get_note.revision before either note mutation: it is the write guard for both replace_note and patch_note. read_note_lines is optional inspection only; its tag and line numbers are not write preconditions. On revision_conflict, re-read the note and recompute the mutation, rather than swapping in the returned revision token.

replace_note is a closed full replacement: it requires id, positive expected_revision, title, complete content, complete attachments, and complete labels. Omitted collections are invalid; [] clears a collection. patch_note is a closed partial replacement: it requires id, positive expected_revision, and at least one writable field. Omitted writable fields are preserved, while supplied arrays replace the whole collection and [] clears it; explicit null and unknown fields are invalid. Its content is either a complete string or strict { "apply_patch": "..." } content. The patch uses plain @@ hunks with exact, unique context/deletions, in ordered non-overlapping locations; file directives, fuzzy matching, and unanchored edits are rejected.

Both mutations are atomic. A semantic no-op returns changed: false and preserves revision, timestamp, attachment blobs, labels, and search/embedding state. A successful mutation returns only id, revision, and changed. Malformed arguments are JSON-RPC invalid-params errors. Operational failures are isError structured {code, message, details, retryable} results with one of not_found, revision_conflict, patch_failed, invalid_request, validation_error, or internal_error.

get_note includes note data and attachment metadata only (id, path, mime, description), not attachment bytes. replace_note.attachments is the complete desired set, so omitting an existing attachment deliberately removes it on a successful replacement. To retain one, copy its metadata and exactly one payload representation fetched with get_note_attachment_content; an included metadata entry without exactly one of content or content_base64 is malformed JSON-RPC invalid-params, rejects the whole request atomically, and leaves the existing note unchanged. Standalone attachment tools and bulk-label updates remain available. REST and frontend internal line-edit behavior remain separate and unchanged.

/org/mcp exposes exactly these 40 Org tools:

org_list_workspaces       org_create_workspace      org_get_workspace
org_update_workspace      org_archive_workspace     org_list_documents
org_get_document          org_put_document           org_create_document
org_rename_document       org_archive_document       org_restore_document
org_move_document         org_move_item              org_import_workspace
org_export_workspace
org_create_item           org_get_item               org_get_item_context
org_create_follow_up      org_assign_item            org_schedule_item
org_query_queue           org_query_agenda           org_claim_item
org_heartbeat_claim       org_release_claim          org_report_progress
org_submit_result         org_transition_item        org_retry_item
org_request_review        org_approve_item            org_reject_item
org_add_dependency        org_remove_dependency      org_link_note
org_unlink_note           org_list_note_work_items   org_list_events

Client-supplied Org actor_id values are asserted audit attribution, not authenticated identities. Opaque fencing tokens are sensitive ownership proofs: only successful claim and retry/reclaim results return them. Do not log them or expose them through general reads, errors, events, or exports. Agent Note has no inbound authentication, authorization, sessions, or trusted proxy-identity-header contract; a front proxy is responsible for TLS, authentication, authorization, Host/origin, and network restrictions. The exact same 40 operation names are REST operationIds under /api/org; the Org console consumes the GET subset, workspace create/update/archive, and document-container create/rename/archive/restore. Raw source and workflow mutations remain absent, and the console adds no application-authentication behavior.

Org offline commands

The binary provides four storage-only modes:

note-server org export-workspace --workspace-id  --output 
note-server org import-workspace --input  --mode create|update \
  --actor-id  --operation-id 
note-server org export-document --document-id  --output 
note-server org import-document --workspace-id  --document-id  \
  --path  --input  --mode create|update \
  --actor-id  --operation-id  [--expected-revision ]

Document update requires --expected-revision; create rejects it and rejects an existing ID. Workspace update uses the workspace and document revisions embedded in its export. A workspace snapshot has this portable layout:

manifest.json
documents/.org

The manifest records its format version, workspace metadata/policy/revision, and each document’s stable ID, canonical Org path, revision, content hash, nullable archived_at, and relative UUID filename. Workspace snapshots include active and archived documents; omission of archived_at in a legacy manifest means active, while ordinary single-document import cannot set lifecycle state. Imports validate the complete snapshot before mutation; exports write atomically and do not implicitly replace a non-empty destination. On Linux, Android, and Apple-vendor targets, publication uses an atomic NOREPLACE rename. On Windows and other targets without that primitive, exports fail closed with an Unsupported publish error; imports remain supported. Every mode writes a machine-readable report to stdout, starts only storage, and does not initialize attachments, embeddings, workers, HTTP, MCP, or Markdown-note data.

The HTTP server exposes MCP 2026-07-28 Streamable HTTP at /mcp for Markdown-note tools and /org/mcp for Org tools. There is no stdio MCP mode.

note-server binds its backend to 0.0.0.0:6222 by default, and the Debug Trunk server binds to 0.0.0.0:6221, so the development UI, REST API, and MCP endpoints are reachable from the local network. Run them only on a trusted network. Packaged builds do not start Trunk and retain their configured bind/static-directory behavior.

View this README on GitHub

설치

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

Open the repository installation guide