documentation update

This commit is contained in:
Storme-bit
2026-04-18 23:37:32 -07:00
parent 1fc6e8a66d
commit e1375e7d1b
5 changed files with 145 additions and 98 deletions

View File

@@ -32,8 +32,7 @@ here for reference and direct debugging use.
``` ```
`model` and `temperature` are optional. Inference parameters (temperature, `model` and `temperature` are optional. Inference parameters (temperature,
topP, topK, repeatPenalty) are read from `settings.json` on every request — topP, topK, repeatPenalty) are read from `settings.json` on every request —
the request body values are not used for these; they are controlled via controlled via `PATCH /settings`.
`PATCH /settings`.
**POST /chat — response:** **POST /chat — response:**
```json ```json
@@ -91,7 +90,7 @@ Returns `{ sessionId, episodes: [...] }`. Episodes ordered newest first.
|---|---|---| |---|---|---|
| GET | /projects | Get all projects | | GET | /projects | Get all projects |
| POST | /projects | Create a new project | | POST | /projects | Create a new project |
| PATCH | /projects/:id | Update a project | | PATCH | /projects/:id | Update a project (partial — any subset of fields) |
| DELETE | /projects/:id | Delete a project (nulls session assignments) | | DELETE | /projects/:id | Delete a project (nulls session assignments) |
**POST /projects — body:** **POST /projects — body:**
@@ -101,13 +100,27 @@ Returns `{ sessionId, episodes: [...] }`. Episodes ordered newest first.
"description": "Optional description", "description": "Optional description",
"colour": "#3d3a79", "colour": "#3d3a79",
"icon": null, "icon": null,
"isolated": 0 "isolated": 1
} }
``` ```
`name` is required. All other fields optional. `isolated` is `0` or `1`. `name` is required. All other fields optional. `isolated` is always `1`
Returns `201` with the created project object. all projects use isolated memory. Returns `201` with the created project object.
**PATCH /projects/:id — body:** same fields as POST, all optional. **PATCH /projects/:id — body:** any subset of fields, all optional.
| Field | Type | Description |
|---|---|---|
| `name` | string | Project name |
| `description` | string | Project description |
| `colour` | string | Hex colour for UI accent |
| `icon` | string | Icon identifier |
| `isolated` | integer | Memory isolation flag (always 1) |
| `notes` | string | User-authored project notes |
Only provided fields are updated — omitted fields are not touched. This
enables safe partial updates (e.g. saving just `notes` without affecting
`name` or `colour`). Both orchestration and memory service implement dynamic
field patching.
### Models ### Models
@@ -127,8 +140,9 @@ with `models.json` in the same folder for label and description metadata.
```json ```json
{ "contextWindow": 64000, "modelAlias": "gemma-4-26B-A4B-Claude-Distill-APEX-I-Mini.gguf" } { "contextWindow": 64000, "modelAlias": "gemma-4-26B-A4B-Claude-Distill-APEX-I-Mini.gguf" }
``` ```
Fetches directly from llama-server `/props`. Returns `503` if llama-server Fetches directly from llama-server `/props`. `n_ctx` is at
is unreachable. `data.default_generation_settings.n_ctx` in the llama-server response.
Returns `503` if llama-server is unreachable.
### Settings ### Settings
@@ -218,8 +232,7 @@ orchestration.
```json ```json
{ "name": "Session Name", "projectId": 3 } { "name": "Session Name", "projectId": 3 }
``` ```
Both fields are optional. Only provided fields are updated — other fields Both fields are optional. Only provided fields are updated.
are not touched.
### Episodes ### Episodes
@@ -251,7 +264,7 @@ are not touched.
| POST | /projects | Create a new project | | POST | /projects | Create a new project |
| GET | /projects | Get all projects | | GET | /projects | Get all projects |
| GET | /projects/:id | Get project by ID | | GET | /projects/:id | Get project by ID |
| PATCH | /projects/:id | Update a project | | PATCH | /projects/:id | Update a project (dynamic — any subset of fields) |
| DELETE | /projects/:id | Delete project + null session assignments | | DELETE | /projects/:id | Delete project + null session assignments |
Same request/response shape as orchestration `/projects` above. Same request/response shape as orchestration `/projects` above.

View File

@@ -1,15 +1,16 @@
# Memory Isolation # Memory Isolation
NexusAI implements project-scoped memory — sessions belonging to the same NexusAI implements project-scoped memory — sessions belonging to the same
project can share semantic context, and isolated projects can be restricted project share semantic context within that project's boundary. All projects
from drawing on memory outside the project. This document describes how the are isolated by default.
system works end-to-end.
## Concepts ## Concepts
**Session** — a single conversation thread. Identified by `external_id`. **Session** — a single conversation thread. Identified by `external_id`.
**Project** — a named grouping of sessions. Has an `isolated` flag (0 or 1). **Project** — a named grouping of sessions. `isolated` is always `1`
the toggle has been removed from the UI and `isolated: 1` is hardcoded on
project creation.
**Semantic search** — at inference time, the user's message is embedded and **Semantic search** — at inference time, the user's message is embedded and
compared against past episodes in Qdrant to surface relevant context. The compared against past episodes in Qdrant to surface relevant context. The
@@ -20,11 +21,10 @@ scope of this search is controlled by the project context.
| Session state | Semantic search scope | | Session state | Semantic search scope |
|---|---| |---|---|
| No project | Own session's episodes only | | No project | Own session's episodes only |
| Assigned to a non-isolated project | All episodes across all sessions in the project | | Assigned to a project | All episodes across all sessions in that project |
| Assigned to an isolated project | All episodes within the project only |
| Removed from a project | Own session's episodes only (from that point) | | Removed from a project | Own session's episodes only (from that point) |
Sessions with no project assigned behave the same as they always have — Sessions with no project assigned behave as they always have —
only their own past episodes are searched. only their own past episodes are searched.
## How It Works ## How It Works
@@ -44,16 +44,9 @@ if (session.project_id) {
} }
``` ```
If the session belongs to any project (isolated or not), `projectSessionIds` If the session belongs to any project, `projectSessionIds` is populated with
is populated with the internal integer IDs of all sessions in that project. the internal integer IDs of all sessions in that project — creating a shared
memory pool across all conversations in the project.
For **non-isolated projects**, this expands the search to all project sessions.
For **isolated projects**, the same set is used but the intent is restriction
— since `projectSessionIds` only contains project sessions, no external
episodes can appear.
Both cases use the same code path — the `isolated` flag does not change the
query logic, only the conceptual meaning.
### Step 2 — Qdrant filter construction ### Step 2 — Qdrant filter construction
@@ -96,26 +89,11 @@ message, `getProjectSessions` will not include that session's ID, so its
episodes disappear from the semantic search scope. episodes disappear from the semantic search scope.
**New sessions created from ProjectView are assigned after the first message.** **New sessions created from ProjectView are assigned after the first message.**
The `useChat` hook writes the `project_id` assignment via `updateSession` after `handleNewProjectChat` in `App.jsx` calls `sendMessage` with the project ID,
`onDone` fires. There is a brief window during the first message where the which is passed to `useChat`. After `onDone` fires, `useChat` calls
session has no project assigned. The project is correctly applied from the `updateSession` to write the project assignment to the backend. There is a
second message onward. brief window during the first message where the session has no project assigned.
The project is correctly applied from the second message onward.
## Isolated vs Non-Isolated
The `isolated` flag is stored on the project but does not currently change the
query logic — both isolated and non-isolated projects result in a
`projectSessionIds` filter. The distinction is semantic and enforced by
the project's membership:
- **Non-isolated** — intentionally draws from all sessions in the project,
creating a shared memory pool for related conversations
- **Isolated** — by design contains only sessions explicitly added to it,
so the same filter naturally restricts context to project-only episodes
If cross-project contamination became a concern (e.g. a session accidentally
added to the wrong project), removing it from the project immediately restores
isolation.
## Qdrant Payload Structure ## Qdrant Payload Structure

View File

@@ -82,7 +82,7 @@ src/
├── api/ ├── api/
│ └── orchestration.js # All fetch calls to the orchestration service │ └── orchestration.js # All fetch calls to the orchestration service
├── config/ ├── config/
│ └── constants.js # FALLBACK_MODELS, DEFAULT_MODEL, API_DEFAULTS │ └── constants.js # FALLBACK_MODELS, DEFAULT_MODEL, API_DEFAULTS, CLIENT_DEFAULTS
├── hooks/ ├── hooks/
│ ├── useSession.js # Session list, history loading, active session state │ ├── useSession.js # Session list, history loading, active session state
│ ├── useChat.js # Message sending, SSE streaming, message state │ ├── useChat.js # Message sending, SSE streaming, message state
@@ -93,14 +93,16 @@ src/
├── components/ ├── components/
│ ├── App.jsx # Root component — layout, shared state, view routing │ ├── App.jsx # Root component — layout, shared state, view routing
│ ├── Sidebar.jsx # Left sidebar — projects, recent chats, navigation │ ├── Sidebar.jsx # Left sidebar — projects, recent chats, navigation
│ ├── HomeView.jsx # Landing screen — greeting, centred input, quick actions
│ ├── ChatWindow.jsx # Centre panel — message thread and input bar │ ├── ChatWindow.jsx # Centre panel — message thread and input bar
│ ├── MessageBubble.jsx # Individual message bubble — renders markdown via react-markdown │ ├── MessageBubble.jsx # Individual message bubble — renders markdown via react-markdown
│ ├── InfoPanel.jsx # Right panel — model selector and session metadata (slide-in) │ ├── InfoPanel.jsx # Right panel — model selector and session metadata (slide-in)
│ ├── SessionModal.jsx # Modal for session rename, project assignment, delete │ ├── SessionModal.jsx # Modal for session rename, project assignment, delete
│ ├── ProjectModal.jsx # Modal for project create, edit, delete │ ├── ProjectModal.jsx # Modal for project create, edit, delete
│ ├── AllChatsView.jsx # Full paginated session list with multi-select bulk delete │ ├── AllChatsView.jsx # Full paginated session list with multi-select bulk delete
│ ├── AllProjectsView.jsx # Project tile grid with create/edit/delete │ ├── AllProjectsView.jsx # Project tile grid with create/edit/delete; tile click navigates to ProjectView
│ ├── ProjectView.jsx # Individual project — session list, new chat button │ ├── ProjectView.jsx # Individual project — conversations, new chat input, memory
│ │ # placeholder, user notes, ⋮ edit/delete menu
│ ├── MemoryView.jsx # Paginated, searchable, expandable, deletable episode viewer │ ├── MemoryView.jsx # Paginated, searchable, expandable, deletable episode viewer
│ └── SettingsView.jsx # Settings — Memory limits, Models (inference params, active │ └── SettingsView.jsx # Settings — Memory limits, Models (inference params, active
│ # model, context window), Service Health, Appearance placeholder │ # model, context window), Service Health, Appearance placeholder
@@ -108,8 +110,6 @@ src/
└── main.jsx # React entry point └── main.jsx # React entry point
``` ```
> `SessionList.jsx` is superseded by `Sidebar.jsx` and kept only as a reference.
## Layout ## Layout
The app uses a view-based layout. `App.jsx` manages a `view` state string The app uses a view-based layout. `App.jsx` manages a `view` state string
@@ -120,13 +120,13 @@ panel are persistent across all views.
┌──────────────────┬──────────────────────────────┐ ┌──────────────────┬──────────────────────────────┐
│ Sidebar │ Main Area (view-dependent) │ │ Sidebar │ Main Area (view-dependent) │
│ (collapsible) │ │ │ (collapsible) │ │
│ │ chatChatWindow │ │ homeHomeView
│ + New Chat │ all-chatsAllChatsView │ + New Chat │ chat → ChatWindow
│ ⊞ View Projects │ all-projects → AllProjectsView│ │ ⊞ View Projects │ all-chats → AllChatsView
│ │ projectProjectView │ │ all-projects → AllProjectsView│
│ PROJECTS ▾ │ settings → SettingsView │ │ PROJECTS ▾ │ project → ProjectView
│ [tile] [tile] │ memory → MemoryView │ [tile] [tile] │ settings → SettingsView │
│ All Projects → │ │ All Projects → │ memory → MemoryView
│ │ │ │ │ │
│ RECENT CHATS ▾ │ │ │ RECENT CHATS ▾ │ │
│ Session 1 │ │ │ Session 1 │ │
@@ -137,24 +137,43 @@ panel are persistent across all views.
└──────────────────┴──────────────────────────────┘ └──────────────────┴──────────────────────────────┘
``` ```
The sidebar collapses to a 48px icon rail. The right `InfoPanel` slides in The sidebar collapses to a 48px icon rail and starts collapsed on the home
from the right using `transform: translateX()` — hidden by default, toggled view. The right `InfoPanel` slides in from the right using
via the `⊹` button in the `ChatWindow` header. `transform: translateX()` — hidden by default, toggled via the `⊹` button
in the `ChatWindow` header.
## View Routing ## View Routing
| View | Component | Trigger | | View | Component | Trigger |
|---|---|---| |---|---|---|
| `'chat'` | `ChatWindow` | Default; selecting a session; new chat | | `'home'` | `HomeView` | Initial load; going back from chat with no history |
| `'chat'` | `ChatWindow` | Selecting a session; new chat; sending from HomeView |
| `'all-chats'` | `AllChatsView` | "All Chats →" or ☰ icon in collapsed rail | | `'all-chats'` | `AllChatsView` | "All Chats →" or ☰ icon in collapsed rail |
| `'all-projects'` | `AllProjectsView` | "View Projects" button or ⊞ icon | | `'all-projects'` | `AllProjectsView` | "View Projects" button or ⊞ icon |
| `'project'` | `ProjectView` | Clicking a project tile in the sidebar | | `'project'` | `ProjectView` | Clicking a project tile in sidebar or AllProjectsView |
| `'settings'` | `SettingsView` | Settings button or ⚙ icon | | `'settings'` | `SettingsView` | Settings button or ⚙ icon |
| `'memory'` | `MemoryView` | "Open →" button in Settings → Memory section | | `'memory'` | `MemoryView` | "Open →" button in Settings → Memory section |
`activeProject` state in `App.jsx` tracks which project `ProjectView` is `activeProject` state in `App.jsx` tracks which project `ProjectView` is
displaying. Set via `onSelectProject` before navigating to `'project'`. displaying. Set via `onSelectProject` before navigating to `'project'`.
### View History Stack
`App.jsx` maintains a `viewHistory` array. Each `navigate(view)` call pushes
the current view onto the stack. `goBack()` pops the last entry and restores
it. All view components receive `onBack={goBack}` — no component hardcodes
its own back destination. Navigating to `'home'` collapses the sidebar;
leaving `'home'` expands it.
## Home View
`HomeView` is the landing screen shown on initial load and when there are no
active sessions. It displays:
- Time-based greeting ("Morning / Afternoon / Evening, Tim")
- Currently loaded model name (from `modelProps.modelAlias`, stripped of `.gguf`)
- Centred textarea input — sending creates a new session and navigates to chat
- Quick action pills that populate the input without auto-sending
## CSS Architecture ## CSS Architecture
Styles follow a hybrid approach — CSS utility classes for static reusable Styles follow a hybrid approach — CSS utility classes for static reusable
@@ -217,6 +236,10 @@ the `uuid` package. New sessions are created locally and auto-registered in
the memory service on the first message. The session list refreshes after the memory service on the first message. The session list refreshes after
each completed response to surface newly created sessions. each completed response to surface newly created sessions.
`useSession.selectSession` skips the history fetch for new (`isNew: true`)
sessions — fetching history for an unsaved session would 404 since it doesn't
exist in the backend yet.
### Auto-naming ### Auto-naming
After the first exchange completes, orchestration fires a secondary inference After the first exchange completes, orchestration fires a secondary inference
@@ -236,46 +259,60 @@ Session rows support rename, project assignment, and delete via:
`SessionModal` handles rename and project assignment together in `settings` `SessionModal` handles rename and project assignment together in `settings`
mode, and delete confirmation in `confirm-delete` mode. mode, and delete confirmation in `confirm-delete` mode.
### Active Session Clearing on Delete
When the deleted session is the currently active one, `App.jsx` clears the
chat window before refreshing the list:
```js
function handleSessionsChange(deletedSession) {
if (deletedSession?.external_id === activeSession?.external_id) {
selectSession(null);
}
refreshSessions();
}
```
### Key Patterns ### Key Patterns
- Button nesting: action icons are siblings of row buttons, not children — HTML forbids `<button>` inside `<button>` - Button nesting: action icons are siblings of row buttons, not children — HTML forbids `<button>` inside `<button>`
- Context menu rendered outside sidebar via React fragment to avoid `overflow: hidden` clipping - Context menu rendered outside sidebar via React fragment to avoid `overflow: hidden` clipping
- `useContextMenu` dismisses on a `window` click listener - `useContextMenu` dismisses on a `window` click listener
- Dynamic `updateSession` SQL builds `SET` clause from only the fields passed — prevents accidental overwrites - Dynamic `updateSession` SQL builds `SET` clause from only the fields passed — prevents accidental overwrites
- `AllChatsView` pagination uses `CLIENT_DEFAULTS.PAGE_SIZE` (not `API_DEFAULTS.PAGE_SIZE` which doesn't exist)
## Project Management ## Project Management
All projects are isolated by default (`isolated: 1` hardcoded on create).
The isolated toggle has been removed from `ProjectModal`.
`useProjects` fetches the project list from `GET /projects` on mount and `useProjects` fetches the project list from `GET /projects` on mount and
exposes `refreshProjects` for keeping the sidebar in sync after mutations. exposes `refreshProjects` for keeping the sidebar in sync after mutations.
`ProjectModal` handles create, edit, and delete confirmation. Fields: name `ProjectModal` handles create, edit, and delete confirmation. Fields: name
(required), description (optional), colour picker, isolated toggle. (required), description (optional), colour picker.
`ProjectView` shows the project's name, description, isolated badge (if set), Clicking a project tile in `AllProjectsView` calls `onSelectProject` then
and a filtered session list. The "+ New Chat" button creates a new session, navigates to `'project'`.
navigates to `'chat'`, and writes the project assignment after the first message.
### ProjectView
`ProjectView` is a full project workspace with:
- Colour accent bar + project title + description
- ⋮ dropdown menu for edit (opens `ProjectModal` pre-filled) and delete
- Conversations list — each session is a clickable row navigating to `'chat'`
- `ChatInput` component below the list (or centred when no sessions exist) for
starting new project-tied conversations without a separate button
- **Project Memory** — placeholder section explaining upcoming auto-summary feature
- **Project Notes** — textarea with Save button; notes saved to `projects.notes`
column in SQLite; save button only appears when content has changed from last
saved value (`savedNotes` state tracks the baseline, not `initialNotes`)
`updateProject` in `orchestration.js` uses a passthrough pattern — spreads
all fields directly into the request body, only transforming `isolated` if
present. This allows partial updates like `{ notes }` without clobbering
other fields.
For memory isolation behaviour, see `memory-isolation.md`. For memory isolation behaviour, see `memory-isolation.md`.
## Settings ## Settings
`useSettings` fetches from `GET /settings` on mount and exposes a `saveSetting(key, value)` `useSettings` fetches from `GET /settings` on mount and exposes a
helper that issues a `PATCH /settings` with a single key-value pair. The `saving` `saveSetting(key, value)` helper that issues a `PATCH /settings` with a
boolean is exposed for disabling save buttons during in-flight requests. single key-value pair. The `saving` boolean is exposed for disabling save
buttons during in-flight requests.
`SettingsView` receives `settings`/`saveSetting`/`saving` from a single
`useSettings()` call at the top level and passes them as props to
`ModelsSection` and `ModelsFolderSetting` — avoiding triple fetch on mount.
`modelProps` (context window, loaded model) is fetched once in `App.jsx` and
passed down as a prop, eliminating a duplicate fetch on every settings open.
`SettingsView` is organised into sections: `SettingsView` is organised into sections:
@@ -285,3 +322,7 @@ boolean is exposed for disabling save buttons during in-flight requests.
loaded model from llama-server) loaded model from llama-server)
- **About** — service health check panel, version - **About** — service health check panel, version
- **Appearance** — theme (coming soon) - **Appearance** — theme (coming soon)
An error boundary (`SettingsSectionErrorBoundary`) wraps the Models section —
if the models fetch fails, only that section shows an error with a Retry
button rather than blanking the entire settings view.

View File

@@ -58,7 +58,7 @@ Six core tables:
- **entities** — named things the system learns about (people, places, concepts) - **entities** — named things the system learns about (people, places, concepts)
- **relationships** — directional labeled links between entities - **relationships** — directional labeled links between entities
- **summaries** — condensed episode groups for efficient context retrieval - **summaries** — condensed episode groups for efficient context retrieval
- **projects** — named groupings of sessions with `name`, `description`, `colour`, `icon`, `isolated` - **projects** — named groupings of sessions with `name`, `description`, `colour`, `icon`, `isolated`, `notes`
### Migrations ### Migrations
@@ -70,6 +70,7 @@ try { db.exec(`ALTER TABLE sessions ADD COLUMN name TEXT`); } catch {}
try { db.exec(`ALTER TABLE sessions ADD COLUMN project_id INTEGER REFERENCES projects(id)`); } catch {} try { db.exec(`ALTER TABLE sessions ADD COLUMN project_id INTEGER REFERENCES projects(id)`); } catch {}
try { db.exec(`CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_id)`); } catch {} try { db.exec(`CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_id)`); } catch {}
try { db.exec(`ALTER TABLE projects ADD COLUMN isolated INTEGER NOT NULL DEFAULT 0`); } catch {} try { db.exec(`ALTER TABLE projects ADD COLUMN isolated INTEGER NOT NULL DEFAULT 0`); } catch {}
try { db.exec(`ALTER TABLE projects ADD COLUMN notes TEXT`); } catch {}
``` ```
New migrations are always appended here — never modify the schema file for New migrations are always appended here — never modify the schema file for
@@ -87,22 +88,34 @@ keep the FTS index automatically in sync with the episodes table.
- `foreign_keys = ON` — enforces referential integrity and cascade deletes - `foreign_keys = ON` — enforces referential integrity and cascade deletes
- PRAGMAs set via `db.pragma()`, not `db.exec()` - PRAGMAs set via `db.pragma()`, not `db.exec()`
### Dynamic Session Updates ### Dynamic Updates
`updateSession` builds its `SET` clause dynamically from only the fields Both `updateSession` and `updateProject` build their `SET` clause dynamically
passed — prevents partial updates from overwriting fields that weren't from only the fields passed — prevents partial updates from overwriting fields
touched: that weren't touched:
```js ```js
function updateSession(id, { name, projectId } = {}) { // updateProject example
function updateProject(id, fields = {}) {
const allowed = ['name', 'description', 'colour', 'icon', 'isolated', 'notes'];
const updates = []; const updates = [];
const values = []; const values = [];
if (name !== undefined) { updates.push('name = ?'); values.push(name ?? null); } for (const key of allowed) {
if (projectId !== undefined) { updates.push('project_id = ?'); values.push(projectId ?? null); } if (fields[key] !== undefined) {
// ... updates.push(`${key} = ?`);
values.push(fields[key] ?? null);
}
}
if (updates.length === 0) return getProject(id);
values.push(id);
db.prepare(`UPDATE projects SET ${updates.join(', ')} WHERE id = ?`).run(...values);
return getProject(id);
} }
``` ```
This means saving just `{ notes: "..." }` won't touch `name`, `colour`, or
any other field.
## Qdrant / Semantic Layer ## Qdrant / Semantic Layer
Three Qdrant collections are initialized on service startup: Three Qdrant collections are initialized on service startup:

View File

@@ -302,14 +302,16 @@ function ChatInput({ value, onChange, onSend, placeholder, autoFocus }) {
function NotesSection({ projectId, initialNotes }) { function NotesSection({ projectId, initialNotes }) {
const [notes, setNotes] = useState(initialNotes); const [notes, setNotes] = useState(initialNotes);
const [savedNotes, setSavedNotes] = useState(initialNotes);
const [saving, setSaving] = useState(false); const [saving, setSaving] = useState(false);
const isDirty = notes !== initialNotes; const isDirty = notes !== savedNotes;
async function handleSave() { async function handleSave() {
setSaving(true); setSaving(true);
try { try {
await updateProject(projectId, { notes }); await updateProject(projectId, { notes });
setSavedNotes(notes);
} catch (err) { } catch (err) {
console.error('[NotesSection] Save failed:', err.message); console.error('[NotesSection] Save failed:', err.message);
} finally { } finally {