Omniscio documentation
Browse all documentation
  1. Getting Started13
  2. Sessions & Agents115
  3. Inbox & Notifications59
  4. Projects & Tasks95
  5. Automation & Scheduling75
  6. Knowledge & Memory26
  7. AI Features60
  8. Integrations100
  9. Plugins & Marketplace33
  10. Cloud & Teams56
  11. Settings & Customization58
  12. Account & Billing28
  13. Troubleshooting84
  14. CLI & API Reference22
  15. Legal & Policies4
  16. Uncategorised22

KMS (the Vault) — notes, editor, search and sharing (part 6)

Part 6 of the KMS page: using the vault away from the desktop and under the hood — the mobile experience, what happens after a crash, what is deliberately not built yet, the command surface an agent or script calls, how the vault is put together, and what it promises about your notes.

What it is

This is part 6, the last part, of the KMS page. It covers the vault on a phone, recovering it after a crash, the features deliberately left out, the command surface underneath it, and the two statements worth reading before trusting it with anything important: how it is built and what it promises about your data.

Where to find it

On mobile the vault appears through the app's mobile layout, and everything else here is behind the scenes — read from a checkout rather than from a screen.

How it behaves

Mobile UX

On a narrow, touch viewport (useIsMobile() — ≤767px + touch; the web-access companion and small windows) the KMS panel swaps its desktop chrome for a purpose-built mobile layout (the "Writer focus" direction). Desktop is unchanged — every surface is gated behind isMobile.

  • Minimal header (KmsMobileHeader) replaces Omniscio's generic project breadcrumb (suppressed for the KMS project via shouldShowMobileBreadcrumb): ☰ Open notes · note title · ⋯ More.
  • Notes drawer (KmsNotesDrawer) — ☰ slides the file tree (FileTreePanel, unchanged) in over the editor with a backdrop scrim; tapping a note opens it and auto-closes the drawer (KmsView watches activeTabId).
  • Swipeable tabs — on mobile TabBar drops the measured-overflow +N dropdown and becomes one horizontally-scrollable momentum row, so every open note is reachable by swipe.
  • Action sheet (KmsActionSheet) — ⋯ (or the quick bar's "Aa More") slides up a bottom sheet holding every editor control: a Format grid built from the same TOOLBAR_GROUPS catalog the desktop toolbar renders (a completeness test pins it, so nothing can be unreachable on mobile), plus a View section (Outline & panels · Find · Source view · Bookmark) wired to the same hooks the desktop View/Bookmark groups use.
  • Quick-format bar (KmsQuickFormatBar) — a slim bottom strip shown only while the editor has focus, with two regions: a horizontally-scrolling icon row (MOBILE_QUICKBAR_COMMAND_IDS: Bold · Italic · Bullet · Numbered · Undo · Redo) and an always-visible pinned cluster (MOBILE_QUICKBAR_PINNED_COMMAND_IDS: Decrease indent · Increase indent) held outside the scroll region, beside the pinned "Aa More" button. Pinning the indent pair (2026-06-15) fixes a reported "I don't see any indent buttons on my phone": they used to be the last two of eight scrolling icons, parked just past the (hidden-scrollbar) right edge — invisible with no cue to swipe. undo/redo now trail the scrolling row (still one tap away under "Aa" if they scroll off). Decrease / Increase indent are the only way to change a list item's nesting on mobile — there's no Tab key — and they reuse the exact indentListItem / outdentListItem helpers the desktop Tab / Shift+Tab keys call (contract I8), greying out when the caret isn't inside a list. The bar subscribes to the editor's transactions (guarding isDestroyed) so the undo/redo + indent enable-state stays live without risking the torn-down-editor crash; link, headings, and the rest stay one tap away in the action sheet.
  • Multi-column blocks become a one-per-screen carousel — a :::columns block (2–5 side-by-side columns on desktop) can't fit on a phone, so at ≤767px each column fills the full pane width and the block scrolls sideways with scroll-snap, showing exactly one column per screen (swipe to the next; the page itself never scrolls sideways). To make this overridable without an !important hack, ColumnBlock.renderHTML emits the per-block track widths as a --kms-column-tracks custom property — so the stylesheet (not an inline grid-template-columns) owns the layout, and the mobile media query can collapse display: grid → display: flex (.column-item { flex: 0 0 100% }); desktop stays a byte-for-byte grid. Snap is proximity (not mandatory) so it never yanks the scroll or traps the caret while editing. (Revises the 2026-06-11 "12rem side-by-side scroll" — at 12rem the visible column sat ~190px and the next was cut off mid-content.) Pinned in kms-editor-styling-contract.md (content-fits-the-pane) + kms-editor-css-contract.test.ts.

The desktop formatting toolbar and the status bar are hidden on mobile (the sheet + quick bar replace them). All tap targets are ≥44px; shared class strings live in features/kms/styles.ts as KMS_MOBILE_*. Bookmark add/remove is shared with the desktop button via the useBookmarkToggle hook. Because these surfaces float over the note text (not the app gradient like the desktop panes), they share the KMS_MOBILE_PANEL_SURFACE opaque floor (.kms-mobile-panel-surface in globals.css) instead of a translucent --panel-bg + blur — otherwise the text bleeds through on mobile, where backdrop-filter is unreliable (contract I13).

AI sessions on mobile (2026-06-09). The KMS sub-sidebar previously hard-returned only <FileTreePanel /> on mobile (if (isMobile) return <FileTreePanel />), so a phone user could browse notes but never reach the vault's AI sessions. KmsSubSidebar's isMobile branch now renders MobileKmsPanel — the same Files | Sessions toggle desktop has, via the shared KmsViewTabStrip (one toggle component for both surfaces). Files is the notes tree (the unchanged default tab); Sessions is the vault's agent sessions through the shared SessionHostSidebar, so the session sort, rename/pause/archive context menu, and status dots are all inherited (no hand-rolled rows). Tapping a session — or + New, which launches into the list rather than auto-jumping (mirroring SMS, since the session-host factory's onSpawn is fire-and-forget) — drills the mobile nav into session-detail; that detail slot yields the SessionPanel chat instead of the KmsView editor when the active integration's registry shouldYieldToSessionChat predicate returns true. KMS's predicate reuses the same pickKmsMainPane decision (Sessions view + a session), which Dashboard resolves once into the generic integrationYieldsToSessionChat boolean both shared layouts consume — so desktop and mobile agree and the gate stays a no-op for every non-yielding virtual project. The invariant is pinned in session-host-contract.md (KMS consumer section), ai-coaching-mobile-layout-contract.md, + MobileKmsPanel.test.tsx.

Crash recovery

If Omniscio crashes (OS kill, power loss, renderer process death) while you have unsaved edits, those edits are recoverable on the next launch. A 2-second heartbeat writes the editor's pending content to a nothari_note_drafts SQLite table while any note has unsaved changes. On relaunch, if a draft's timestamp is newer than the on-disk note's updatedAt, a persistent toast appears: "Recovered unsaved edits for [note names]" with Restore all / Discard actions. Restoring loads the draft body back into the editor, marks it dirty, and triggers autosave; discarding deletes the draft rows. Drafts older than 7 days are auto-pruned. A successful autosave clears the matching draft immediately, so drafts only persist across a crash — they never accumulate during normal use. See kms-crash-recovery-contract.md.

What's still deferred or cut

Cut from scope (not shipping, not planned for follow-on):

  • Phase 4 — Tasks UI. Recurrence engine, calendar view, smart-date inputs, OutlineTasks panel. The vault is a notes tool, not a project-management tool — tasks-in-Markdown didn't justify the surface area.
  • Phase 6h/6i/6j — Destructive agent tools. nothari_append, nothari_create_note, nothari_set_tags, nothari_archive_note, nothari_delete_note with an approval queue. The read-only five cover the agent's actual usage pattern; destructive operations are still available as bash file-system tools.

Deferred (worth doing later as follow-on PRs, not shipping-critical):

  • Phase 7 — Quick Compose. Frameless capture window with three tabs (note/task/idea), append mode. (Note: Ctrl+Alt+N now opens the standalone KMS window, shipped separately — see "Standalone KMS window" above.)
  • Phase 8 — Polish (10 items remaining). Shortcut help dialog, vault export (MD/JSON/ZIP), web clipping import, templates with {{date}}/{{cursor}} placeholders, bookmark folders, hidden notes admin (bulk-unhide), print stylesheet, per-vault LLM.md generator, Sentry wiring with redaction filter. (Theme integration shipped — see "Custom theme options (appearance)" above. Crash recovery shipped — see "Crash recovery" below.)
  • Phase 9 — Full test port. Porting the source vault's ~1,580 Rust + Vitest tests into Omniscio was scoped out in favor of trusting the existing Omniscio test layer plus the integration's per-IPC unit tests. The 4-tier Playwright sweep, 15% perf regression budget, and the four new shipping-gate lint rules are also deferred.

Existing structural deferrals (carried over from earlier phases):

  • Embedding / semantic search. KMS ships FTS5 only — both note and image search are bag-of-words bm25.
  • Multiple vault roots. Omniscio supports exactly one nothariVaultRootPath.
  • Migration UI for importing notes from the KMS desktop database (the source Tauri app).
  • Live push sync on web/mobile. The KMS push channels (kms:note-changed, kms:image-analyzed, kms:summary-updated, etc.) remain desktop-only (in DESKTOP_ONLY_CHANNELS), so a web-access client renders the full KMS UI (now with a dedicated mobile layout — see "Mobile UX" above) but does NOT receive live cross-client updates; it reflects edits on the next load / note reopen rather than in real time.
  • Image archive column. The Unified Search filter schema accepts includeArchived as a reserved knob, but no archived column exists on nothari_notes yet — the flag is a no-op until that migration lands.

IPC channels + the HTTP CLI surface

KMS has two distinct surfaces, easy to conflate:

  • IPC channels (kms:*, listed below) are the renderer↔main contract the in-app editor uses. They are not reachable by a spawned agent or an external script — they need the Electron preload bridge, not HTTP. (Earlier drafts of this page wrongly called these an "agent CLI surface"; they are not.)
  • HTTP CLI control-server routes (/kms/*) are the agent-reachable surface on 127.0.0.1:19519. They reuse the same db queries + write service as the IPC handlers, so the two cannot drift. Added 2026-06-08 as a curated read + safe-capture subset.

HTTP CLI control-server routes (agent-reachable, 2026-06-08)

Seven routes, gated by nothariEnabled — when KMS is off they return 403 with code: "KMS_FEATURE_DISABLED" (released-but-off, not 404):

  • GET /kms/vaults — list registered vaults.
  • GET /kms/notes?vaultId= — list a vault's notes (metadata only, no body).
  • GET /kms/notes/:id — read one note with its body.
  • GET /kms/notes/:id/backlinks — notes that wiki-link to this note.
  • GET /kms/search?q=&vaultId=&limit= — FTS5 search (all vaults when vaultId omitted).
  • GET /kms/tags?vaultId= — distinct tags + per-tag counts.
  • POST /kms/notes — create a note (atomic write, vault-contained — the only write).

Vault is resolved: explicit vaultId → the configured vault → the single registered vault → otherwise 409. Destructive / billable operations are deliberately NOT on this surface (overwrite, rename, move, soft-delete, tag mutation, image import, AI analysis / summaries) — so there is no delete/overwrite path to abuse from the CLI. Invariants: kms-cli-routes-contract.md. Full route reference + curl examples: the omniscio-control skill's kms.md spoke.

IPC channels — read (renderer↔main; Phase 1)

  • kms:vault-list — returns { vaults: KmsVault[] } summarizing every registered vault (one in Phase 2).
  • kms:note-read — input { noteId } → returns { note: KmsNote } | null. Populates body (up to 5 MB; larger files surface body: undefined with bodyBytes set so callers can decide whether to slurp via the filesystem instead).
  • kms:search — input { query, limit?, vaultId? } → returns { hits: KmsSearchHit[] } ranked by FTS5 bm25() (lower = stronger match). snippet is the FTS5 snippet() output with <mark> tags.
  • kms:health — returns KmsHealthSnapshot summarizing per-vault index state (root exists, root is symlink, health enum ok | pending | error, last index timestamp, last error string).
  • kms:note-list — input { vaultId? } → returns { notes: KmsNoteSummary[] }. The renderer's file tree consumer.

IPC channels — write (renderer↔main; Phase 2)

  • kms:note-create — input { vaultId, relativePath, body? } → returns { noteId, relativePath }. Creates a new .md file under the vault root with optional initial body. Rejects paths that already exist or that resolve outside the vault.
  • kms:note-write — input { noteId, body } → returns { noteId, bytesWritten }. The autosave path. Idempotent body overwrite — same body in, same file out, no spurious mtime bump if the bytes match. The renderer's kms-store debounces calls at 500ms.
  • kms:note-rename — input { vaultId, oldRelativePath, newRelativePath } → returns { noteId, newRelativePath, rewroteLinks: number }. Renames the file, updates the nothari_notes row, and rewrites every [[wiki-link]] back-reference (whether [[notes/old]] or [[notes/old|Alias]]) in every other note that referenced it. The whole operation runs in a single SQLite transaction so a mid-rename crash either lands every link rewrite or none.
  • kms:note-move — input { vaultId, oldRelativePath, newParentDir } → returns { note: KmsNote }. Relocates the note FILE into a new parent folder (newParentDir: '' = vault root) and rewrites every [[wiki-link]] back-reference in the same transaction (like rename). Unlike rename the input is a destination FOLDER, not a full path, and there is no ordering / beforePath parameter — the file-tree drag-and-drop sends exactly this shape (so a dropped note relocates folders only, never reorders).
  • kms:note-soft-delete — input { vaultId, relativePath } → returns { success: true }. Moves the file into <vaultRoot>/.trash/<timestamp>__<basename> and removes the nothari_notes row. The vault root file watcher ignores .trash/ so the move doesn't ping-pong back through the indexer.

Tabs (new in Phase 2 — nothari_tabs table, v194)

The renderer mirrors its open-tabs strip into a small table so relaunches restore the user's last working set. All four routes gate on nothariEnabled and respect the standard is_deleted = 0 soft-delete contract.

  • kms:tab-list — input { vaultId? } → returns { tabs: KmsTab[] } sorted by display_order (0-based, left-to-right slot in the strip).
  • kms:tab-upsert — input { vaultId, notePath, isPinned?, displayOrder? } → returns { tab: KmsTab }. Idempotent — opening an already-open note bumps last_active_at and re-uses the row.
  • kms:tab-delete — input { tabId } → returns { success: true }. Soft-deletes the row (is_deleted = 1).
  • kms:tab-reorder — input { vaultId, orderedTabIds } → returns { success: true }. Bulk reassigns display_order to match the array, in a single transaction.

Pinning a tab (desktop). Pinned tabs sort first as a block and stay visible when the strip overflows, and the set survives a relaunch (is_pinned above). The on-ramp is a right-click context menu on any tab — Pin tab / Unpin tab plus Close tab. (Before 2026-06-20 the only pin affordance was an inline unpin glyph that rendered solely on already-pinned tabs, so a fresh tab could never be pinned from the UI — the backend shipped in Phase 2 but sat dormant for want of this entry point.) The menu reuses the shared EditorContextMenu (portal + viewport-clamp + Esc / outside-click + keyboard nav) the file tree and editor body already use. The mobile strip omits it — right-click / long-press there is reserved for the Phase-9 multi-select gesture, so its <Tab/> rows render without the handler.

Push events

  • kms:note-changed — fires when the file watcher OR a write-side handler updates a note. Payload: { vaultId, noteId, relativePath, op: 'added' | 'updated' | 'removed' }. Desktop-only listener (the channel is in the lint test's DESKTOP_ONLY_CHANNELS allow-list).

How it works

Backend

Data lives in six tables. Phase 1 added five (migration v193): nothari_vaults (one row per vault root), nothari_notes (one row per .md file), nothari_tags (denormalized (note_id, tag) for #hashtag lookups), nothari_links (denormalized [[wiki-link]] occurrences with target_title and best-effort resolved_note_id), and nothari_notes_fts (FTS5 external-content virtual table indexed on title + body). Phase 2 adds the sixth (migration v194): nothari_tabs (id, vault_id, note_path, is_pinned, display_order, opened_at, last_active_at, is_deleted, with FK CASCADE on vault_id so unregistering a vault sweeps the tabs).

The query module at /src/main/db/queries-kms/index.ts carries the Phase 1 read paths plus the new write paths (createNote, writeNote, renameNote, moveNote, softDeleteNote). The tabs queries live alongside in /src/main/db/queries-kms-tabs.ts. Every user-supplied value is parameterized (? placeholder); the only string interpolation is hard-coded identifier names (per the project SQL rule). The 5 MB body cap and the 1000-link-per-note cap from Phase 1 are still enforced at write time.

The parser at /src/main/services/kms/parser.ts and the watcher at /src/main/services/kms/watcher.ts are unchanged from Phase 1 (the watcher's ignore list gained .trash/ so soft-deletes don't re-enter the indexer). The indexer at /src/main/services/kms/indexer.ts walks the vault root, calls the parser per file, and writes the rows in a single transaction. New in Phase 2: /src/main/services/kms/note-crud-service.ts owns the safe-write contract (resolveInsideVault() + atomic writeFileSync + post-write reindex of just the touched row) and the wiki-link back-reference rewriter (single-pass over nothari_links, batched into one transaction per rename).

The IPC handlers at /src/main/ipc/kms-handlers.ts wrap every Phase 2 route through wrapHandler with Zod input validation and path-validation. The lifecycle (parser + watcher) is still started by /src/main/index.ts only when settings.nothariEnabled === true and settings.nothariVaultRootPath resolves to a real directory — both checks fail closed.

Frontend (new in Phase 2)

The KMS panel mounts in /src/renderer/src/features/kms/KmsView.tsx. The view itself is small; the heavy lifting is in three lazy-loaded children:

Layout (post-2026-05-21 rework): KMS no longer renders its own three-pane grid. The file tree is registered as the integration's sidebarComponent (/src/renderer/src/features/kms/KmsSubSidebar.tsx) so it mounts in Omniscio's native Pane 2 slot via the standard RoutedSubSidebar — same shape as Tools, Skills, Alarms, AI Coaching. On desktop the sub-sidebar toggles between Files (the tree) and Sessions (/src/renderer/src/features/kms/sessions/KmsSessionsList.tsx) — the Sessions view lists the vault's AI sessions, each rendered with the shared SidebarSessionRow so a KMS session carries the SAME controls as a main-sidebar session: middle-click to archive, the right-click context menu (archive / snooze / pause / move), the hover ⋯ menu, status dots, and Ctrl+Z / Undo (it reuses the shared useCloseSession hook; single-select, no drag-reorder). The panel body is a single editor column. The right-pane panel stack (Outline / Backlinks / Smart Connections / Tags / Bookmarks / Hidden) is a slide-out drawer (KmsRightPaneDrawer.tsx) toggled by the PanelRight button in the editor toolbar's View group; open state persists via appSettings.nothariRightPaneOpen (default false). The drawer mounts INSIDE the editor body region — below the toolbar + tab bar — so it overlays only the editor and never covers the header (its toggle stays clickable while open). It animates in/out via transition-transform + translate-x (200ms, reduced-motion aware, mirroring the mobile KmsNotesDrawer) and lazy-mounts its panels on first open (a never-opened drawer fires no per-note IPC). Because its toggle lives in the note-only toolbar, the drawer is reachable only while a note is open. (Before the 2026-06-09 rework it mounted/unmounted instantly at top-0, covering the toolbar.) Prior to the rework KMS shipped with panelOwnsLayout: true and an in-panel left column, which duplicated DOM rows whenever a sidebar entry was registered for the same integration; the rework removes the in-panel column and adds the registry entry simultaneously, so the two changes can't drift apart.

  • ./editor (KmsEditor.tsx) — the TipTap-based markdown editor. React.lazy()'d behind the panel so the ~150 KB gzipped TipTap + StarterKit + Markdown + 17 custom extensions bundle never enters the entry chunk when KMS is disabled. The lazy boundary is enforced by /tests/unit/build-output/kms-view-lazy-load.test.ts — a source-scan fails CI if anything outside features/kms/editor/** statically imports KmsEditor, and a build-output assertion fails CI if extension name: tokens leak into the entry chunk.
  • ./toolbar/EditorToolbar — lazy too, for the same TipTap-dep reason. Renders the collapsed single-row layout described above (Bold · Italic · Heading▾ · Bullet · Link · ⋯ More). Subscribes to selection / transaction updates via useEditorState with a destroyed-editor guard (ed && !ed.isDestroyed) so the selector never dereferences a torn-down editor; every button additionally short-circuits its onClick on editor === null || editor.isDestroyed. The shared dropdown primitive lives at ./toolbar/ToolbarMenu.tsx.
  • ./context-menu/EditorContextMenu, ./popovers/LinkPopover, ./popovers/ImagePopover — lazy, each opens-on-demand only.

The 19 custom TipTap extensions in /src/renderer/src/features/kms/editor/extensions/ cover behavior the upstream StarterKit doesn't (column blocks, hidden blocks, task blocks, vault-relative image rendering, wiki-link suggestion, undo grouping, block inserter, search highlight, URL paste handler, link normalizer, auto-capitalize, block escape, markdown shortcuts, no-join ordered list, ordered-list numbering continuation, slash command, keyboard shortcuts, tables with GFM roundtrip, frontmatter passthrough via the Markdown extension).

Editor state lives in the Zustand store at /src/renderer/src/features/kms/kms-store.ts. The store owns: the open tabs (mirrored to nothari_tabs via the kms:tab-* IPC channels), the active tab, the in-flight autosave timer (500ms debounce per tab), the dirty bit per tab, and the cached body for each open note. On kms:note-changed from the watcher, the store invalidates the affected note's cache and (if it's the active tab) re-reads it into the editor — but only if the local dirty bit is false, so a concurrent external edit can't clobber in-progress user typing.

The find/replace overlay at /src/renderer/src/features/kms/find-replace/ hooks into the editor via the SearchHighlight extension; the source view toggle at /src/renderer/src/features/kms/source-view/ swaps the TipTap surface for a raw textarea while preserving the round-trip through the markdown serializer.

The status bar at /src/renderer/src/features/kms/status-bar/StatusBar.tsx is one of the few KMS files statically imported by KmsView. It uses @tiptap/react's useEditorState for word / char count, which would normally pull @tiptap/react into the entry chunk — but @tiptap/react is already in the entry chunk via the type-only import type { Editor } from '@tiptap/core' shared across the panel, so the static import is layout-stable without a bundle regression. (The heavy bits — StarterKit, @tiptap/markdown, and all 17 extensions — stay in the editor chunk; the lazy-load test only asserts those tokens, not @tiptap/react identifiers.)

Privacy + safety contract

  • Omniscio writes only inside the configured vault root. Every write-side IPC handler runs the caller-supplied path through resolveInsideVault() which rejects .. traversal, absolute paths outside the root, system paths (Windows: C:/Windows, C:/Program Files; POSIX: /etc, /sys, /proc), and symlinks. The pre-Phase-2 data-loss incidents (2026-03-23, 2026-04-06) destroyed the user's "Primary Page" via CDP / Playwright dispatching events into a live editor without isolation — Phase 2's E2E spec is structurally protected from re-running that pattern: the vault fixture mints every test vault under os.tmpdir() and throws before seeding if the resolved path is anywhere else.
  • Soft-delete is non-destructive, but it is not permanent — the trash is swept. kms:note-soft-delete moves the file into <vaultRoot>/.trash/<timestamp>__<basename> rather than unlinking, so a deleted note's body is recoverable by hand (or via a future "Trash" UI surface) without restoring from external backup. That recovery window is time-limited: the periodic retention tick runs pruneKmsTrashOlderThan() (trash-retention.ts), which deletes every regular file sitting directly inside a vault's .trash whose mtime is older than your Settings → General → Data Retention (days) value (default 30 days, range 1–365). Past that window the note body is gone for good — .trash is excluded from the indexer and from the backup mirror, so no other copy exists inside Omniscio. Copy anything you want to keep out of .trash before the window closes. Not surfaced in the App UI, and it only ever touches regular files directly inside .trash (never a subdirectory, never a live note). Operator kill switch: AMC_DISABLE_KMS_TRASH_PURGE=1.
  • External edits don't clobber in-progress typing. The watcher's kms:note-changed push triggers a re-read of the active tab only when the local dirty bit is false. If the user is mid-edit, the external change is recorded in the DB but the editor body stays untouched until the next clean state.
  • Omniscio's own saves don't masquerade as "another window". The vault watcher fires kms:note-changed for every on-disk .md change — including Omniscio's own autosave / summary / image writes. Main fingerprints the bytes of each in-app write (/src/main/services/kms/self-write-tracker.ts); the watcher's re-index of an echo whose on-disk bytes still match that fingerprint refreshes tags/links/title but does NOT advance the note's optimistic-concurrency version token (updated_at) — which otherwise produced a false "This note changed in another window" conflict. Because it matches on CONTENT, it recognizes EVERY echo of a rapid autosave burst (fast typing, or a note open in the KMS pop-out as well as the main window), not just the first. A genuine external / second-window / mobile edit (different bytes) still bumps the token, so real conflicts are still detected. Invariants in kms-self-write-echo-contract.md.
  • 5 MB body cap on kms:note-read (unchanged from Phase 1). Files larger than the cap return body: undefined with bodyBytes populated so the renderer can offer "open in your default editor" instead of attempting to render in TipTap.
  • 1000-link cap per note prevents pathological notes from exploding the nothari_links index (unchanged from Phase 1).
  • No LLM spend in Phase 2 — no embedding model loads, no Haiku calls, no MCP server registration. Q5 carve-out (log every LLM call when added) only applies once a future phase wires an LLM caller.

Importing from Obsidian

A guided wizard at Settings → Features → KMS → Import from Obsidian walks users through a one-time migration from an Obsidian vault. It copies .md files (preserving folder structure), content-addressed images (via the existing importImageBytes asset store), and rewrites Obsidian ![[image.png]] embeds to standard ![](assets/<sha>.ext) Markdown. .obsidian/, .git/, .trash/, and node_modules/ directories are skipped; .canvas files and non-image attachments are listed as skipped. Name collisions with existing vault notes use auto-numbering. The original Obsidian files are never modified. See kms-obsidian-import.md for full detail.

Related

Earlier parts: the vault itself, the editor, appearance and search, images and links, and AI, windows and sharing. Importing a whole existing vault has its own page, KMS Obsidian Import Wizard.

Last verified 2026-09-28