documentation update

This commit is contained in:
Storme-bit
2026-08-16 23:25:40 -07:00
parent b87b2fc930
commit 8f8177615c
4 changed files with 100 additions and 5 deletions
+23 -3
View File
@@ -99,7 +99,7 @@ returns without writing anything.
For each entity in `parsed.entities`:
1. Validate `name`, `type` (must be in `ENTITY_TYPES`), and not in `IGNORED_NAMES`
1. Validate `name`, `type` (must be in `ENTITY_TYPES`), and not a greeting via `isIgnoredName(name)`
2. Call `upsertEntity(name, type, notes)`:
- **Insert**: creates new row with `mention_count = 1`, `source = 'extraction'`
- **Conflict** on `(name, type)`: increments `mention_count`, updates `last_seen_at`, preserves existing `notes` if new extraction returns null
@@ -109,7 +109,27 @@ For each entity in `parsed.entities`:
**Valid entity types:** `person`, `place`, `project`, `technology`, `concept`, `organization`
**Stoplist (ignored names):** `good morning`, `good night`, `hello`, `goodbye`, `thanks`, `thank you`
**Greeting filter:** `isIgnoredName(name)` normalizes the candidate before
matching — lowercased, punctuation stripped (`/[^\w\s]/g`), trimmed — then
tests membership against the `IGNORED_NAMES` set. Normalizing means variants
like `"Good morning!"` or `" hello "` are caught, not just exact lowercase
matches. Current set: `good morning`, `good night`, `good evening`,
`good afternoon`, `hello`, `hi`, `hey`, `goodbye`, `bye`, `thanks`,
`thank you`, `morning`.
The extraction prompt also instructs the model not to emit greetings or
conversational filler as entities, so the filter is a backstop rather than the
sole defense — necessary because the model (qwen2.5:3b) will occasionally label
a greeting as a `concept` or `topic`, both valid types that the `ENTITY_TYPES`
check alone won't reject.
> **Note:** the filter runs only at extraction time; it does not retroactively
> remove greetings stored before the filter (or an entry) existed. Pre-filter
> stragglers must be deleted manually via `DELETE /entities/:id`, which now
> also removes the Qdrant vector (see `memory-service.md` → Delete Behaviour).
> `\w` is ASCII-only, so non-ASCII greetings (e.g. `buenos días`) normalize
> imperfectly — switch to `/[^\p{L}\p{N}\s]/gu` if the set gains non-ASCII
> entries.
## Relationship Processing
@@ -137,4 +157,4 @@ All steps after the initial model call are wrapped in a single outer try/catch.
If Ollama is unreachable, returns a non-200 status, or the JSON cannot be
parsed, the function logs at `warn` level and returns. There is no retry logic.
Individual entity embedding failures are caught per-entity and logged at `warn`
level without affecting other entities in the same batch.
level without affecting other entities in the same batch.
+32
View File
@@ -147,4 +147,36 @@ data: [DONE]
`model` and `tokenCount` are captured from the llama.cpp `finish_reason: stop`
chunk and emitted on the done event.
### SSE parsing (buffered)
`llama-server` emits SSE events as `data:` lines, but a single network chunk
from `res.body` is **not** guaranteed to contain whole lines — an event can be
split across two chunks (mid-`data:` prefix or even mid-JSON). The provider
therefore accumulates a string `buffer`, splits on `\n`, and retains the final
(possibly incomplete) element for the next chunk rather than parsing each raw
chunk in isolation:
```js
let buffer = '';
for await (const chunk of res.body) {
buffer += Buffer.from(chunk).toString('utf-8');
const lines = buffer.split('\n');
buffer = lines.pop() ?? ''; // keep incomplete tail for next chunk
for (const line of lines) yield* processLine(line.trim());
}
if (buffer.trim()) yield* processLine(buffer.trim()); // flush remainder
```
`processLine()` handles exactly one line: it skips non-`data:` lines and
`[DONE]`, guards `JSON.parse` in a try/catch (a malformed line logs a warning
and is skipped rather than throwing and killing the stream), extracts
`delta.content`, and captures `model` / token usage from the terminal chunks.
Content is emitted only when a delta is present (`if (delta) yield { response,
done: false }`).
> This mirrors the same buffering the orchestration service uses when reading
> the inference stream. Without it, long responses intermittently die
> mid-stream on a chunk boundary, and a run where every content delta lands on
> a split line surfaces as an empty response with a `done` event.
For all HTTP endpoints, see `api-routes.md`.
+35 -1
View File
@@ -184,6 +184,40 @@ service is responsible only for CRUD — generation logic lives in orchestration
> For full details on trigger conditions, prompt format, cumulative updates,
> and ChatML token stripping, see `summarization.md`.
## Delete Behaviour (SQLite + Qdrant consistency)
SQLite cascades handle relational cleanup, but Qdrant is a separate store and
must be cleaned explicitly. Each delete path that removes embedded rows also
removes the corresponding vectors:
| Delete | SQLite effect | Qdrant cleanup |
|---|---|---|
| `DELETE /episodes/:id` | Row removed | `semantic.deleteEpisode(id)` — vector by point ID |
| `DELETE /sessions/by-external/:id` | Session + episodes cascade-deleted | `semantic.deleteEpisodesBySession(id)` — **payload-filter** delete on `sessionId` |
| `DELETE /entities/:id` | Row removed, relationships cascade | `semantic.deleteEntity(id)` — vector by point ID |
All three Qdrant deletes are **fire-and-forget** with error logging, matching
the fire-and-forget write path — a Qdrant failure logs but does not fail the
delete.
The session path uses a **payload-filter** delete (matching on the `sessionId`
field in the vector payload) rather than enumerating episode point IDs. This
matters because the SQLite cascade has already removed the episode rows by the
time cleanup runs, so there are no IDs left to enumerate — the filter deletes
by payload regardless. It also cleans up any pre-existing orphans for that
session as a side effect.
> **Not cleaned on session delete:** entity vectors. Entities are shared across
> sessions and projects (`UNIQUE(name, type)` is global), so deleting one
> session must not remove entities that other sessions still reference. Entity
> vector lifecycle is tied to explicit entity deletion and the (planned) memory
> consolidation / orphan-cleanup pass.
> **Historical orphans:** vectors orphaned by session deletes *before* this
> cleanup existed are not removed retroactively. A one-time sweep (scroll the
> `episodes` collection, delete points whose `sessionId` no longer exists in
> SQLite) clears them.
## Project Delete Behaviour
Deleting a project runs as a transaction — it first nulls out `project_id`
@@ -197,4 +231,4 @@ const doDelete = db.transaction(() => {
});
```
For all HTTP endpoints, see `api-routes.md`.
For all HTTP endpoints, see `api-routes.md`.