@ponraaj/dimple-sdk 0.0.0-alpha.6 → 0.0.0-alpha.8
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 +56 -514
- package/dist/dimple.d.ts +6 -0
- package/dist/index.js +242 -78
- package/dist/internal/embedder/auto-install.d.ts +8 -33
- package/dist/internal/installer/index.d.ts +26 -0
- package/dist/internal/store/service.d.ts +8 -1
- 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,60 @@ 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
|
-
|
|
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
|
+
| `writeMany(units, { embed })` | Bulk ingest: batch insert + batch embed + routing in one pass (skips the per-write candidate scan — quadratic under bulk; run the dream job afterwards for graph edges) |
|
|
61
|
+
| `get(id)` / `forget(id)` | Fetch / remove by id or logical key |
|
|
62
|
+
| `query(text, options)` | Hybrid search — profiles, budgets, content-type filter |
|
|
63
|
+
| `updateEmbedding(id, vec, modelKey?)` | Attach an external embedding |
|
|
64
|
+
| `index({ paths })` | Codebase ingestion (idempotent re-index) |
|
|
65
|
+
| `graph.addEdge/confirm/reject/expire/candidates/coverage` | Memory relationships: supports / supersedes / contradicts |
|
|
66
|
+
| `explain({ kind, id }, options)` | Bounded graph neighborhood |
|
|
67
|
+
| `dream(modelKey?)` / `repair(modelKey?)` | Consolidation + maintenance jobs |
|
|
68
|
+
| `health(modelKey?)` | Topic-tree invariant health |
|
|
69
|
+
| `reload(input?)` | Blue-green config reload |
|
|
70
|
+
|
|
71
|
+
Errors are a closed hierarchy (`InputError`, `NotFound`, `DuplicateKey`,
|
|
72
|
+
`JobFailed`, `LLMNotConfigured`, `StoreFailure`) — see the docs site.
|
|
293
73
|
|
|
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
|
|
74
|
+
## Configuration
|
|
478
75
|
|
|
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) |
|
|
76
|
+
`makeDimple({ config: "./dimple.jsonc" })` — every section is optional
|
|
77
|
+
except `embedding`. `dimple init` (CLI) writes the full annotated schema;
|
|
78
|
+
`${ENV_VAR}` placeholders keep tokens out of files. The complete reference
|
|
79
|
+
(store / jobs / embeddings / topics / graph / llm / logging / Turso) is on
|
|
80
|
+
the docs site: [Configuration reference](https://dimple.ponraaj.me/reference/config).
|
|
486
81
|
|
|
487
|
-
##
|
|
82
|
+
## MCP server
|
|
488
83
|
|
|
489
|
-
|
|
490
|
-
|
|
84
|
+
`dimple mcp` (from the CLI package) exposes dimple to any MCP client over
|
|
85
|
+
the 2026-07-28 protocol — API discovery plus sandboxed code execution with
|
|
86
|
+
the whole API as a `dimple` namespace. Guide:
|
|
87
|
+
[MCP server & code mode](https://dimple.ponraaj.me/guides/mcp).
|
|
491
88
|
|
|
492
89
|
## Agent integration patterns
|
|
493
90
|
|
|
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
|
|
91
|
+
Session-start injection, the supersedes recipe, and namespaces as the
|
|
92
|
+
code/message partition are covered on the docs site (Architecture section)
|
|
93
|
+
and in `docs/MCP.md` in the repository.
|
|
516
94
|
|
|
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
|
|
95
|
+
## Changelog
|
|
552
96
|
|
|
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)
|
|
97
|
+
[Release history](https://dimple.ponraaj.me/changelog/sdk/) · repository
|
|
98
|
+
[`CHANGELOG.md`](./CHANGELOG.md)
|
package/dist/dimple.d.ts
CHANGED
|
@@ -117,6 +117,12 @@ export interface Dimple {
|
|
|
117
117
|
* `stats` use when no model is passed. */
|
|
118
118
|
readonly defaultModelId: string;
|
|
119
119
|
readonly write: (input: WriteMemoryInput, options?: WriteOptions) => Promise<MemoryUnit>;
|
|
120
|
+
/** Bulk write: batch insert + batch embed + topic routing in ONE pass.
|
|
121
|
+
* Skips the per-write graph candidate scan (O(leaf size) per write —
|
|
122
|
+
* quadratic under bulk ingest); callers that need near-duplicate/
|
|
123
|
+
* conflict edges on bulk content should run the dream job afterwards.
|
|
124
|
+
* All-or-nothing: a duplicate logical key rejects the whole batch. */
|
|
125
|
+
readonly writeMany: (units: readonly WriteMemoryInput[], options?: WriteOptions) => Promise<readonly MemoryUnit[]>;
|
|
120
126
|
readonly get: (id: string) => Promise<MemoryUnit>;
|
|
121
127
|
readonly query: (text: string, options?: QueryOptions) => Promise<AugmentedMemory[]>;
|
|
122
128
|
readonly forget: (id: string) => Promise<void>;
|
package/dist/index.js
CHANGED
|
@@ -766,6 +766,7 @@ import { Effect as Effect7, Schema as Schema7 } from "effect";
|
|
|
766
766
|
import { parse, parseTree, printParseErrorCode } from "jsonc-parser";
|
|
767
767
|
var CONFIG_FILE_NAMES = ["dimple.jsonc", "dimple.json"];
|
|
768
768
|
var GLOBAL_DIR = ".config/dimple";
|
|
769
|
+
var resolveCwd = () => process.env["DIMPLE_CWD"] ?? process.cwd();
|
|
769
770
|
var defaults = {
|
|
770
771
|
storeUrl: "file:./.dimple/dimple.db",
|
|
771
772
|
defaultK: 60,
|
|
@@ -871,7 +872,7 @@ var loadFs = () => Effect7.tryPromise(() => import("node:fs"));
|
|
|
871
872
|
var findProjectConfig = () => Effect7.gen(function* () {
|
|
872
873
|
const fs = yield* loadFs();
|
|
873
874
|
const path = yield* Effect7.tryPromise(() => import("node:path"));
|
|
874
|
-
let dir =
|
|
875
|
+
let dir = resolveCwd();
|
|
875
876
|
for (let i = 0;i < 20; i++) {
|
|
876
877
|
for (const name of CONFIG_FILE_NAMES) {
|
|
877
878
|
const p = path.join(dir, name);
|
|
@@ -940,6 +941,23 @@ var resolveEnvVars = (value) => {
|
|
|
940
941
|
}
|
|
941
942
|
return value;
|
|
942
943
|
};
|
|
944
|
+
var resolveCwdUrls = (value) => {
|
|
945
|
+
const cwd = process.env["DIMPLE_CWD"];
|
|
946
|
+
if (cwd === undefined)
|
|
947
|
+
return value;
|
|
948
|
+
if (typeof value === "string" && value.startsWith("file:./")) {
|
|
949
|
+
return `file:${cwd}${cwd.endsWith("/") ? "" : "/"}${value.slice("file:./".length)}`;
|
|
950
|
+
}
|
|
951
|
+
if (Array.isArray(value))
|
|
952
|
+
return value.map(resolveCwdUrls);
|
|
953
|
+
if (value !== null && typeof value === "object") {
|
|
954
|
+
const result = {};
|
|
955
|
+
for (const [k, v] of Object.entries(value))
|
|
956
|
+
result[k] = resolveCwdUrls(v);
|
|
957
|
+
return result;
|
|
958
|
+
}
|
|
959
|
+
return value;
|
|
960
|
+
};
|
|
943
961
|
var deepMerge = (base, over) => {
|
|
944
962
|
const out = { ...base };
|
|
945
963
|
for (const [key, value] of Object.entries(over)) {
|
|
@@ -957,7 +975,7 @@ var loadConfigFromFile = (path) => Effect7.gen(function* () {
|
|
|
957
975
|
const parsed = yield* parseJsoncEffect(content, (cause) => new Error(`config parse ${path}: ${cause.message}`));
|
|
958
976
|
const merged = deepMerge(defaults, parsed);
|
|
959
977
|
const resolved = resolveEnvVars(merged);
|
|
960
|
-
return yield* Schema7.decodeUnknownEffect(DimpleConfig, { errors: "all" })(resolved).pipe(Effect7.mapError((e) => new Error(`config validation: ${e.message}`)));
|
|
978
|
+
return yield* Schema7.decodeUnknownEffect(DimpleConfig, { errors: "all" })(resolveCwdUrls(resolved)).pipe(Effect7.mapError((e) => new Error(`config validation: ${e.message}`)));
|
|
961
979
|
});
|
|
962
980
|
var loadConfig = (overrides) => Effect7.gen(function* () {
|
|
963
981
|
const fileParts = yield* loadFromFiles();
|
|
@@ -970,7 +988,7 @@ var loadConfig = (overrides) => Effect7.gen(function* () {
|
|
|
970
988
|
if (overrides)
|
|
971
989
|
merged = deepMerge(merged, overrides);
|
|
972
990
|
const resolved = resolveEnvVars(merged);
|
|
973
|
-
return yield* Schema7.decodeUnknownEffect(DimpleConfig, { errors: "all" })(resolved).pipe(Effect7.mapError((e) => new Error(`config validation: ${e.message}`)));
|
|
991
|
+
return yield* Schema7.decodeUnknownEffect(DimpleConfig, { errors: "all" })(resolveCwdUrls(resolved)).pipe(Effect7.mapError((e) => new Error(`config validation: ${e.message}`)));
|
|
974
992
|
});
|
|
975
993
|
|
|
976
994
|
// ../config/src/service.ts
|
|
@@ -1236,7 +1254,7 @@ var makeBackend = (url, client, db, migrationFolder) => ({
|
|
|
1236
1254
|
ORDER BY f.rank
|
|
1237
1255
|
LIMIT ?`,
|
|
1238
1256
|
args: [
|
|
1239
|
-
`"${
|
|
1257
|
+
query.split(/\s+/).filter((token) => token.length > 0).map((token) => `"${token.replaceAll('"', '""')}"`).join(" "),
|
|
1240
1258
|
namespace,
|
|
1241
1259
|
...topicKeys && topicKeys.length > 0 ? [...topicKeys, modelKey ?? ""] : [],
|
|
1242
1260
|
...typeof contentType === "string" ? [contentType] : contentType ?? [],
|
|
@@ -4610,6 +4628,58 @@ var makeStore2 = (backend, modelConfig, topicsOptions, graphConfig, auditOptions
|
|
|
4610
4628
|
embeddingModel: undefined
|
|
4611
4629
|
};
|
|
4612
4630
|
});
|
|
4631
|
+
const writeMany = Effect26.fn("Store.writeMany")(function* (inputs) {
|
|
4632
|
+
if (inputs.length === 0)
|
|
4633
|
+
return [];
|
|
4634
|
+
for (const input of inputs) {
|
|
4635
|
+
if (input.content.includes("\x00")) {
|
|
4636
|
+
return yield* new StoreError({
|
|
4637
|
+
message: "Store.writeMany: content contains a NUL byte (libSQL would silently truncate it)"
|
|
4638
|
+
});
|
|
4639
|
+
}
|
|
4640
|
+
}
|
|
4641
|
+
const now = Date.now();
|
|
4642
|
+
const rows = inputs.map((input) => {
|
|
4643
|
+
const metadataBlob = input.typedMetadata !== undefined ? mergeMetadataBlob(input.metadata, input.typedMetadata) : input.metadata ?? null;
|
|
4644
|
+
return {
|
|
4645
|
+
id: crypto.randomUUID(),
|
|
4646
|
+
logicalKey: input.logicalKey,
|
|
4647
|
+
content: input.content,
|
|
4648
|
+
contentType: input.contentType ?? "text/plain",
|
|
4649
|
+
createdAt: now,
|
|
4650
|
+
ttlSecs: input.ttlSecs === null ? null : input.ttlSecs ?? defaultTtlSecs,
|
|
4651
|
+
namespace: input.namespace ?? "default",
|
|
4652
|
+
metadata: metadataBlob
|
|
4653
|
+
};
|
|
4654
|
+
});
|
|
4655
|
+
yield* Effect26.tryPromise({
|
|
4656
|
+
try: () => db.transaction(async (tx) => {
|
|
4657
|
+
await tx.insert(memories2).values(rows);
|
|
4658
|
+
await tx.insert(memoryEvents2).values(rows.map((row) => ({
|
|
4659
|
+
memoryId: row.id,
|
|
4660
|
+
eventType: "created",
|
|
4661
|
+
createdAt: now
|
|
4662
|
+
})));
|
|
4663
|
+
}),
|
|
4664
|
+
catch: (cause) => backend.isUniqueViolation(cause) ? new DuplicateActiveKey({
|
|
4665
|
+
logicalKey: inputs[0].logicalKey,
|
|
4666
|
+
namespace: inputs[0].namespace ?? "default"
|
|
4667
|
+
}) : new StoreError({ message: `Store.writeMany: ${cause}` })
|
|
4668
|
+
});
|
|
4669
|
+
return rows.map((row) => ({
|
|
4670
|
+
id: row.id,
|
|
4671
|
+
logicalKey: row.logicalKey,
|
|
4672
|
+
content: row.content,
|
|
4673
|
+
contentType: row.contentType,
|
|
4674
|
+
state: "active",
|
|
4675
|
+
createdAt: row.createdAt,
|
|
4676
|
+
ttlSecs: row.ttlSecs,
|
|
4677
|
+
namespace: row.namespace,
|
|
4678
|
+
metadata: row.metadata ?? undefined,
|
|
4679
|
+
embedding: undefined,
|
|
4680
|
+
embeddingModel: undefined
|
|
4681
|
+
}));
|
|
4682
|
+
});
|
|
4613
4683
|
const get = Effect26.fn("Store.get")(function* (id) {
|
|
4614
4684
|
const rows = yield* Effect26.tryPromise({
|
|
4615
4685
|
try: () => db.select(memoryFields).from(memories2).where(eq8(memories2.id, id)).limit(1),
|
|
@@ -4733,6 +4803,36 @@ var makeStore2 = (backend, modelConfig, topicsOptions, graphConfig, auditOptions
|
|
|
4733
4803
|
if (!result)
|
|
4734
4804
|
return yield* new NotFound({ id: key });
|
|
4735
4805
|
});
|
|
4806
|
+
const decrementTreeCf = (model, membership, x) => Effect26.tryPromise({
|
|
4807
|
+
try: () => db.transaction(async (tx) => {
|
|
4808
|
+
const leafRows = await tx.select({
|
|
4809
|
+
topicKey: topics.topicKey,
|
|
4810
|
+
memberCount: topics.memberCount,
|
|
4811
|
+
sumSquares: topics.sumSquares
|
|
4812
|
+
}).from(topics).where(and8(eq8(topics.topicKey, membership.topicKey), eq8(topics.modelKey, membership.modelKey)));
|
|
4813
|
+
const leaf = leafRows[0];
|
|
4814
|
+
if (!leaf)
|
|
4815
|
+
return;
|
|
4816
|
+
const next = cfRemove({
|
|
4817
|
+
count: Number(leaf.memberCount),
|
|
4818
|
+
ls: new Float32Array(0),
|
|
4819
|
+
ss: Number(leaf.sumSquares)
|
|
4820
|
+
}, x);
|
|
4821
|
+
await tx.update(topics).set({
|
|
4822
|
+
memberCount: Math.max(0, next.count),
|
|
4823
|
+
sumSquares: Math.max(0, next.ss),
|
|
4824
|
+
updatedAt: Date.now()
|
|
4825
|
+
}).where(and8(eq8(topics.topicKey, membership.topicKey), eq8(topics.modelKey, membership.modelKey)));
|
|
4826
|
+
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")));
|
|
4827
|
+
let ancestorKey = relRows[0]?.parentKey;
|
|
4828
|
+
while (ancestorKey) {
|
|
4829
|
+
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)));
|
|
4830
|
+
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")));
|
|
4831
|
+
ancestorKey = up[0]?.parentKey;
|
|
4832
|
+
}
|
|
4833
|
+
}),
|
|
4834
|
+
catch: (cause) => new StoreError({ message: `Store: tree CF decrement: ${cause}` })
|
|
4835
|
+
});
|
|
4736
4836
|
const forget = Effect26.fn("Store.forget")(function* (id) {
|
|
4737
4837
|
let membership;
|
|
4738
4838
|
if (topicsEnabled) {
|
|
@@ -4746,37 +4846,7 @@ var makeStore2 = (backend, modelConfig, topicsOptions, graphConfig, auditOptions
|
|
|
4746
4846
|
const vectorRows = yield* backend.fetchMemoryVectors([id], modelConfig);
|
|
4747
4847
|
if (vectorRows.length > 0) {
|
|
4748
4848
|
const x = new Float32Array(vectorRows[0].vector);
|
|
4749
|
-
|
|
4750
|
-
yield* Effect26.tryPromise({
|
|
4751
|
-
try: () => db.transaction(async (tx) => {
|
|
4752
|
-
const leafRows = await tx.select({
|
|
4753
|
-
topicKey: topics.topicKey,
|
|
4754
|
-
memberCount: topics.memberCount,
|
|
4755
|
-
sumSquares: topics.sumSquares
|
|
4756
|
-
}).from(topics).where(and8(eq8(topics.topicKey, mem.topicKey), eq8(topics.modelKey, mem.modelKey)));
|
|
4757
|
-
const leaf = leafRows[0];
|
|
4758
|
-
if (!leaf)
|
|
4759
|
-
return;
|
|
4760
|
-
const next = cfRemove({
|
|
4761
|
-
count: Number(leaf.memberCount),
|
|
4762
|
-
ls: new Float32Array(0),
|
|
4763
|
-
ss: Number(leaf.sumSquares)
|
|
4764
|
-
}, x);
|
|
4765
|
-
await tx.update(topics).set({
|
|
4766
|
-
memberCount: Math.max(0, next.count),
|
|
4767
|
-
sumSquares: Math.max(0, next.ss),
|
|
4768
|
-
updatedAt: now
|
|
4769
|
-
}).where(and8(eq8(topics.topicKey, mem.topicKey), eq8(topics.modelKey, mem.modelKey)));
|
|
4770
|
-
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")));
|
|
4771
|
-
let ancestorKey = relRows[0]?.parentKey;
|
|
4772
|
-
while (ancestorKey) {
|
|
4773
|
-
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)));
|
|
4774
|
-
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")));
|
|
4775
|
-
ancestorKey = up[0]?.parentKey;
|
|
4776
|
-
}
|
|
4777
|
-
}),
|
|
4778
|
-
catch: (cause) => new StoreError({ message: `Store.forget: tree CF: ${cause}` })
|
|
4779
|
-
});
|
|
4849
|
+
yield* decrementTreeCf(modelConfig, mem, x);
|
|
4780
4850
|
yield* updateTopicVectorForget(backend, modelConfig, mem.topicKey, x);
|
|
4781
4851
|
}
|
|
4782
4852
|
}
|
|
@@ -4972,7 +5042,7 @@ var makeStore2 = (backend, modelConfig, topicsOptions, graphConfig, auditOptions
|
|
|
4972
5042
|
}
|
|
4973
5043
|
return yield* topicHealth(modelKey, topicsConfig).pipe(Effect26.provideService(Service2, backend));
|
|
4974
5044
|
});
|
|
4975
|
-
const updateEmbedding = Effect26.fn("Store.updateEmbedding")(function* (id, embedding, modelName) {
|
|
5045
|
+
const updateEmbedding = Effect26.fn("Store.updateEmbedding")(function* (id, embedding, modelName, options) {
|
|
4976
5046
|
const now = Date.now();
|
|
4977
5047
|
const affected = yield* Effect26.tryPromise({
|
|
4978
5048
|
try: () => db.transaction(async (tx) => {
|
|
@@ -5003,10 +5073,34 @@ var makeStore2 = (backend, modelConfig, topicsOptions, graphConfig, auditOptions
|
|
|
5003
5073
|
return yield* new NotFound({ id });
|
|
5004
5074
|
if (modelConfig && modelConfig.id === modelName) {
|
|
5005
5075
|
yield* backend.ensureVectorTable(modelConfig);
|
|
5076
|
+
if (topicsEnabled) {
|
|
5077
|
+
const memberRows = yield* Effect26.tryPromise({
|
|
5078
|
+
try: () => db.select({
|
|
5079
|
+
topicKey: topicMemberships.topicKey,
|
|
5080
|
+
modelKey: topicMemberships.modelKey
|
|
5081
|
+
}).from(topicMemberships).where(eq8(topicMemberships.memoryId, id)),
|
|
5082
|
+
catch: (cause) => new StoreError({ message: `Store.updateEmbedding: load membership: ${cause}` })
|
|
5083
|
+
});
|
|
5084
|
+
const mem = memberRows[0];
|
|
5085
|
+
if (mem && mem.modelKey === modelName) {
|
|
5086
|
+
const oldVec = yield* backend.fetchMemoryVectors([id], modelConfig);
|
|
5087
|
+
if (oldVec.length > 0) {
|
|
5088
|
+
const x = new Float32Array(oldVec[0].vector);
|
|
5089
|
+
yield* decrementTreeCf(modelConfig, mem, x);
|
|
5090
|
+
yield* updateTopicVectorForget(backend, modelConfig, mem.topicKey, x);
|
|
5091
|
+
}
|
|
5092
|
+
yield* Effect26.tryPromise({
|
|
5093
|
+
try: () => db.delete(topicMemberships).where(and8(eq8(topicMemberships.memoryId, id), eq8(topicMemberships.modelKey, modelName))),
|
|
5094
|
+
catch: (cause) => new StoreError({ message: `Store.updateEmbedding: delete membership: ${cause}` })
|
|
5095
|
+
});
|
|
5096
|
+
}
|
|
5097
|
+
}
|
|
5006
5098
|
yield* backend.insertVector(id, embedding, modelConfig);
|
|
5007
5099
|
if (topicsEnabled) {
|
|
5008
5100
|
const routed = yield* routeInsert(id, embedding, modelConfig, topicsConfig).pipe(Effect26.provideService(Service2, backend), Effect26.mapError((cause) => new StoreError({ message: `Store.updateEmbedding topic route: ${cause}` })));
|
|
5009
|
-
|
|
5101
|
+
if (options?.scanCandidates !== false) {
|
|
5102
|
+
yield* scanLeafCandidates(id, embedding, routed.topicKey, modelConfig);
|
|
5103
|
+
}
|
|
5010
5104
|
if (routed.overflow) {
|
|
5011
5105
|
const enqueueSplit = topicsOptions?.enqueueSplit;
|
|
5012
5106
|
if (enqueueSplit) {
|
|
@@ -5566,6 +5660,7 @@ var makeStore2 = (backend, modelConfig, topicsOptions, graphConfig, auditOptions
|
|
|
5566
5660
|
});
|
|
5567
5661
|
return {
|
|
5568
5662
|
write,
|
|
5663
|
+
writeMany,
|
|
5569
5664
|
writeSuperseding,
|
|
5570
5665
|
reconcile,
|
|
5571
5666
|
reconcileRenameCandidates,
|
|
@@ -6531,10 +6626,16 @@ import { existsSync as existsSync2, readFileSync } from "node:fs";
|
|
|
6531
6626
|
import { join as join2 } from "node:path";
|
|
6532
6627
|
import { pathToFileURL } from "node:url";
|
|
6533
6628
|
|
|
6534
|
-
// ../
|
|
6535
|
-
import { execFile } from "node:child_process";
|
|
6629
|
+
// ../installer/src/index.ts
|
|
6536
6630
|
import { existsSync } from "node:fs";
|
|
6537
6631
|
import { join } from "node:path";
|
|
6632
|
+
|
|
6633
|
+
class InstallerError extends Error {
|
|
6634
|
+
constructor(message) {
|
|
6635
|
+
super(message);
|
|
6636
|
+
this.name = "InstallerError";
|
|
6637
|
+
}
|
|
6638
|
+
}
|
|
6538
6639
|
var detectPackageManager = (cwd) => {
|
|
6539
6640
|
let dir = cwd;
|
|
6540
6641
|
while (true) {
|
|
@@ -6549,38 +6650,74 @@ var detectPackageManager = (cwd) => {
|
|
|
6549
6650
|
return { pm: "npm", root: dir };
|
|
6550
6651
|
const parent = join(dir, "..");
|
|
6551
6652
|
if (parent === dir)
|
|
6552
|
-
return { pm: "
|
|
6653
|
+
return { pm: "npm", root: cwd };
|
|
6553
6654
|
dir = parent;
|
|
6554
6655
|
}
|
|
6555
6656
|
};
|
|
6556
|
-
var
|
|
6557
|
-
|
|
6558
|
-
|
|
6559
|
-
|
|
6560
|
-
|
|
6561
|
-
|
|
6562
|
-
|
|
6563
|
-
|
|
6564
|
-
|
|
6565
|
-
|
|
6566
|
-
"--no-audit",
|
|
6567
|
-
"--no-fund"
|
|
6568
|
-
];
|
|
6569
|
-
case "pnpm":
|
|
6570
|
-
return ["pnpm", "add", "@huggingface/transformers", "--ignore-scripts"];
|
|
6571
|
-
case "yarn":
|
|
6572
|
-
return ["yarn", "add", "@huggingface/transformers", "--ignore-scripts"];
|
|
6573
|
-
}
|
|
6657
|
+
var resolveExecutable = (pm, platform = process.platform) => platform === "win32" && pm !== "bun" ? `${pm}.cmd` : pm;
|
|
6658
|
+
var installArgs = (pm, packages, root) => {
|
|
6659
|
+
const argv = [pm === "npm" ? "install" : "add", ...packages];
|
|
6660
|
+
if (pm !== "bun")
|
|
6661
|
+
argv.push("--ignore-scripts");
|
|
6662
|
+
if (pm === "npm")
|
|
6663
|
+
argv.push("--no-audit", "--no-fund");
|
|
6664
|
+
if (pm === "pnpm" && existsSync(join(root, "pnpm-workspace.yaml")))
|
|
6665
|
+
argv.push("-w");
|
|
6666
|
+
return argv;
|
|
6574
6667
|
};
|
|
6575
|
-
var
|
|
6668
|
+
var installOnce = async (target, packages, opts) => {
|
|
6669
|
+
const { spawn } = await import("node:child_process");
|
|
6670
|
+
return new Promise((resolve, reject) => {
|
|
6671
|
+
const timeoutMs = opts.timeoutMs ?? 300000;
|
|
6672
|
+
const cmd = resolveExecutable(target.pm, opts.platform);
|
|
6673
|
+
const args = installArgs(target.pm, packages, target.root);
|
|
6674
|
+
const manual = `${cmd} ${args.join(" ")}`;
|
|
6675
|
+
const child = spawn(cmd, args, { cwd: target.root, stdio: "ignore" });
|
|
6676
|
+
let settled = false;
|
|
6677
|
+
const timer = setTimeout(() => {
|
|
6678
|
+
child.kill("SIGKILL");
|
|
6679
|
+
if (settled)
|
|
6680
|
+
return;
|
|
6681
|
+
settled = true;
|
|
6682
|
+
reject(new InstallerError(`auto-install timed out after ${timeoutMs} ms — the child was killed; run it manually: ${manual}`));
|
|
6683
|
+
}, timeoutMs);
|
|
6684
|
+
child.on("error", (cause) => {
|
|
6685
|
+
clearTimeout(timer);
|
|
6686
|
+
if (settled)
|
|
6687
|
+
return;
|
|
6688
|
+
settled = true;
|
|
6689
|
+
reject(new InstallerError(`auto-install failed to start (${manual}): ${cause.message} — run it manually`));
|
|
6690
|
+
});
|
|
6691
|
+
child.on("exit", (code, signal) => {
|
|
6692
|
+
clearTimeout(timer);
|
|
6693
|
+
if (settled)
|
|
6694
|
+
return;
|
|
6695
|
+
settled = true;
|
|
6696
|
+
if (code === 0) {
|
|
6697
|
+
resolve();
|
|
6698
|
+
return;
|
|
6699
|
+
}
|
|
6700
|
+
reject(new InstallerError(`auto-install failed (${manual} exited ${signal !== null ? `with ${signal}` : `code ${code ?? "?"}`}) — run it manually`));
|
|
6701
|
+
});
|
|
6702
|
+
});
|
|
6703
|
+
};
|
|
6704
|
+
var inflight = new Map;
|
|
6705
|
+
var installOptionalPeers = (packages, opts) => {
|
|
6706
|
+
const cwd = opts?.cwd ?? process.cwd();
|
|
6576
6707
|
const target = detectPackageManager(cwd);
|
|
6577
|
-
const
|
|
6578
|
-
|
|
6579
|
-
|
|
6580
|
-
|
|
6581
|
-
|
|
6582
|
-
|
|
6583
|
-
|
|
6708
|
+
const key = `${target.root}\x00${packages.join(",")}`;
|
|
6709
|
+
const existing = inflight.get(key);
|
|
6710
|
+
if (existing !== undefined)
|
|
6711
|
+
return existing;
|
|
6712
|
+
const run = installOnce(target, packages, opts ?? {});
|
|
6713
|
+
inflight.set(key, run);
|
|
6714
|
+
run.then(() => inflight.delete(key), () => inflight.delete(key));
|
|
6715
|
+
return run;
|
|
6716
|
+
};
|
|
6717
|
+
// ../embedder/src/auto-install.ts
|
|
6718
|
+
var installTransformers = (cwd) => installOptionalPeers(["@huggingface/transformers"], { cwd, timeoutMs: 300000 }).catch((cause) => {
|
|
6719
|
+
throw new EmbeddingError({
|
|
6720
|
+
message: cause instanceof InstallerError ? `${cause.message} — or set \`autoInstall: false\` and configure a hosted embedding provider` : `auto-install of @huggingface/transformers failed: ${String(cause)} — run it manually ` + "or set `autoInstall: false` and configure a hosted embedding provider"
|
|
6584
6721
|
});
|
|
6585
6722
|
});
|
|
6586
6723
|
|
|
@@ -6645,9 +6782,12 @@ var make5 = (options) => {
|
|
|
6645
6782
|
}
|
|
6646
6783
|
return Effect32.gen(function* () {
|
|
6647
6784
|
yield* Effect32.logInfo("auto-installing @huggingface/transformers (first local-embed use — scripts skipped)").pipe(Effect32.annotateLogs({ scope: "@dimple/embedder", event: "embedder.auto-install" }));
|
|
6648
|
-
yield* Effect32.tryPromise(() => installTransformers(process.cwd())).pipe(Effect32.
|
|
6649
|
-
|
|
6650
|
-
|
|
6785
|
+
yield* Effect32.tryPromise(() => installTransformers(process.cwd())).pipe(Effect32.catch((cause) => {
|
|
6786
|
+
const inner = cause.cause;
|
|
6787
|
+
return Effect32.fail(inner instanceof EmbeddingError ? inner : new EmbeddingError({
|
|
6788
|
+
message: `auto-install failed: ${inner instanceof Error ? inner.message : String(cause)}`
|
|
6789
|
+
}));
|
|
6790
|
+
}));
|
|
6651
6791
|
return yield* Effect32.tryPromise(importFromDisk).pipe(Effect32.mapError(() => new EmbeddingError({
|
|
6652
6792
|
message: "@huggingface/transformers still unavailable after auto-install — run `bun add " + "@huggingface/transformers` manually (or set `autoInstall: false` and configure a hosted embedding provider)"
|
|
6653
6793
|
})));
|
|
@@ -6855,8 +6995,8 @@ var layer11 = (options) => Layer13.effect(Service10, Effect35.gen(function* () {
|
|
|
6855
6995
|
const vWeight = profile.modeWeights?.vector ?? 1;
|
|
6856
6996
|
const fWeight = profile.modeWeights?.fts ?? 1;
|
|
6857
6997
|
const mode = vWeight === 0 && fWeight > 0 ? "keyword" : fWeight === 0 && vWeight > 0 ? "semantic" : "hybrid";
|
|
6858
|
-
const denseRanks = vectorResults.length > 0 ? vectorResults.map((s) => [s.memory.id, s.score * vWeight]) : null;
|
|
6859
|
-
const fts5Ranks = fts5Results.length > 0 ? fts5Results.map((s) => [s.memory.id, s.score * fWeight]) : null;
|
|
6998
|
+
const denseRanks = vWeight > 0 && vectorResults.length > 0 ? vectorResults.map((s) => [s.memory.id, s.score * vWeight]) : null;
|
|
6999
|
+
const fts5Ranks = fWeight > 0 && fts5Results.length > 0 ? fts5Results.map((s) => [s.memory.id, s.score * fWeight]) : null;
|
|
6860
7000
|
if (!denseRanks && !fts5Ranks) {
|
|
6861
7001
|
yield* store.logAudit({
|
|
6862
7002
|
query: query2,
|
|
@@ -8395,8 +8535,8 @@ var walkFiles = (paths2, extensions) => {
|
|
|
8395
8535
|
const files = [];
|
|
8396
8536
|
const errors3 = [];
|
|
8397
8537
|
let unsupported = 0;
|
|
8398
|
-
const resolved = paths2.map((p) => resolve(p));
|
|
8399
|
-
const root = commonRoot([...resolved, process.cwd()]);
|
|
8538
|
+
const resolved = paths2.map((p) => resolve(process.env["DIMPLE_CWD"] ?? process.cwd(), p));
|
|
8539
|
+
const root = commonRoot([...resolved, process.env["DIMPLE_CWD"] ?? process.cwd()]);
|
|
8400
8540
|
const visit = (path2) => {
|
|
8401
8541
|
let stat;
|
|
8402
8542
|
try {
|
|
@@ -8789,6 +8929,30 @@ var makeFacade = (state, reload, dispose) => {
|
|
|
8789
8929
|
return yield* invalid;
|
|
8790
8930
|
return yield* writeUnit(input, options?.embed !== false);
|
|
8791
8931
|
})),
|
|
8932
|
+
writeMany: (units, options) => run(Effect49.gen(function* () {
|
|
8933
|
+
if (!Array.isArray(units)) {
|
|
8934
|
+
return yield* new InputError({ message: "writeMany: units must be an array" });
|
|
8935
|
+
}
|
|
8936
|
+
for (const input of units) {
|
|
8937
|
+
const invalid = validateWriteInput(input);
|
|
8938
|
+
if (invalid !== undefined)
|
|
8939
|
+
return yield* invalid;
|
|
8940
|
+
}
|
|
8941
|
+
const mems = yield* core().store.writeMany(units);
|
|
8942
|
+
if (options?.embed !== false) {
|
|
8943
|
+
yield* core().store.ensureTopicTree(core().defaultModel.id);
|
|
8944
|
+
const vectors = yield* core().defaultEmbedder.embed(mems.map((m) => m.content));
|
|
8945
|
+
for (let i = 0;i < mems.length; i++) {
|
|
8946
|
+
yield* core().store.updateEmbedding(mems[i].id, [...vectors[i]], core().defaultModel.id, {
|
|
8947
|
+
scanCandidates: false
|
|
8948
|
+
});
|
|
8949
|
+
}
|
|
8950
|
+
return yield* Effect49.forEach(mems, (mem) => core().store.get(mem.id), {
|
|
8951
|
+
discard: false
|
|
8952
|
+
});
|
|
8953
|
+
}
|
|
8954
|
+
return mems;
|
|
8955
|
+
})),
|
|
8792
8956
|
get: (id) => run(resolveMemory(id)),
|
|
8793
8957
|
query: runQuery,
|
|
8794
8958
|
forget: (id) => run(resolveMemory(id).pipe(Effect49.flatMap((memory) => core().store.forget(memory.id)))),
|
|
@@ -9089,7 +9253,7 @@ var makeDimple = async (options) => {
|
|
|
9089
9253
|
let disposed = false;
|
|
9090
9254
|
let gate = Promise.resolve();
|
|
9091
9255
|
let releaseGate;
|
|
9092
|
-
let
|
|
9256
|
+
let inflight2 = 0;
|
|
9093
9257
|
let drainWaiters = [];
|
|
9094
9258
|
let reloadChain = Promise.resolve();
|
|
9095
9259
|
const run = (effect) => {
|
|
@@ -9101,12 +9265,12 @@ var makeDimple = async (options) => {
|
|
|
9101
9265
|
return Promise.reject(new StoreFailure({ message: "SDK disposed" }));
|
|
9102
9266
|
}
|
|
9103
9267
|
const stack = current;
|
|
9104
|
-
|
|
9268
|
+
inflight2 += 1;
|
|
9105
9269
|
return stack.runPromise(effect.pipe(Effect50.mapError(remapError))).catch((cause) => {
|
|
9106
9270
|
return Promise.reject(remapError(cause));
|
|
9107
9271
|
}).finally(() => {
|
|
9108
|
-
|
|
9109
|
-
if (
|
|
9272
|
+
inflight2 -= 1;
|
|
9273
|
+
if (inflight2 === 0 && drainWaiters.length > 0) {
|
|
9110
9274
|
const waiters = drainWaiters;
|
|
9111
9275
|
drainWaiters = [];
|
|
9112
9276
|
for (const wake of waiters)
|
|
@@ -9127,7 +9291,7 @@ var makeDimple = async (options) => {
|
|
|
9127
9291
|
release?.();
|
|
9128
9292
|
};
|
|
9129
9293
|
const drain = () => {
|
|
9130
|
-
if (
|
|
9294
|
+
if (inflight2 === 0)
|
|
9131
9295
|
return Promise.resolve();
|
|
9132
9296
|
return new Promise((resolve2) => {
|
|
9133
9297
|
drainWaiters.push(resolve2);
|
|
@@ -1,35 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
*
|
|
6
|
-
* installs). When the config declares a `transformers` provider model,
|
|
7
|
-
* the intent is explicit — so the FIRST local-embed use installs the
|
|
8
|
-
* package automatically with the project's own package manager, instead
|
|
9
|
-
* of failing with a manual step.
|
|
10
|
-
*
|
|
11
|
-
* Safety notes:
|
|
12
|
-
* - The install is scoped to the detected project root (the manifest's
|
|
13
|
-
* directory) and uses the manager the project already uses.
|
|
14
|
-
* - EVERY install runs `--ignore-scripts` (npm/pnpm/yarn) or bun's
|
|
15
|
-
* default script-blocking — verified harmless: the onnxruntime-node
|
|
16
|
-
* binaries ship in the package; its postinstall is not required for
|
|
17
|
-
* embedding, and sharp is never loaded for text embeddings.
|
|
18
|
-
* - Failure (offline, restricted env) is a typed EmbeddingError with the
|
|
19
|
-
* manual command — never a crash.
|
|
20
|
-
*/
|
|
21
|
-
export type PackageManager = "bun" | "npm" | "pnpm" | "yarn";
|
|
22
|
-
export interface InstallTarget {
|
|
23
|
-
readonly pm: PackageManager;
|
|
24
|
-
readonly root: string;
|
|
25
|
-
}
|
|
26
|
-
/** Detect the project's package manager by walking UP from `cwd` for a
|
|
27
|
-
* lockfile (bun.lock/bun.lockb → bun, package-lock.json → npm,
|
|
28
|
-
* pnpm-lock.yaml → pnpm, yarn.lock → yarn). Falls back to bun + cwd. */
|
|
29
|
-
export declare const detectPackageManager: (cwd: string) => InstallTarget;
|
|
30
|
-
/** The install argv for a manager. `--ignore-scripts` on npm/pnpm/yarn:
|
|
31
|
-
* the blocked postinstalls are proven unnecessary for embedding (bun
|
|
32
|
-
* blocks them by default already). */
|
|
1
|
+
import { type PackageManager } from "./internal/installer/index.js";
|
|
2
|
+
export type { InstallTarget, PackageManager } from "./internal/installer/index.js";
|
|
3
|
+
export { detectPackageManager } from "./internal/installer/index.js";
|
|
4
|
+
/** The install argv for a manager with the transformers package (kept for
|
|
5
|
+
* callers/tests that assert the shape). */
|
|
33
6
|
export declare const installCommand: (pm: PackageManager) => readonly [string, ...string[]];
|
|
34
|
-
/**
|
|
7
|
+
/** Install transformers at the project root detected from `cwd`. The
|
|
8
|
+
* shared installer kills the child on timeout and dedups concurrent
|
|
9
|
+
* first-use installs; failures reject with the manual command. */
|
|
35
10
|
export declare const installTransformers: (cwd: string) => Promise<void>;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export type PackageManager = "bun" | "npm" | "pnpm" | "yarn";
|
|
2
|
+
export interface InstallTarget {
|
|
3
|
+
readonly pm: PackageManager;
|
|
4
|
+
readonly root: string;
|
|
5
|
+
}
|
|
6
|
+
export declare class InstallerError extends Error {
|
|
7
|
+
constructor(message: string);
|
|
8
|
+
}
|
|
9
|
+
/** Detect the project's package manager by walking UP from `cwd` for a
|
|
10
|
+
* lockfile. NO lockfile → npm (the most universal — a bun fallback broke
|
|
11
|
+
* Node-only machines with ENOENT). */
|
|
12
|
+
export declare const detectPackageManager: (cwd: string) => InstallTarget;
|
|
13
|
+
/** The executable name for a manager. win32 ships `.cmd` shims for
|
|
14
|
+
* npm/pnpm/yarn — the bare names fail with ENOENT. */
|
|
15
|
+
export declare const resolveExecutable: (pm: PackageManager, platform?: NodeJS.Platform) => string;
|
|
16
|
+
/** The base install argv for a manager. Scripts are skipped everywhere;
|
|
17
|
+
* pnpm at a workspace root additionally gets `-w`. */
|
|
18
|
+
export declare const installArgs: (pm: PackageManager, packages: readonly string[], root: string) => string[];
|
|
19
|
+
export interface InstallOptions {
|
|
20
|
+
readonly cwd?: string;
|
|
21
|
+
/** Hard deadline; the child is SIGKILLed when it trips (default 5 min). */
|
|
22
|
+
readonly timeoutMs?: number;
|
|
23
|
+
/** Injectable for tests (defaults to process.platform). */
|
|
24
|
+
readonly platform?: NodeJS.Platform;
|
|
25
|
+
}
|
|
26
|
+
export declare const installOptionalPeers: (packages: readonly string[], opts?: InstallOptions) => Promise<void>;
|
|
@@ -65,6 +65,11 @@ export interface ExplainResult {
|
|
|
65
65
|
}
|
|
66
66
|
export interface Interface {
|
|
67
67
|
readonly write: (input: WriteMemoryInput) => Effect.Effect<MemoryUnit, StoreError | DuplicateActiveKey>;
|
|
68
|
+
/** Bulk write: batch insert in ONE transaction (all-or-nothing; a
|
|
69
|
+
* duplicate logical key fails the whole batch with DuplicateActiveKey).
|
|
70
|
+
* The per-write graph candidate scan is NOT part of this — pair with
|
|
71
|
+
* `updateEmbedding(..., { scanCandidates: false })` for bulk ingest. */
|
|
72
|
+
readonly writeMany: (inputs: readonly WriteMemoryInput[]) => Effect.Effect<readonly MemoryUnit[], StoreError | DuplicateActiveKey>;
|
|
68
73
|
/** Upsert for the index path — the identity rule (same logical key ⇒
|
|
69
74
|
* confirmed `memory_supersedes`) made reachable for ACTIVE keys: demote
|
|
70
75
|
* the active holder of the key (if any), write the new unit, and link
|
|
@@ -139,7 +144,9 @@ export interface Interface {
|
|
|
139
144
|
readonly forget: (id: string) => Effect.Effect<void, NotFound | StoreError>;
|
|
140
145
|
readonly sweep: (namespace: string) => Effect.Effect<SweepResult, StoreError>;
|
|
141
146
|
readonly events: (memoryId: string) => Effect.Effect<readonly MemoryEvent[], StoreError>;
|
|
142
|
-
readonly updateEmbedding: (id: string, embedding: ReadonlyArray<number>, model: string
|
|
147
|
+
readonly updateEmbedding: (id: string, embedding: ReadonlyArray<number>, model: string, options?: {
|
|
148
|
+
readonly scanCandidates?: boolean;
|
|
149
|
+
}) => Effect.Effect<void, NotFound | StoreError>;
|
|
143
150
|
readonly findStale: (model: string) => Effect.Effect<readonly string[], StoreError>;
|
|
144
151
|
readonly createConcept: (name: string, embedding: ReadonlyArray<number>) => Effect.Effect<Concept, StoreError>;
|
|
145
152
|
readonly getConcept: (id: string) => Effect.Effect<Concept, NotFound | StoreError>;
|