@ponraaj/dimple-sdk 0.0.0-alpha.5 → 0.0.0-alpha.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +55 -514
- package/dist/index.js +66 -45
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,35 +1,18 @@
|
|
|
1
1
|
# @ponraaj/dimple-sdk
|
|
2
2
|
|
|
3
|
-
A local-first memory system for AI agents
|
|
4
|
-
codebases
|
|
5
|
-
|
|
6
|
-
structured as it grows. Everything runs locally
|
|
3
|
+
A local-first memory system for AI agents: store text memories and whole
|
|
4
|
+
codebases, retrieve them with hybrid semantic + lexical search, and let a
|
|
5
|
+
self-organizing **topic tree + artifact graph + concepts** keep everything
|
|
6
|
+
structured as it grows. Everything runs locally — ONNX embeddings on your
|
|
7
7
|
CPU, SQLite files on disk.
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
> **Documentation**: [dimple.ponraaj.me](https://dimple.ponraaj.me) —
|
|
10
|
+
> quickstart, architecture, config reference, changelog ·
|
|
11
|
+
> **Package**: [npmx.dev/package/@ponraaj/dimple](https://npmx.dev/package/@ponraaj/dimple) ·
|
|
12
|
+
> **Status**: alpha (`0.0.0-alpha.n`) — the API is stabilizing.
|
|
12
13
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
### The one-liner
|
|
16
|
-
|
|
17
|
-
```ts
|
|
18
|
-
import { makeDimple } from "@ponraaj/dimple-sdk"; // ← makeDimple, not dimple
|
|
19
|
-
|
|
20
|
-
const dimple = await makeDimple({ config: "./dimple.jsonc" });
|
|
21
|
-
const hits = await dimple.query("what did we decide about X", {
|
|
22
|
-
limit: 5,
|
|
23
|
-
contentType: "text/plain", // ← code-vs-message separation
|
|
24
|
-
}); // query(text, options) — text is the FIRST positional argument
|
|
25
|
-
await dimple.dispose();
|
|
26
|
-
```
|
|
27
|
-
|
|
28
|
-
Calling conventions: every method takes **plain arguments** (`query(text,
|
|
29
|
-
opts)`, `write(input, opts)`, `index({ paths })`), every failure is a
|
|
30
|
-
rejected promise with a closed `_tag` (`"Sdk.NotFound"`, `"Sdk.InputError"`,
|
|
31
|
-
…), and `dispose()` is **required** before `process.exit(0)` — the durable
|
|
32
|
-
worker keeps the event loop alive (`dispose()` stops it; it is idempotent).
|
|
14
|
+
This is the programmatic SDK. The [`@ponraaj/dimple`](https://www.npmjs.com/package/@ponraaj/dimple)
|
|
15
|
+
CLI is a thin facade over it.
|
|
33
16
|
|
|
34
17
|
## Install
|
|
35
18
|
|
|
@@ -37,16 +20,15 @@ worker keeps the event loop alive (`dispose()` stops it; it is idempotent).
|
|
|
37
20
|
npm install @ponraaj/dimple-sdk
|
|
38
21
|
```
|
|
39
22
|
|
|
40
|
-
Runtime
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
the `@dimple/*` specifiers — plain consumers never need the workspace).
|
|
23
|
+
Runtime deps only (Effect, libSQL, transformers) — workspace internals are
|
|
24
|
+
bundled, types are self-contained, and the default install ships **zero
|
|
25
|
+
postinstall scripts** (the local ONNX embedder is an optional peer,
|
|
26
|
+
auto-installed on first use).
|
|
45
27
|
|
|
46
28
|
## Quickstart
|
|
47
29
|
|
|
48
30
|
```ts
|
|
49
|
-
import { makeDimple } from "@ponraaj/dimple-sdk";
|
|
31
|
+
import { makeDimple } from "@ponraaj/dimple-sdk"; // ← makeDimple, not dimple
|
|
50
32
|
|
|
51
33
|
const dimple = await makeDimple({ config: "./dimple.jsonc" });
|
|
52
34
|
|
|
@@ -57,500 +39,59 @@ const mem = await dimple.write({
|
|
|
57
39
|
});
|
|
58
40
|
|
|
59
41
|
// hybrid semantic + FTS search
|
|
60
|
-
const
|
|
61
|
-
limit: 3,
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
// index real code — files + extracted functions/classes/methods become memories
|
|
65
|
-
const indexed = await dimple.index({ paths: ["./src"] });
|
|
66
|
-
|
|
67
|
-
await dimple.dispose(); // stop the worker, close DBs (idempotent)
|
|
68
|
-
```
|
|
69
|
-
|
|
70
|
-
## The problem & the approach
|
|
71
|
-
|
|
72
|
-
**Problem**: naive memory stores scan every vector per query (cost grows
|
|
73
|
-
with memory count), return flat results with no structure, and give the
|
|
74
|
-
agent no notion of _why_ a memory is relevant, what supersedes it, or what
|
|
75
|
-
contradicts it.
|
|
76
|
-
|
|
77
|
-
**Approach** — three structures maintained automatically by a durable
|
|
78
|
-
background worker:
|
|
79
|
-
|
|
80
|
-
1. **Topic tree** — memories are routed by embedding similarity into tree
|
|
81
|
-
leaves. Queries descend the tree (beam search on centroids) and only
|
|
82
|
-
score the reached leaves: **search cost grows with depth, not with
|
|
83
|
-
memory count**. A durable worker splits overflowing leaves and merges
|
|
84
|
-
underflowing ones with deterministic algorithms (CF-triplet k-means) —
|
|
85
|
-
same embeddings ⇒ same tree, anywhere.
|
|
86
|
-
2. **Hybrid retrieval** — vector (cosine) + FTS5 lexical legs fused with
|
|
87
|
-
Reciprocal Rank Fusion, optional recency boost, and per-result graph
|
|
88
|
-
context.
|
|
89
|
-
3. **Artifact graph + concepts** — directed `memory_supports` /
|
|
90
|
-
`memory_supersedes` / `memory_contradicts` edges written by structural
|
|
91
|
-
rules, code indexing, the agent, and the optional **dream** job (LLM
|
|
92
|
-
classification of candidates + deterministic concept clustering).
|
|
93
|
-
Retrieval consumes the graph: superseded memories demote, conflicts
|
|
94
|
-
surface, supports ride along as provenance; concept filters give the
|
|
95
|
-
query sovereignty.
|
|
96
|
-
|
|
97
|
-
All structural decisions are **deterministic**; everything is **durable**
|
|
98
|
-
(two SQLite files, resume-safe jobs).
|
|
99
|
-
|
|
100
|
-
## Configuration
|
|
101
|
-
|
|
102
|
-
Pass a config file path or an inline object:
|
|
103
|
-
|
|
104
|
-
```ts
|
|
105
|
-
const dimple = await makeDimple({ config: "./dimple.jsonc" });
|
|
106
|
-
// or — the inline object mirrors the JSON config exactly (quoted keys):
|
|
107
|
-
const dimple = await makeDimple({
|
|
108
|
-
config: {
|
|
109
|
-
storeUrl: "file:./.dimple/dimple.db",
|
|
110
|
-
jobs: { dbUrl: "file:./.dimple/jobs.db" },
|
|
111
|
-
embedding: {
|
|
112
|
-
defaultModel: "minilm",
|
|
113
|
-
models: [
|
|
114
|
-
{
|
|
115
|
-
id: "minilm",
|
|
116
|
-
provider: "transformers",
|
|
117
|
-
model: "Xenova/all-MiniLM-L6-v2",
|
|
118
|
-
dimensions: 384,
|
|
119
|
-
},
|
|
120
|
-
],
|
|
121
|
-
},
|
|
122
|
-
},
|
|
123
|
-
});
|
|
124
|
-
```
|
|
125
|
-
|
|
126
|
-
### Config sections (`dimple.jsonc`)
|
|
42
|
+
const hits = await dimple.query("how does the pipeline deploy", { limit: 3 });
|
|
127
43
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
| `storeUrl` | string | memory store DB (`file:` or remote libSQL) |
|
|
131
|
-
| `jobs` | `dbUrl`, `execution` (reserved), `concurrency`, `leaseTimeoutMs` (reserved), `maxAttempts` (1–100), `pollIntervalMs` (reserved) | separate DB for the durable queue |
|
|
132
|
-
| `embedding` | `defaultModel` + `models[]` | **required** — see Models below |
|
|
133
|
-
| `topics` | tree shape (`minLeafItems`, `maxLeafItems`, …) | optional, defaults apply |
|
|
134
|
-
| `graph` | graph config | optional |
|
|
135
|
-
| `llm` | `baseURL`, `apiKey`, `modelId`, `temperature` | enables `dream`/`repair` |
|
|
136
|
-
| `logging` | `enabled`, `level`, `format` (`jsonl`\|`pretty`), `file`, `scopes`, `ringSize` | silent by default; `file` = durable JSONL |
|
|
137
|
-
| `defaultK` / `defaultLimit` | numbers | retrieval defaults |
|
|
138
|
-
| `defaultTtlSecs` | number | write TTL default |
|
|
44
|
+
// index a codebase (tree-sitter: one unit per file + per definition)
|
|
45
|
+
await dimple.index({ paths: ["./src"] });
|
|
139
46
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
`storeUrl` and `jobs.dbUrl` accept **remote libSQL URLs** (`libsql://`,
|
|
143
|
-
`https://`, `ws://` — Turso or any libsql-compatible host). The same
|
|
144
|
-
engine runs everything the local file does — FTS5, vector search, the
|
|
145
|
-
topic tree, durable jobs — over the wire. Verified end-to-end against
|
|
146
|
-
the real Turso service (`tests/store/turso-remote.test.ts`).
|
|
147
|
-
|
|
148
|
-
Turso tokens are **per-database**, so each URL carries its own token.
|
|
149
|
-
`dimple.jsonc` resolves `${ENV_VAR}` in values, so tokens stay out of
|
|
150
|
-
config files:
|
|
151
|
-
|
|
152
|
-
```jsonc
|
|
153
|
-
{
|
|
154
|
-
"storeUrl": "libsql://<org>-<memdb>.turso.io?authToken=${TURSO_MEMORIES_TOKEN}",
|
|
155
|
-
"jobs": {
|
|
156
|
-
"dbUrl": "libsql://<org>-<jobsdb>.turso.io?authToken=${TURSO_JOBS_TOKEN}",
|
|
157
|
-
},
|
|
158
|
-
"embedding": {
|
|
159
|
-
/* hosted providers for serverless — local models are
|
|
160
|
-
local-only (auto-install needs Node) */
|
|
161
|
-
},
|
|
162
|
-
}
|
|
163
|
-
```
|
|
164
|
-
|
|
165
|
-
Notes:
|
|
166
|
-
|
|
167
|
-
- The **jobs DB must also be remote** when the store is remote (the
|
|
168
|
-
durable queue + workflow state are cross-invocation)
|
|
169
|
-
- `file:`-only behaviors (parent-dir creation, migration lock) skip
|
|
170
|
-
for remote URLs
|
|
171
|
-
- A single-DB setup can share one URL + one token (`TURSO_TOKEN`)
|
|
172
|
-
- For Cloudflare Workers the client swaps to the pure-JS entry
|
|
173
|
-
(`@libsql/client/web` — one bundler alias, no source changes)
|
|
174
|
-
|
|
175
|
-
### Embedding models
|
|
176
|
-
|
|
177
|
-
Local embeddings (`provider: "transformers"`) auto-install on first
|
|
178
|
-
use — `@huggingface/transformers` is an **optional peer** so hosted-only
|
|
179
|
-
consumers never pull its tree (the `sharp` + `onnxruntime-node`
|
|
180
|
-
postinstall scripts that abort npm 11 installs). The first `embed()`
|
|
181
|
-
installs it with the project's own package manager, scripts skipped
|
|
182
|
-
(proven unnecessary for embedding), and retries in-process. Offline or
|
|
183
|
-
restricted environments get a typed `Sdk.Embedding` error with the manual
|
|
184
|
-
command. Set `autoInstall: false` on the model to disable:
|
|
185
|
-
`embedding.models[].autoInstall: false`
|
|
186
|
-
|
|
187
|
-
### Embedding models
|
|
188
|
-
|
|
189
|
-
Two providers — **local** (Transformers.js ONNX, runs on your CPU, no
|
|
190
|
-
keys) and **hosted** (any OpenAI-compatible endpoint, plus native
|
|
191
|
-
provider families):
|
|
192
|
-
|
|
193
|
-
```jsonc
|
|
194
|
-
"embedding": {
|
|
195
|
-
"defaultModel": "bge",
|
|
196
|
-
"models": [
|
|
197
|
-
{ "id": "minilm", "provider": "transformers",
|
|
198
|
-
"model": "Xenova/all-MiniLM-L6-v2", "dimensions": 384 },
|
|
199
|
-
{ "id": "bge", "provider": "transformers",
|
|
200
|
-
"model": "Xenova/bge-small-en-v1.5", "dimensions": 384 },
|
|
201
|
-
{ "id": "bge-base", "provider": "transformers",
|
|
202
|
-
"model": "Xenova/bge-base-en-v1.5", "dimensions": 768 },
|
|
203
|
-
{ "id": "gemini", "provider": "openai",
|
|
204
|
-
"model": "gemini-embedding-2", "dimensions": 3072, "apiKey": "…",
|
|
205
|
-
"baseURL": "https://generativelanguage.googleapis.com/v1beta/openai/" },
|
|
206
|
-
{ "id": "openai", "provider": "openai",
|
|
207
|
-
"model": "text-embedding-3-small", "dimensions": 1536, "apiKey": "…" }
|
|
208
|
-
]
|
|
209
|
-
}
|
|
47
|
+
await dimple.dispose(); // required before process.exit — the durable worker
|
|
210
48
|
```
|
|
211
49
|
|
|
212
|
-
**
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
| `openai` (default) | any OpenAI-compatible endpoint — OpenAI, vLLM, **ollama** (`http://localhost:11434/v1`), Ollama Cloud, LM Studio, Together, Fireworks, Groq, Mistral, … | optional (defaults to OpenAI) — set it for custom endpoints |
|
|
217
|
-
| `google` | `gemini-embedding-001`, `gemini-embedding-2`, `gemini-embedding-2-preview` (native provider) | ❌ no |
|
|
218
|
-
| `cohere` | `embed-english-v3.0`, `embed-multilingual-v3.0`, … (native provider) | ❌ no |
|
|
219
|
-
| `bedrock` | `amazon.titan-embed-text-v2:0`, `amazon.nova-embed-text-v2:0`, `cohere.embed-*` (native provider; AWS creds from env) | ❌ no |
|
|
220
|
-
|
|
221
|
-
`dimensions` is sent as a provider option where the API supports it
|
|
222
|
-
(openai-compatible/`outputDimensionality`/`outputDimension`/titan `dimensions`
|
|
223
|
-
and nova `embeddingDimension`) and ALWAYS enforced on the output.
|
|
224
|
-
Verified end-to-end against real `gemini-embedding-2` (3072-dim), local
|
|
225
|
-
ollama `Qwen3-Embedding-0.6B` (1024-dim), and the openai-compatible
|
|
226
|
-
contract.
|
|
227
|
-
|
|
228
|
-
- `dimensions` must match the model's real output — a mismatch fails typed
|
|
229
|
-
on first use
|
|
230
|
-
- Multiple models coexist: each gets its own vector tables
|
|
231
|
-
(`vec_<dims>_<model>`) and its own topic tree; queries route per model
|
|
232
|
-
- **Per-model runtime options** (transformers provider, all optional):
|
|
233
|
-
`device` (`auto` | `gpu` | `cpu` | `wasm` | `webgpu` | `cuda` | `dml` |
|
|
234
|
-
`coreml` | `webnn` — `cpu`/`wasm` tested; GPU/CUDA needs the onnxruntime
|
|
235
|
-
CUDA libs, fails typed without them), `dtype` (`fp32` | `fp16` | `q8` |
|
|
236
|
-
`int8` | `uint8` | `q4` | `bnb4` | `q4f16` | `q2` | `q2f16` | `q1` |
|
|
237
|
-
`q1f16` — quantizes model WEIGHTS only, never the output vectors;
|
|
238
|
-
`fp32`/`q8`/`int8`/`q4`/`fp16` verified end-to-end, others depend on the
|
|
239
|
-
HF repo shipping that ONNX variant), `pooling` (`mean` | `cls` | `none`),
|
|
240
|
-
`normalize` (boolean), `cacheDir`
|
|
241
|
-
- **DB-level vector quantization** via `vectorType` (`float32` |
|
|
242
|
-
`float16` | `float8` | `float1bit`): the per-memory search vectors are
|
|
243
|
-
stored as int8/bfloat16/binary blobs and searched with libSQL's native
|
|
244
|
-
`vector_distance_cos` — `float8` verified end-to-end at **395 B vs
|
|
245
|
-
1536 B (4× smaller)** for 384-dim. The topic tree's centroid math and
|
|
246
|
-
`memories.embedding` always stay float32.
|
|
247
|
-
- **Switching models**: set `defaultModel` to a configured model (multi-
|
|
248
|
-
model configs coexist — each model gets its own tree + `vec_<dims>_<model>`
|
|
249
|
-
tables). After a restart the new default handles all writes/queries;
|
|
250
|
-
the old model's tree stays intact. Existing memories are backfilled via
|
|
251
|
-
the durable `memory.embed` job (re-embed + re-route into the new tree,
|
|
252
|
-
idempotent/resume-safe) — the store-level switch (`setActiveModel` +
|
|
253
|
-
`findStale`) drives it, reachable through the `{ core: … }` path. Until
|
|
254
|
-
backfilled, old memories remain in the old tree (queries descend the
|
|
255
|
-
ACTIVE model's tree), and the runtime is bound to one active model at a
|
|
256
|
-
time (per-model reads for another model fail typed with
|
|
257
|
-
`topics.ModelMismatch`).
|
|
258
|
-
- Vectors are always stored as float32 (`F32_BLOB`, 4 B/dim: 384 = 1536 B,
|
|
259
|
-
768 = 3072 B) regardless of `dtype`
|
|
260
|
-
|
|
261
|
-
### Power users: `{ core: … }`
|
|
262
|
-
|
|
263
|
-
Instead of a config, pass full `CoreOptions` (store/jobs URLs, models,
|
|
264
|
-
`defaultModelId`, `embedderFor` factory, topics/graph/logging/llm,
|
|
265
|
-
`concurrency`, `maxAttempts`, `extraDefs`/`extraHandlers` for custom durable
|
|
266
|
-
jobs — register `{ name: "my.job", handler }` and enqueue it via the
|
|
267
|
-
runtime; `extraHandlers` entries override built-ins by name). This is the
|
|
268
|
-
injection seam used by tests (synthetic embedders).
|
|
50
|
+
Calling conventions: **plain arguments** (`query(text, opts)`,
|
|
51
|
+
`write(input, opts)`, `index({ paths })`); every failure is a rejected
|
|
52
|
+
promise with a closed `_tag` (`"Sdk.NotFound"`, `"Sdk.InputError"`, …);
|
|
53
|
+
`dispose()` is idempotent.
|
|
269
54
|
|
|
270
55
|
## API surface
|
|
271
56
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
contentType, state: "active" | "superseded" | …, createdAt, ttlSecs,
|
|
288
|
-
namespace, metadata, embedding? }` (re-fetched after embedding). Duplicate
|
|
289
|
-
active `logicalKey` → `Sdk.DuplicateKey` (carries `logicalKey`/`namespace`).
|
|
290
|
-
|
|
291
|
-
**Memory expiry**: `ttlSecs` (or the config `defaultTtlSecs`) schedules
|
|
292
|
-
expiry; a sweep job retires expired memories in the background.
|
|
57
|
+
| Method | What it does |
|
|
58
|
+
| --------------------------------------------------------- | --------------------------------------------------------- |
|
|
59
|
+
| `write(input, { embed })` | Store a memory (idempotent per `logicalKey`) |
|
|
60
|
+
| `get(id)` / `forget(id)` | Fetch / remove by id or logical key |
|
|
61
|
+
| `query(text, options)` | Hybrid search — profiles, budgets, content-type filter |
|
|
62
|
+
| `updateEmbedding(id, vec, modelKey?)` | Attach an external embedding |
|
|
63
|
+
| `index({ paths })` | Codebase ingestion (idempotent re-index) |
|
|
64
|
+
| `graph.addEdge/confirm/reject/expire/candidates/coverage` | Memory relationships: supports / supersedes / contradicts |
|
|
65
|
+
| `explain({ kind, id }, options)` | Bounded graph neighborhood |
|
|
66
|
+
| `dream(modelKey?)` / `repair(modelKey?)` | Consolidation + maintenance jobs |
|
|
67
|
+
| `health(modelKey?)` | Topic-tree invariant health |
|
|
68
|
+
| `reload(input?)` | Blue-green config reload |
|
|
69
|
+
|
|
70
|
+
Errors are a closed hierarchy (`InputError`, `NotFound`, `DuplicateKey`,
|
|
71
|
+
`JobFailed`, `LLMNotConfigured`, `StoreFailure`) — see the docs site.
|
|
293
72
|
|
|
294
|
-
|
|
295
|
-
context guardrail: caps the row count, truncates result content to the
|
|
296
|
-
total char budget, and marks `results.truncated` when the soft time cap
|
|
297
|
-
is exceeded.
|
|
298
|
-
|
|
299
|
-
### `get(id)` / `forget(id)`
|
|
300
|
-
|
|
301
|
-
Fetch one memory; soft-delete (cascades edges, rebalances the tree).
|
|
302
|
-
|
|
303
|
-
### `query(text, options?)`
|
|
304
|
-
|
|
305
|
-
```ts
|
|
306
|
-
await dimple.query("how does the pipeline deploy", {
|
|
307
|
-
limit?: 10,
|
|
308
|
-
k?: 60, // RRF smoothing
|
|
309
|
-
namespace?: "default",
|
|
310
|
-
profile?: {
|
|
311
|
-
recencyBoost?: 0, // 0 = off; hyperbolically decays with age
|
|
312
|
-
modeWeights?: { vector?: 0.5, fts?: 0.5 },
|
|
313
|
-
conceptFilter?: string[], // concept ids (query sovereignty)
|
|
314
|
-
} | "hybrid" | "semantic" | "keyword",
|
|
315
|
-
topics?: { topicKeys?: string[] },
|
|
316
|
-
});
|
|
317
|
-
```
|
|
318
|
-
|
|
319
|
-
Returns `AugmentedMemory[]` — each entry carries the memory, the fused
|
|
320
|
-
score, its 1-based `rank`, and the graph context (`supersededBy`,
|
|
321
|
-
`conflicts`, `supportedBy`). Zero-result queries resolve to `[]` (the
|
|
322
|
-
absent-vector-table case is an empty dense leg, never an error).
|
|
323
|
-
|
|
324
|
-
### `index(options)` — codebase ingestion
|
|
325
|
-
|
|
326
|
-
```ts
|
|
327
|
-
await dimple.index({
|
|
328
|
-
paths: ["./src", "./packages"], // dirs (recursive) and/or files
|
|
329
|
-
keyPrefix?: "code-",
|
|
330
|
-
namespace?: "default",
|
|
331
|
-
embed?: true,
|
|
332
|
-
maxFileBytes?: 64 * 1024,
|
|
333
|
-
maxChunkBytes?: 8 * 1024,
|
|
334
|
-
extensions?: [".ts", ".tsx", ".js", ".jsx", …],
|
|
335
|
-
});
|
|
336
|
-
// → { files, indexed, updated, reconciled, skipped, commit, unsupported, failed }
|
|
337
|
-
```
|
|
338
|
-
|
|
339
|
-
- Tree-sitter parse (TypeScript/JavaScript/TSX/JSX) → the extractor
|
|
340
|
-
produces one **file unit** per file (with its import graph in metadata)
|
|
341
|
-
and one **chunk unit** per definition (function/class/method/interface/
|
|
342
|
-
type/enum/variable, with kind/name/line-range/docstring metadata)
|
|
343
|
-
- Every unit embeds + topic-routes (unless `embed: false`)
|
|
344
|
-
- AST-derived graph: relative imports resolve to indexed files and
|
|
345
|
-
class→method containment become **confirmed `memory_supports` edges**
|
|
346
|
-
(`source: "rule"`)
|
|
347
|
-
- **Idempotent + upserting**: keys are cwd-anchored + deterministic
|
|
348
|
-
(chunk keys drop the line number, so edits ABOVE a definition update
|
|
349
|
-
it instead of duplicating it). Re-indexing skips unchanged units
|
|
350
|
-
(`skipped`), and changed units are **superseded + rewritten under the
|
|
351
|
-
same key** (`updated`) with a confirmed identity `memory_supersedes`
|
|
352
|
-
edge — stale copies never accumulate
|
|
353
|
-
- **Edges re-form after `forget`**: phase-2 resolves endpoints by logical
|
|
354
|
-
key against the store (not just the current run), so a re-created unit
|
|
355
|
-
re-connects to its pre-existing dependencies
|
|
356
|
-
- **Reconcile + rename detection**: units whose source vanished (deleted
|
|
357
|
-
file/definition) are demoted (`reconciled`, `source_removed` — gone
|
|
358
|
-
from search; domain-scoped to the walked roots, so `index lib` never
|
|
359
|
-
touches other files). A vanished unit whose embedding is a near-duplicate
|
|
360
|
-
of a new unit becomes a PENDING `memory_supersedes` (reason rename) —
|
|
361
|
-
`graph confirm` completes it. Code units are TTL-exempt
|
|
362
|
-
(`ttlSecs: null` — the index owns their lifecycle); `commit` stamps the
|
|
363
|
-
repo HEAD into every unit's metadata as `codeVersion`
|
|
364
|
-
- **`code_calls` edges**: every call site inside a definition resolves to
|
|
365
|
-
the callee's chunk (same file, then imported files) → confirmed edges —
|
|
366
|
-
`explain(id, { edgeKinds: ["code_calls"] })` gives one-hop impact
|
|
367
|
-
analysis
|
|
368
|
-
- Per-file failures are collected, never fatal
|
|
369
|
-
|
|
370
|
-
### `updateEmbedding(id, embedding, modelKey?)` / `embed(text)`
|
|
371
|
-
|
|
372
|
-
Attach an externally-computed embedding (completes the `{ embed: false }`
|
|
373
|
-
workflow — the memory becomes semantically retrievable + topic-routed), or
|
|
374
|
-
embed text with the SDK's own pipeline.
|
|
375
|
-
|
|
376
|
-
### `dream(modelKey)` / `repair(modelKey)`
|
|
377
|
-
|
|
378
|
-
Run the consolidation job (concept clustering + LLM edge classification;
|
|
379
|
-
requires the `llm` config section) or the graph repair pass. Each call
|
|
380
|
-
gets a fresh run id (always executes). Failures are typed:
|
|
381
|
-
`Sdk.LLMNotConfigured`, `Sdk.JobFailed`.
|
|
382
|
-
|
|
383
|
-
### `explain({ kind: "memory" | "topic" | "concept", id }, options?)`
|
|
384
|
-
|
|
385
|
-
Graph neighborhood traversal — `{ nodes: [{ kind, id, depth, edgeKind }] }`.
|
|
386
|
-
`options.edgeKinds` scopes the answer (memory-only navigation: pass the
|
|
387
|
-
`memory_*` kinds; `code_calls` shows the call graph).
|
|
388
|
-
|
|
389
|
-
### `health(modelKey)` / `graph.*`
|
|
390
|
-
|
|
391
|
-
- `health` — topic-tree invariant sweep (`warnCount` + violation strings)
|
|
392
|
-
- `graph.confirm(id)` / `graph.reject(id)` / `graph.expire(id)` — the
|
|
393
|
-
pending-edge lifecycle: promote to confirmed (a `memory_supersedes`
|
|
394
|
-
confirm demotes the old endpoint in the same transaction), reject, or
|
|
395
|
-
soft-delete. `Sdk.EdgeNotFound` for stale ids
|
|
396
|
-
- `graph.addEdge({ fromMemoryId, toMemoryId, edgeKind: "memory_supports" |
|
|
397
|
-
"memory_supersedes" | "memory_contradicts", source: "rule" | "llm" |
|
|
398
|
-
"agent", weight?, confidence?, confirmed? })` — explicit edges; typed
|
|
399
|
-
validation (no self-loops, known kinds, weight > 0, confidence ∈ [0,1])
|
|
400
|
-
- `graph.candidates()` — pending structural candidates + isolated count
|
|
401
|
-
- `graph.coverage(modelKey)` — coverage %, pending, concept count
|
|
402
|
-
|
|
403
|
-
### `reload(input?)` — apply a config change
|
|
404
|
-
|
|
405
|
-
Pauses and re-boots with the re-resolved config:
|
|
406
|
-
|
|
407
|
-
1. the config source is re-resolved **typed** — a `dimple.jsonc` path is
|
|
408
|
-
re-read from disk, an inline object re-validated (a bad config rejects
|
|
409
|
-
the reload with **zero impact** — nothing pauses);
|
|
410
|
-
2. an **identical** config resolves as `{ reloaded: false }` — no pause,
|
|
411
|
-
no restart (safe to call from a file watcher);
|
|
412
|
-
3. new calls wait, in-flight calls settle on the old stack;
|
|
413
|
-
4. a fresh stack boots (new worker fleet, embedders, logger — jobs are
|
|
414
|
-
durable, so an interrupted handler's lease expires and re-claims after
|
|
415
|
-
boot);
|
|
416
|
-
5. the swap: the old stack is disposed, the new one serves.
|
|
417
|
-
|
|
418
|
-
Resolves with `{ config, changed, reloaded }` — `changed` flags which
|
|
419
|
-
sections actually moved (`storeUrl` / `jobsUrl` / `rest`).
|
|
420
|
-
|
|
421
|
-
Works for `{ config: … }`-built dimples (reload with no argument re-reads
|
|
422
|
-
the original source). `{ core: … }`-built dimples have no config source
|
|
423
|
-
and reject with `Sdk.Reload`. Boot failure leaves the previous system
|
|
424
|
-
running (blue-green); a drain that cannot settle in-flight calls (30s)
|
|
425
|
-
rejects with `Sdk.Reload` and nothing is torn down.
|
|
426
|
-
|
|
427
|
-
**What happens to data when the database URL changes:** the old database
|
|
428
|
-
file is preserved on disk but no longer served — nothing is migrated or
|
|
429
|
-
merged, the new db is bootstrapped empty (a warning is logged for the
|
|
430
|
-
switch; roll back with `reload(oldConfig)`). Pending jobs in a switched
|
|
431
|
-
`jobs.dbUrl` stay durable in the old db, re-claimable by switching back
|
|
432
|
-
or draining it externally.
|
|
433
|
-
|
|
434
|
-
```ts
|
|
435
|
-
await dimple.reload(); // re-read dimple.jsonc
|
|
436
|
-
await dimple.reload({ storeUrl: "file:…", … }); // or a fresh config
|
|
437
|
-
```
|
|
438
|
-
|
|
439
|
-
### `dispose()`
|
|
440
|
-
|
|
441
|
-
Stops the worker, closes sinks + the store. Idempotent; post-dispose calls
|
|
442
|
-
reject with a closed `Sdk.StoreFailure`.
|
|
443
|
-
|
|
444
|
-
## Errors — the closed hierarchy
|
|
445
|
-
|
|
446
|
-
Every promise rejects with typed errors from one family (never raw Effect
|
|
447
|
-
internals). Export surface: `ConfigError`, `NotFoundError`,
|
|
448
|
-
`DuplicateKeyError` (with `logicalKey`/`namespace`), `DuplicateEdgeError`,
|
|
449
|
-
`LLMNotConfiguredError`, `JobFailedError`, `StoreFailure`, `InputError`,
|
|
450
|
-
plus the `remapError` helper (maps unknown failures onto the hierarchy).
|
|
451
|
-
Also importable from the `@ponraaj/dimple-sdk/errors` subpath.
|
|
452
|
-
|
|
453
|
-
**Type exports** (all importable from the package root): `Dimple` (the
|
|
454
|
-
facade interface), `MakeDimpleOptions`, `ConfigInput`, `QueryOptions`,
|
|
455
|
-
`IndexOptions`, `IndexResult`, plus the error classes and the `DimpleError`
|
|
456
|
-
union type. Errors also carry stable `_tag` values (`Sdk.DuplicateKey`,
|
|
457
|
-
`Sdk.NotFound`, `Sdk.InputError`, `Sdk.JobFailed`, `Sdk.LLMNotConfigured`,
|
|
458
|
-
`Sdk.StoreFailure`, …) — the recommended discriminator.
|
|
459
|
-
|
|
460
|
-
## Background jobs (what runs while you work)
|
|
461
|
-
|
|
462
|
-
| Job | Trigger | Work |
|
|
463
|
-
| ------------------------------------------------- | ------------------- | -------------------------------------------------------------------------------------------------------------------------- |
|
|
464
|
-
| `topic.split` / `topic.merge` / `topic.rebalance` | automatic per write | keep every leaf within its size budget (deterministic splits, donor borrow, nearest-sibling merge, uneven-split rebalance) |
|
|
465
|
-
| `memory.embed` | model backfill | re-embed a memory with a target model + route into its tree |
|
|
466
|
-
| `memory.dream` | `dream()` | concept clustering + LLM edge classification |
|
|
467
|
-
| `graph.repair` | `repair()` | graph invariant repair |
|
|
468
|
-
| `audit.write` | query path | async audit-log writes (never blocks queries) — one FUSED row per hybrid query with the per-leg breakdown |
|
|
469
|
-
|
|
470
|
-
Jobs are durable (leased, retried up to `maxAttempts`, resume-safe across
|
|
471
|
-
processes). Every `makeDimple` boots the worker; `dispose` stops it.
|
|
472
|
-
|
|
473
|
-
Every `query` also writes a retrieval audit row (query, namespace, mode,
|
|
474
|
-
per-leg counts/scores, sources) into `retrieval_log` — machine-readable
|
|
475
|
-
query history, persisted asynchronously (never blocks the query).
|
|
476
|
-
|
|
477
|
-
## Storage & logs
|
|
73
|
+
## Configuration
|
|
478
74
|
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
| Grammar WASMs | `~/.cache/dimple/tree-sitter/` (downloaded once, offline after) |
|
|
485
|
-
| ONNX model | bun/npm cache (~90 MB, first use) |
|
|
75
|
+
`makeDimple({ config: "./dimple.jsonc" })` — every section is optional
|
|
76
|
+
except `embedding`. `dimple init` (CLI) writes the full annotated schema;
|
|
77
|
+
`${ENV_VAR}` placeholders keep tokens out of files. The complete reference
|
|
78
|
+
(store / jobs / embeddings / topics / graph / llm / logging / Turso) is on
|
|
79
|
+
the docs site: [Configuration reference](https://dimple.ponraaj.me/reference/config).
|
|
486
80
|
|
|
487
|
-
##
|
|
81
|
+
## MCP server
|
|
488
82
|
|
|
489
|
-
|
|
490
|
-
|
|
83
|
+
`dimple mcp` (from the CLI package) exposes dimple to any MCP client over
|
|
84
|
+
the 2026-07-28 protocol — API discovery plus sandboxed code execution with
|
|
85
|
+
the whole API as a `dimple` namespace. Guide:
|
|
86
|
+
[MCP server & code mode](https://dimple.ponraaj.me/guides/mcp).
|
|
491
87
|
|
|
492
88
|
## Agent integration patterns
|
|
493
89
|
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
### Session-start injection
|
|
498
|
-
|
|
499
|
-
The guaranteed-usage path: a warmup query returns a compact memory block
|
|
500
|
-
the harness injects as context, and the pending-graph review list rides
|
|
501
|
-
along:
|
|
502
|
-
|
|
503
|
-
```ts
|
|
504
|
-
const [context, pending] = await Promise.all([
|
|
505
|
-
dimple.query("ongoing work and decisions", { limit: 8, budget: { maxChars: 4000 } }),
|
|
506
|
-
dimple.graph.candidates(),
|
|
507
|
-
]);
|
|
508
|
-
// context → inject the top-N content strings into the system prompt
|
|
509
|
-
// pending → offer each candidate as a confirm/reject decision
|
|
510
|
-
```
|
|
511
|
-
|
|
512
|
-
`graph.candidates()` pending `memory_supersedes` edges from the rename
|
|
513
|
-
scanner (rule 0.9x near-duplicates) are the highest-value batch to review.
|
|
514
|
-
|
|
515
|
-
### Supersedes recipe
|
|
90
|
+
Session-start injection, the supersedes recipe, and namespaces as the
|
|
91
|
+
code/message partition are covered on the docs site (Architecture section)
|
|
92
|
+
and in `docs/MCP.md` in the repository.
|
|
516
93
|
|
|
517
|
-
|
|
518
|
-
the full state flip in one transaction (the old memory demotes
|
|
519
|
-
immediately) — it is safe to use directly. For human-gated flows the
|
|
520
|
-
pending → confirm pair keeps the old memory ranking until a reviewer
|
|
521
|
-
agrees:
|
|
522
|
-
|
|
523
|
-
```ts
|
|
524
|
-
const edge = await dimple.graph.addEdge({
|
|
525
|
-
fromMemoryId: oldId,
|
|
526
|
-
toMemoryId: newId,
|
|
527
|
-
edgeKind: "memory_supersedes",
|
|
528
|
-
source: "agent",
|
|
529
|
-
}); // pending — nothing changed yet
|
|
530
|
-
await dimple.graph.confirm(edge.id); // ← the flip happens here
|
|
531
|
-
```
|
|
532
|
-
|
|
533
|
-
### Namespaces as the code/message partition
|
|
534
|
-
|
|
535
|
-
Until a query-time content-type filter exists, namespaces give clean
|
|
536
|
-
isolation: `index({ paths, namespace: "code" })` and
|
|
537
|
-
`write({ …, namespace: "messages" })` partition the store; query with
|
|
538
|
-
`namespace: "code"` for code and `namespace: "messages"` for
|
|
539
|
-
conversations. (`query({ contentType })` now does the same within one
|
|
540
|
-
namespace — the two levers compose.)
|
|
541
|
-
|
|
542
|
-
### Remember
|
|
543
|
-
|
|
544
|
-
- `dimple.reload(newConfig)` swaps the whole stack blue-green (validate →
|
|
545
|
-
pause → boot → drain → swap); a failed boot leaves the old stack
|
|
546
|
-
serving.
|
|
547
|
-
- JSON output is compact by default: CLI `--json` omits embeddings (pass
|
|
548
|
-
`--embedding` to include them); the SDK always keeps them on
|
|
549
|
-
`memory.embedding`.
|
|
550
|
-
|
|
551
|
-
## Notes
|
|
94
|
+
## Changelog
|
|
552
95
|
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
- `migrationFolder` is accepted in `makeDimple` options for advanced users;
|
|
556
|
-
the default path uses migrations embedded at build time (nothing to ship)
|
|
96
|
+
[Release history](https://dimple.ponraaj.me/changelog/sdk) · repository
|
|
97
|
+
[`CHANGELOG.md`](./CHANGELOG.md)
|
package/dist/index.js
CHANGED
|
@@ -508,7 +508,10 @@ var consoleSink = (pretty) => {
|
|
|
508
508
|
};
|
|
509
509
|
};
|
|
510
510
|
var fileSink = (path) => Effect5.tryPromise(async () => {
|
|
511
|
-
const
|
|
511
|
+
const fs = await import("node:fs");
|
|
512
|
+
const { dirname } = await import("node:path");
|
|
513
|
+
fs.mkdirSync(dirname(path), { recursive: true });
|
|
514
|
+
const { openSync, closeSync, fsyncSync, writeSync } = fs;
|
|
512
515
|
const fd = openSync(path, "a");
|
|
513
516
|
let disposed = false;
|
|
514
517
|
const flush = () => {
|
|
@@ -2971,17 +2974,13 @@ var workflowTagFor = (workflows, store, executionId) => Effect17.gen(function* (
|
|
|
2971
2974
|
return;
|
|
2972
2975
|
return workflows.get(record.workflowTag)?.workflow;
|
|
2973
2976
|
});
|
|
2974
|
-
var ensureDbDir = (url) => {
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
};
|
|
2981
|
-
var libsqlStack = (options) => {
|
|
2982
|
-
ensureDbDir(options.url);
|
|
2983
|
-
return Layer6.mergeAll(PersistenceNamespace2.layerSql, PersistedQueue2.layer.pipe(Layer6.provide(storeSqliteLayer))).pipe(Layer6.provide(exports_LibsqlClient.layer(options)));
|
|
2984
|
-
};
|
|
2977
|
+
var ensureDbDir = (url) => url.startsWith("file:") ? import("node:fs").then((fs) => import("node:path").then(({ dirname }) => {
|
|
2978
|
+
fs.mkdirSync(dirname(url.slice("file:".length)), { recursive: true });
|
|
2979
|
+
})) : Promise.resolve();
|
|
2980
|
+
var libsqlStack = (options) => Layer6.unwrap(Effect17.tryPromise({
|
|
2981
|
+
try: () => ensureDbDir(options.url),
|
|
2982
|
+
catch: (cause) => new Error(String(cause))
|
|
2983
|
+
}).pipe(Effect17.catch(() => Effect17.void), Effect17.map(() => Layer6.mergeAll(PersistenceNamespace2.layerSql, PersistedQueue2.layer.pipe(Layer6.provide(storeSqliteLayer))).pipe(Layer6.provide(exports_LibsqlClient.layer(options))))));
|
|
2985
2984
|
var layerDurableLibsql = (options) => layerDurable.pipe(Layer6.provide(libsqlStack(options)));
|
|
2986
2985
|
|
|
2987
2986
|
// ../workflow/src/runtime.ts
|
|
@@ -4734,6 +4733,36 @@ var makeStore2 = (backend, modelConfig, topicsOptions, graphConfig, auditOptions
|
|
|
4734
4733
|
if (!result)
|
|
4735
4734
|
return yield* new NotFound({ id: key });
|
|
4736
4735
|
});
|
|
4736
|
+
const decrementTreeCf = (model, membership, x) => Effect26.tryPromise({
|
|
4737
|
+
try: () => db.transaction(async (tx) => {
|
|
4738
|
+
const leafRows = await tx.select({
|
|
4739
|
+
topicKey: topics.topicKey,
|
|
4740
|
+
memberCount: topics.memberCount,
|
|
4741
|
+
sumSquares: topics.sumSquares
|
|
4742
|
+
}).from(topics).where(and8(eq8(topics.topicKey, membership.topicKey), eq8(topics.modelKey, membership.modelKey)));
|
|
4743
|
+
const leaf = leafRows[0];
|
|
4744
|
+
if (!leaf)
|
|
4745
|
+
return;
|
|
4746
|
+
const next = cfRemove({
|
|
4747
|
+
count: Number(leaf.memberCount),
|
|
4748
|
+
ls: new Float32Array(0),
|
|
4749
|
+
ss: Number(leaf.sumSquares)
|
|
4750
|
+
}, x);
|
|
4751
|
+
await tx.update(topics).set({
|
|
4752
|
+
memberCount: Math.max(0, next.count),
|
|
4753
|
+
sumSquares: Math.max(0, next.ss),
|
|
4754
|
+
updatedAt: Date.now()
|
|
4755
|
+
}).where(and8(eq8(topics.topicKey, membership.topicKey), eq8(topics.modelKey, membership.modelKey)));
|
|
4756
|
+
const relRows = await tx.select({ parentKey: topicRelations.parentKey }).from(topicRelations).where(and8(eq8(topicRelations.childKey, membership.topicKey), eq8(topicRelations.modelKey, membership.modelKey), eq8(topicRelations.relationKind, "parent_child")));
|
|
4757
|
+
let ancestorKey = relRows[0]?.parentKey;
|
|
4758
|
+
while (ancestorKey) {
|
|
4759
|
+
await tx.update(topics).set({ memberCount: sql7`max(0, ${topics.memberCount} - 1)`, updatedAt: Date.now() }).where(and8(eq8(topics.topicKey, ancestorKey), eq8(topics.modelKey, membership.modelKey)));
|
|
4760
|
+
const up = await tx.select({ parentKey: topicRelations.parentKey }).from(topicRelations).where(and8(eq8(topicRelations.childKey, ancestorKey), eq8(topicRelations.modelKey, membership.modelKey), eq8(topicRelations.relationKind, "parent_child")));
|
|
4761
|
+
ancestorKey = up[0]?.parentKey;
|
|
4762
|
+
}
|
|
4763
|
+
}),
|
|
4764
|
+
catch: (cause) => new StoreError({ message: `Store: tree CF decrement: ${cause}` })
|
|
4765
|
+
});
|
|
4737
4766
|
const forget = Effect26.fn("Store.forget")(function* (id) {
|
|
4738
4767
|
let membership;
|
|
4739
4768
|
if (topicsEnabled) {
|
|
@@ -4747,37 +4776,7 @@ var makeStore2 = (backend, modelConfig, topicsOptions, graphConfig, auditOptions
|
|
|
4747
4776
|
const vectorRows = yield* backend.fetchMemoryVectors([id], modelConfig);
|
|
4748
4777
|
if (vectorRows.length > 0) {
|
|
4749
4778
|
const x = new Float32Array(vectorRows[0].vector);
|
|
4750
|
-
|
|
4751
|
-
yield* Effect26.tryPromise({
|
|
4752
|
-
try: () => db.transaction(async (tx) => {
|
|
4753
|
-
const leafRows = await tx.select({
|
|
4754
|
-
topicKey: topics.topicKey,
|
|
4755
|
-
memberCount: topics.memberCount,
|
|
4756
|
-
sumSquares: topics.sumSquares
|
|
4757
|
-
}).from(topics).where(and8(eq8(topics.topicKey, mem.topicKey), eq8(topics.modelKey, mem.modelKey)));
|
|
4758
|
-
const leaf = leafRows[0];
|
|
4759
|
-
if (!leaf)
|
|
4760
|
-
return;
|
|
4761
|
-
const next = cfRemove({
|
|
4762
|
-
count: Number(leaf.memberCount),
|
|
4763
|
-
ls: new Float32Array(0),
|
|
4764
|
-
ss: Number(leaf.sumSquares)
|
|
4765
|
-
}, x);
|
|
4766
|
-
await tx.update(topics).set({
|
|
4767
|
-
memberCount: Math.max(0, next.count),
|
|
4768
|
-
sumSquares: Math.max(0, next.ss),
|
|
4769
|
-
updatedAt: now
|
|
4770
|
-
}).where(and8(eq8(topics.topicKey, mem.topicKey), eq8(topics.modelKey, mem.modelKey)));
|
|
4771
|
-
const relRows = await tx.select({ parentKey: topicRelations.parentKey }).from(topicRelations).where(and8(eq8(topicRelations.childKey, mem.topicKey), eq8(topicRelations.modelKey, mem.modelKey), eq8(topicRelations.relationKind, "parent_child")));
|
|
4772
|
-
let ancestorKey = relRows[0]?.parentKey;
|
|
4773
|
-
while (ancestorKey) {
|
|
4774
|
-
await tx.update(topics).set({ memberCount: sql7`max(0, ${topics.memberCount} - 1)`, updatedAt: now }).where(and8(eq8(topics.topicKey, ancestorKey), eq8(topics.modelKey, mem.modelKey)));
|
|
4775
|
-
const up = await tx.select({ parentKey: topicRelations.parentKey }).from(topicRelations).where(and8(eq8(topicRelations.childKey, ancestorKey), eq8(topicRelations.modelKey, mem.modelKey), eq8(topicRelations.relationKind, "parent_child")));
|
|
4776
|
-
ancestorKey = up[0]?.parentKey;
|
|
4777
|
-
}
|
|
4778
|
-
}),
|
|
4779
|
-
catch: (cause) => new StoreError({ message: `Store.forget: tree CF: ${cause}` })
|
|
4780
|
-
});
|
|
4779
|
+
yield* decrementTreeCf(modelConfig, mem, x);
|
|
4781
4780
|
yield* updateTopicVectorForget(backend, modelConfig, mem.topicKey, x);
|
|
4782
4781
|
}
|
|
4783
4782
|
}
|
|
@@ -5004,6 +5003,28 @@ var makeStore2 = (backend, modelConfig, topicsOptions, graphConfig, auditOptions
|
|
|
5004
5003
|
return yield* new NotFound({ id });
|
|
5005
5004
|
if (modelConfig && modelConfig.id === modelName) {
|
|
5006
5005
|
yield* backend.ensureVectorTable(modelConfig);
|
|
5006
|
+
if (topicsEnabled) {
|
|
5007
|
+
const memberRows = yield* Effect26.tryPromise({
|
|
5008
|
+
try: () => db.select({
|
|
5009
|
+
topicKey: topicMemberships.topicKey,
|
|
5010
|
+
modelKey: topicMemberships.modelKey
|
|
5011
|
+
}).from(topicMemberships).where(eq8(topicMemberships.memoryId, id)),
|
|
5012
|
+
catch: (cause) => new StoreError({ message: `Store.updateEmbedding: load membership: ${cause}` })
|
|
5013
|
+
});
|
|
5014
|
+
const mem = memberRows[0];
|
|
5015
|
+
if (mem && mem.modelKey === modelName) {
|
|
5016
|
+
const oldVec = yield* backend.fetchMemoryVectors([id], modelConfig);
|
|
5017
|
+
if (oldVec.length > 0) {
|
|
5018
|
+
const x = new Float32Array(oldVec[0].vector);
|
|
5019
|
+
yield* decrementTreeCf(modelConfig, mem, x);
|
|
5020
|
+
yield* updateTopicVectorForget(backend, modelConfig, mem.topicKey, x);
|
|
5021
|
+
}
|
|
5022
|
+
yield* Effect26.tryPromise({
|
|
5023
|
+
try: () => db.delete(topicMemberships).where(and8(eq8(topicMemberships.memoryId, id), eq8(topicMemberships.modelKey, modelName))),
|
|
5024
|
+
catch: (cause) => new StoreError({ message: `Store.updateEmbedding: delete membership: ${cause}` })
|
|
5025
|
+
});
|
|
5026
|
+
}
|
|
5027
|
+
}
|
|
5007
5028
|
yield* backend.insertVector(id, embedding, modelConfig);
|
|
5008
5029
|
if (topicsEnabled) {
|
|
5009
5030
|
const routed = yield* routeInsert(id, embedding, modelConfig, topicsConfig).pipe(Effect26.provideService(Service2, backend), Effect26.mapError((cause) => new StoreError({ message: `Store.updateEmbedding topic route: ${cause}` })));
|
|
@@ -6856,8 +6877,8 @@ var layer11 = (options) => Layer13.effect(Service10, Effect35.gen(function* () {
|
|
|
6856
6877
|
const vWeight = profile.modeWeights?.vector ?? 1;
|
|
6857
6878
|
const fWeight = profile.modeWeights?.fts ?? 1;
|
|
6858
6879
|
const mode = vWeight === 0 && fWeight > 0 ? "keyword" : fWeight === 0 && vWeight > 0 ? "semantic" : "hybrid";
|
|
6859
|
-
const denseRanks = vectorResults.length > 0 ? vectorResults.map((s) => [s.memory.id, s.score * vWeight]) : null;
|
|
6860
|
-
const fts5Ranks = fts5Results.length > 0 ? fts5Results.map((s) => [s.memory.id, s.score * fWeight]) : null;
|
|
6880
|
+
const denseRanks = vWeight > 0 && vectorResults.length > 0 ? vectorResults.map((s) => [s.memory.id, s.score * vWeight]) : null;
|
|
6881
|
+
const fts5Ranks = fWeight > 0 && fts5Results.length > 0 ? fts5Results.map((s) => [s.memory.id, s.score * fWeight]) : null;
|
|
6861
6882
|
if (!denseRanks && !fts5Ranks) {
|
|
6862
6883
|
yield* store.logAudit({
|
|
6863
6884
|
query: query2,
|