---
title: KMS (the Vault) — notes, editor, search and sharing (part 6)
---

# KMS (the Vault) — notes, editor, search and sharing (part 6)

## What it is

This is part 6, the last part, of the [KMS](kms.md) 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](/.claude/memory/contracts/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](/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](/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](/src/main/services/kms/parser.ts) and the watcher at [/src/main/services/kms/watcher.ts](/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](/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](/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](/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](/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](/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](/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](/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](/src/renderer/src/features/kms/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](/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/](/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](/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/](/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/](/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](/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](/tests/e2e/fixtures/kms-vault-fixture.ts) 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](/src/main/services/kms/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](/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](/.claude/memory/contracts/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](kms-obsidian-import.md) for full detail.

## Related

Earlier parts: the [vault itself](kms.md), [the editor](kms-part-2.md), [appearance and search](kms-part-3.md), [images and links](kms-part-4.md), and [AI, windows and sharing](kms-part-5.md). Importing a whole existing vault has its own page, [KMS Obsidian Import Wizard](kms-obsidian-import.md).
