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
- Install Rust (stable), Trunk (
cargo install trunk), and thewasm32-unknown-unknowntarget (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 Homebrewrustformula on yourPATH, otherwisecargoresolves to a single-target rust that can’t build wasm andtrunk buildfails.
- Note: if Rust is managed by rustup installed via Homebrew, rustup is keg-only — put
- (Optional, for real local embeddings) Download the BGE-M3 int8-quantized ONNX model:
The whole app runs without it using a deterministic stub embedder. To use the real model, edit the generatedhf download gpahal/bge-m3-onnx-int8 --local-dir ./models/bge-m3-int8.onnxdev-data/config.tomlso its embedding table is:[embedding] engine = "local" model_path = "../models/bge-m3-int8.onnx/model_quantized.onnx"NOTE_MODEL_PATHis the fallback whenmodel_pathis omitted. Embedding inference uses all detected physical CPU cores by default; setNOTE_EMBEDDING_THREADSto a positive integer to override it, or explicitly set it toautoto 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)
- Install Trunk once:
cargo install --locked trunk - From the repo root, start the backend and hot-reloading frontend together:
Then open http://0.0.0.0:6221. Trunk proxiescargo run/api,/mcp, and/org/mcptonote-serveron127.0.0.1:6222. A debug build atomically creates./dev-data/config.tomlon first start, without replacing an existing file. Its generated settings store the embedded Turso database at./dev-data/notes.dband attachments under./dev-data/attachments. A note’s unrendered Markdown is available atGET /api/notes/{id}/rawandGET /notes/{id}/content. During local development, use the backend URLhttp://127.0.0.1:6222/notes/{id}/contentfor the latter because Trunk owns the frontend/notes/*routes on port 6221. Add?type=htmlto 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, defaultembed); forembed,path(NOTE_DB_PATH, defaultnotes.db); forpg,url(DATABASE_URL) andmax_connections(NOTE_DB_MAX_CONNECTIONS, default10). The embedded adapter is Rust Turso Database.[embedding]:engine(NOTE_EMBEDDING_ENGINE, defaultlocal); forlocal, optionalmodel_path(NOTE_MODEL_PATH). No model path selects the deterministic stub; a path selects local BGE-M3 ONNX inference. Foropenai,base_url(NOTE_EMBEDDING_BASE_URL),model(NOTE_EMBEDDING_MODEL, defaultbge-m3), optionalapi_key_env(NOTE_EMBEDDING_API_KEY_ENV),timeout_secs(NOTE_EMBEDDING_TIMEOUT_SECS, default30), andmax_retries(NOTE_EMBEDDING_MAX_RETRIES, default3).[attachments]:engine(NOTE_ATTACHMENTS_ENGINE, defaultfilesystem); forfilesystem,path(NOTE_ATTACHMENTS_DIR, defaultattachments). Fors3,bucket(NOTE_S3_BUCKET),prefix(NOTE_S3_PREFIX, default empty), optionalregion(AWS_REGION), optionalendpoint(NOTE_S3_ENDPOINT), andforce_path_style(NOTE_S3_FORCE_PATH_STYLE, defaultfalse).
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.
インストール
This server does not publish a one-line install command.
Open the repository installation guide