dsh-session-query-vector
A semantic session-search backend for DSH, backed by Ollama or any OpenAI-compatible /v1/embeddings endpoint.
DSH ships a SQLite full-text backend (@deepseek-ai/dsh-session-query-sqlite) that answers "which session contained this word". This package replaces it with a vector backend that answers "which session was about this" — you ask in your own words, the query is embedded, and the closest chunks come back with their session and snippet.
It is a drop-in replacement for the official backend: both extend the same sessionQuery service, so exactly one of them may be mounted at a time.
Requirements
| |
|---|
| DSH | 0.2.1-rc.2 (developed and tested against it) |
@deepseek-ai/cordis | ~4.0.5 |
@deepseek-ai/schemastery | 3.18.5 (ships with DSH) |
| Node.js | 22+ — the indexer uses the built-in node:sqlite (DatabaseSync) |
| An embedding endpoint | Ollama locally, or any OpenAI-compatible API |
Earlier DSH releases are untested.
Two pieces
| Piece | What it is |
|---|
lib/index.js | The query side — a SessionQueryEngine subclass mounted as a plugin. Answers searches by reading the index. Never embeds. |
indexer.mjs | The index side — a standalone script. Walks ~/.dsh/sessions, embeds the message text, writes the vectors. |
They are deliberately separate: search must stay fast, and embedding is slow. The query side never triggers an embedding on the search path; if the index is empty it returns nothing.
The two sides must use the same embedding model. Otherwise the stored vectors and the query vector live in different spaces and every score is noise. Configure embedModel identically in both.
Install
dsh plugin --profile <your-profile> add github:minhaoli/dsh-session-query-vector
The package declares dsh.bundle.patch, so dsh plugin add appends it to dsh.profile.bundles and the loader mounts it on the next start.
The bundle patch also disables the official backend, because both register sessionQuery:
# cordis.patch.yml shipped with this package
- id: session-query-sqlite
disabled: true
- insert:
- id: session-query-vector
name: 'dsh-session-query-vector'
config:
provider: ollama
baseUrl: 'http://127.0.0.1:11434'
embedModel: bge-m3
openAt: first-search
If you would rather keep the official backend, remove that first row and do not mount this package — they cannot coexist.
Configuration
Set these in your profile's cordis.patch.yml. A patch replaces the targeted row's whole config rather than merging into it, so restate every key you want to keep.
# ~/.dsh/profiles/<profile>/cordis.patch.yml
- insert:
- id: session-query-vector
name: 'dsh-session-query-vector'
config:
path: 'C:/Users/<you>/.dsh/storages/session-query-vector.sqlite'
provider: openai
baseUrl: 'https://api.siliconflow.cn/v1'
apiKey: 'sk-...'
embedModel: 'BAAI/bge-m3'
openAt: first-search
topK: 20
minScore: 0.45
| Key | Default | Meaning |
|---|
path | ~/.dsh/storages/session-query-vector.sqlite | Index database. Created on first use. |
openAt | first-search | startup opens the DB at mount; first-search on the first query; never disables search entirely. |
provider | ollama | ollama or openai. openai means any OpenAI-compatible /v1/embeddings. |
baseUrl | http://127.0.0.1:11434 | Embedding endpoint. A trailing /v1 is stripped and re-added, so https://host/v1 and https://host both work. |
apiKey | "" | Sent as Authorization: Bearer when non-empty. Not needed for Ollama. |
embedModel | bge-m3 | Must match the indexer. |
embedTimeoutMs | 120000 | Per-request timeout. |
topK | 20 | Candidates pulled from the index before ranking. |
minScore | 0.45 | Cosine floor. |
relMargin | 0.10 | Relative cutoff: a hit is kept only if its score is within relMargin of the best hit. |
snippetChars | 240 | Snippet length in the result. |
readWindowMax | 50 | Read window for the engine's live-preferred reads. |
Endpoint contracts
Ollama (provider: ollama)
POST {baseUrl}/api/embeddings
content-type: application/json
{ "model": "bge-m3", "prompt": "the text to embed" }
Response: { "embedding": [0.013, -0.021, ...] }.
Pull the model first:
ollama pull bge-m3
OpenAI-compatible (provider: openai)
POST {baseUrl}/v1/embeddings
authorization: Bearer <apiKey>
content-type: application/json
{ "model": "BAAI/bge-m3", "input": ["chunk one", "chunk two"] }
Response: { "data": [{ "index": 0, "embedding": [...] }, ...] }. Results are sorted by data[].index before use — some providers return them out of order.
A trailing /v1 on baseUrl is stripped before /v1/embeddings is appended, so https://api.siliconflow.cn/v1 does not become /v1/v1/embeddings.
Any endpoint that satisfies this contract works: SiliconFlow, OpenAI, Azure OpenAI, Together, a local vLLM or LM Studio server, and so on. Set baseUrl, apiKey and embedModel to whatever that provider names the model.
Building the index
indexer.mjs is not run by DSH — start it yourself, after the query side is configured. It must be able to import the @deepseek-ai/* packages, so run it with the DSH runtime (see below).
# layered: layer 1 (user messages) first, then layer 2 (assistant messages)
node indexer.mjs
node indexer.mjs --layer=1 # only user messages — fastest path to a usable search
node indexer.mjs --layer=2 # only assistant messages
node indexer.mjs --limit=200 # cap sessions this run
node indexer.mjs --status # print progress, index nothing
node indexer.mjs --force # re-index unchanged sessions
node indexer.mjs --quiet # no progress lines, only key events
node indexer.mjs --skip-active=5 # skip sessions written in the last 5 minutes
| Environment variable | Default | Meaning |
|---|
DSH_HOME | ~/.dsh | DSH home. Everything else derives from it. |
DSH_MODULES | $DSH_HOME/node_modules/@deepseek-ai | Where to resolve @deepseek-ai/*. Override for the packaged desktop app. |
VEC_DB | $DSH_HOME/storages/session-query-vector.sqlite | Index path — must equal the plugin's path. |
VEC_SESSIONS | $DSH_HOME/sessions | Session log root. |
VEC_PROVIDER | ollama | ollama or openai — must equal the plugin's provider. |
VEC_BASE_URL | http://127.0.0.1:11434 | Must equal the plugin's baseUrl. |
VEC_API_KEY | "" | Must equal the plugin's apiKey. |
VEC_MODEL | bge-m3 | Must equal the plugin's embedModel. |
VEC_CHUNK | 1200 | Characters per chunk. |
VEC_BATCH | 16 | Chunks per embedding request. |
VEC_SUBAGENTS | — | Set to 1 to include subagent sessions. |
Running the indexer against the packaged desktop app
The Electron app's @deepseek-ai/* modules live inside app.asar, which plain node cannot read. Point DSH_MODULES at the asar and run the indexer under the app's own binary in Node mode:
$env:ELECTRON_RUN_AS_NODE = '1'
$env:DSH_MODULES = 'D:/APP/deepseek harness/resources/app.asar/dsh/node_modules/@deepseek-ai/'
& 'D:\APP\deepseek harness\DeepSeek Harness.exe' indexer.mjs --status
From a source checkout, plain node indexer.mjs works — DSH_MODULES already points at real files.
Index schema
One SQLite file, application_id = 1146308690 (distinct from the official backend's), user_version = 1. A file whose application_id belongs to something else is refused rather than clobbered.
| Table | Purpose |
|---|
vec_chunks(session_id, seq, type, time, chunk_idx, text, dim, vector BLOB) | One row per embedded chunk. Primary key (session_id, seq, chunk_idx). |
vec_sessions(id, revision, indexed_at, chunks, cwd, layer) | Per-session bookkeeping; revision drives the incremental skip. |
vec_queries(text, vector, dim, at) | Query-vector cache, so repeating a search skips the embedding round trip. |
All tables are STRICT. Each session is one transaction, so an interrupted run never leaves a half-written session.
Failure modes worth knowing
- Empty results, no error. The index has not been built, or was built with a different model. Check
--status.
database ... belongs to another application. path points at a file this package did not create.
database ... is not an empty or recognized index. The file exists with tables but no application_id — point path somewhere else or delete it.
/v1/v1/embeddings 404. You set baseUrl with a trailing /v1 and the code already handles it; a 404 here means the provider's path differs from the OpenAI contract.
- Both backends mounted.
sessionQuery is registered twice and the second mount fails. Disable session-query-sqlite.
Companion plugins
License
MIT — see LICENSE.