update documentation

This commit is contained in:
Storme-bit
2026-04-17 03:48:49 -07:00
parent ac1bd963ef
commit bb05d1508d
2 changed files with 0 additions and 0 deletions

View File

@@ -0,0 +1,128 @@
# Memory Isolation
NexusAI implements project-scoped memory — sessions belonging to the same
project can share semantic context, and isolated projects can be restricted
from drawing on memory outside the project. This document describes how the
system works end-to-end.
## Concepts
**Session** — a single conversation thread. Identified by `external_id`.
**Project** — a named grouping of sessions. Has an `isolated` flag (0 or 1).
**Semantic search** — at inference time, the user's message is embedded and
compared against past episodes in Qdrant to surface relevant context. The
scope of this search is controlled by the project context.
## Semantic Search Scope
| Session state | Semantic search scope |
|---|---|
| No project | Own session's episodes only |
| Assigned to a non-isolated project | All episodes across all sessions in the project |
| Assigned to an isolated project | All episodes within the project only |
| Removed from a project | Own session's episodes only (from that point) |
Sessions with no project assigned behave the same as they always have —
only their own past episodes are searched.
## How It Works
### Step 1 — Project context resolution (orchestration)
In `chat/index.js`, immediately after session resolution:
```js
let projectSessionIds = null;
if (session.project_id) {
const project = await memory.getProject(session.project_id);
if (project) {
const projectSessions = await memory.getProjectSessions(session.project_id);
projectSessionIds = projectSessions.map(s => s.id);
}
}
```
If the session belongs to any project (isolated or not), `projectSessionIds`
is populated with the internal integer IDs of all sessions in that 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
In `services/qdrant.js`, `searchEpisodes` builds the filter:
```js
if (projectSessionIds) {
body.filter = {
should: projectSessionIds.map(id => ({
key: 'sessionId', match: { value: id }
}))
};
} else if (sessionId) {
body.filter = { must: [{ key: 'sessionId', match: { value: sessionId } }] };
}
```
`should` is Qdrant's "match any of" operator — equivalent to SQL
`WHERE sessionId IN (...)`. When `projectSessionIds` is set, the single-session
filter is not used.
### Step 3 — Episode payloads
Every episode upserted into Qdrant carries `{ sessionId, createdAt }` in its
payload. `sessionId` here is the **internal integer ID** from SQLite. This
is what the Qdrant filter matches against.
This means the filter works correctly regardless of when episodes were created
or when a session was added to a project — the payload is immutable.
## Important Behaviours
**Pre-existing episodes are included immediately.** When a session is added
to a project and a new message is sent, Qdrant can match all of that session's
existing episodes since the filter only requires the `sessionId` to be in the
project's session list.
**Removing a session from a project takes effect immediately.** On the next
message, `getProjectSessions` will not include that session's ID, so its
episodes disappear from the semantic search scope.
**New sessions created from ProjectView are assigned after the first message.**
The `useChat` hook writes the `project_id` assignment via `updateSession` after
`onDone` fires. There is a 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
Episodes are stored with this payload:
```json
{ "sessionId": 42, "createdAt": 1776080188 }
```
`sessionId` is the SQLite `sessions.id` integer, not the `external_id` UUID.
This is important when building filters — always use internal IDs.