@ponraaj/dimple-sdk 0.0.0-alpha.3 → 0.0.0-alpha.5

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 CHANGED
@@ -12,6 +12,25 @@ thin facade over it.
12
12
 
13
13
  > **Status**: alpha (`0.0.0-alpha.n`). The API is stabilizing but may change.
14
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).
33
+
15
34
  ## Install
16
35
 
17
36
  ```bash
@@ -19,8 +38,10 @@ npm install @ponraaj/dimple-sdk
19
38
  ```
20
39
 
21
40
  Runtime dependencies are the only things installed (Effect, libSQL,
22
- Transformers.js, drizzle) — the workspace internals are bundled into the
23
- package.
41
+ Transformers.js, drizzle) — the workspace internals (`@dimple/*`) are
42
+ bundled into the package, including fully **self-contained type
43
+ declarations** (the publish build vendors every internal d.ts and rewrites
44
+ the `@dimple/*` specifiers — plain consumers never need the workspace).
24
45
 
25
46
  ## Quickstart
26
47
 
@@ -36,7 +57,9 @@ const mem = await dimple.write({
36
57
  });
37
58
 
38
59
  // hybrid semantic + FTS search
39
- const results = await dimple.query("how does the pipeline deploy", { limit: 3 });
60
+ const results = await dimple.query("how does the pipeline deploy", {
61
+ limit: 3,
62
+ });
40
63
 
41
64
  // index real code — files + extracted functions/classes/methods become memories
42
65
  const indexed = await dimple.index({ paths: ["./src"] });
@@ -80,7 +103,7 @@ Pass a config file path or an inline object:
80
103
 
81
104
  ```ts
82
105
  const dimple = await makeDimple({ config: "./dimple.jsonc" });
83
- // or
106
+ // or — the inline object mirrors the JSON config exactly (quoted keys):
84
107
  const dimple = await makeDimple({
85
108
  config: {
86
109
  storeUrl: "file:./.dimple/dimple.db",
@@ -114,10 +137,58 @@ const dimple = await makeDimple({
114
137
  | `defaultK` / `defaultLimit` | numbers | retrieval defaults |
115
138
  | `defaultTtlSecs` | number | write TTL default |
116
139
 
140
+ ### Remote databases (Turso)
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
+
117
175
  ### Embedding models
118
176
 
119
- Config-swappable — any Transformers.js ONNX model (the only tested
120
- provider):
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):
121
192
 
122
193
  ```jsonc
123
194
  "embedding": {
@@ -128,11 +199,32 @@ provider):
128
199
  { "id": "bge", "provider": "transformers",
129
200
  "model": "Xenova/bge-small-en-v1.5", "dimensions": 384 },
130
201
  { "id": "bge-base", "provider": "transformers",
131
- "model": "Xenova/bge-base-en-v1.5", "dimensions": 768 }
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": "…" }
132
208
  ]
133
209
  }
134
210
  ```
135
211
 
212
+ **Hosted providers** — the config `provider` field selects the family:
213
+
214
+ | `provider` | Models | `baseURL` needed? |
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
+
136
228
  - `dimensions` must match the model's real output — a mismatch fails typed
137
229
  on first use
138
230
  - Multiple models coexist: each gets its own vector tables
@@ -199,6 +291,11 @@ active `logicalKey` → `Sdk.DuplicateKey` (carries `logicalKey`/`namespace`).
199
291
  **Memory expiry**: `ttlSecs` (or the config `defaultTtlSecs`) schedules
200
292
  expiry; a sweep job retires expired memories in the background.
201
293
 
294
+ `query(text, { budget: { maxUnits, maxChars, maxMs } })` — LLM-facing
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
+
202
299
  ### `get(id)` / `forget(id)`
203
300
 
204
301
  Fetch one memory; soft-delete (cascades edges, rebalances the tree).
@@ -236,7 +333,7 @@ await dimple.index({
236
333
  maxChunkBytes?: 8 * 1024,
237
334
  extensions?: [".ts", ".tsx", ".js", ".jsx", …],
238
335
  });
239
- // → { files, indexed, skipped, unsupported, failed: [{ filePath, message }] }
336
+ // → { files, indexed, updated, reconciled, skipped, commit, unsupported, failed }
240
337
  ```
241
338
 
242
339
  - Tree-sitter parse (TypeScript/JavaScript/TSX/JSX) → the extractor
@@ -247,8 +344,27 @@ await dimple.index({
247
344
  - AST-derived graph: relative imports resolve to indexed files and
248
345
  class→method containment become **confirmed `memory_supports` edges**
249
346
  (`source: "rule"`)
250
- - **Idempotent**: keys are cwd-anchored + deterministic — re-indexing the
251
- same tree skips existing units
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
252
368
  - Per-file failures are collected, never fatal
253
369
 
254
370
  ### `updateEmbedding(id, embedding, modelKey?)` / `embed(text)`
@@ -264,13 +380,19 @@ requires the `llm` config section) or the graph repair pass. Each call
264
380
  gets a fresh run id (always executes). Failures are typed:
265
381
  `Sdk.LLMNotConfigured`, `Sdk.JobFailed`.
266
382
 
267
- ### `explain({ kind: "memory" | "topic" | "concept", id })`
383
+ ### `explain({ kind: "memory" | "topic" | "concept", id }, options?)`
268
384
 
269
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).
270
388
 
271
389
  ### `health(modelKey)` / `graph.*`
272
390
 
273
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
274
396
  - `graph.addEdge({ fromMemoryId, toMemoryId, edgeKind: "memory_supports" |
275
397
  "memory_supersedes" | "memory_contradicts", source: "rule" | "llm" |
276
398
  "agent", weight?, confidence?, confirmed? })` — explicit edges; typed
@@ -278,6 +400,42 @@ Graph neighborhood traversal — `{ nodes: [{ kind, id, depth, edgeKind }] }`.
278
400
  - `graph.candidates()` — pending structural candidates + isolated count
279
401
  - `graph.coverage(modelKey)` — coverage %, pending, concept count
280
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
+
281
439
  ### `dispose()`
282
440
 
283
441
  Stops the worker, closes sinks + the store. Idempotent; post-dispose calls
@@ -326,6 +484,70 @@ query history, persisted asynchronously (never blocks the query).
326
484
  | Grammar WASMs | `~/.cache/dimple/tree-sitter/` (downloaded once, offline after) |
327
485
  | ONNX model | bun/npm cache (~90 MB, first use) |
328
486
 
487
+ ## Changelog
488
+
489
+ Every release creates a GitHub Release with the per-version changes (the
490
+ tag-based publish workflow): repo → **Releases** page.
491
+
492
+ ## Agent integration patterns
493
+
494
+ These are the shapes that work well when the consumer is another agent
495
+ (a harness) rather than a human with a REPL.
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
516
+
517
+ `addEdge({ … edgeKind: "memory_supersedes", confirmed: true })` performs
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
+
329
551
  ## Notes
330
552
 
331
553
  - Determinism: same data + same config ⇒ same tree, same clusters, same
package/dist/dimple.d.ts CHANGED
@@ -1,9 +1,11 @@
1
- import { ManagedRuntime } from "effect";
1
+ import { Effect } from "effect";
2
2
  import type { QueryProfile } from "./internal/retriever/index.js";
3
3
  import type { CoreInterface } from "./internal/core/index.js";
4
4
  import type { CodeExtractor } from "./internal/extractor/index.js";
5
5
  import type { CodeParser } from "./internal/parser/index.js";
6
+ import type { DimpleConfig } from "./internal/config/index.js";
6
7
  import type { AddEdgeInput, AugmentedMemory, ExplainResult, GraphCandidatesResult, GraphCoverage, MemoryUnit, TopicHealth, WriteMemoryInput } from "./internal/store/index.js";
8
+ import type { ConfigInput } from "./config.ts";
7
9
  import { type DimpleError } from "./errors.ts";
8
10
  /** Options for a `query` call — mirrors the hybrid retriever's options. */
9
11
  export interface QueryOptions {
@@ -16,6 +18,19 @@ export interface QueryOptions {
16
18
  readonly topics?: {
17
19
  readonly topicKeys?: ReadonlyArray<string>;
18
20
  };
21
+ /** Content-type filter: one kind or a list (e.g. `"text/x-typescript"`
22
+ * or `["text/plain", "text/markdown"]`) — the code-vs-message
23
+ * separation lever at query time. */
24
+ readonly contentType?: string | readonly string[];
25
+ /** Retrieval context budget (LLM-facing guardrail): `maxUnits` caps the
26
+ * row count, `maxChars` truncates result content to fit the total char
27
+ * budget, `maxMs` marks the result truncated when the query exceeds it.
28
+ * Truncation is surfaced as `results.truncated` on the returned array. */
29
+ readonly budget?: {
30
+ readonly maxUnits?: number;
31
+ readonly maxChars?: number;
32
+ readonly maxMs?: number;
33
+ };
19
34
  }
20
35
  /** Options for a `write` call. */
21
36
  export interface WriteOptions {
@@ -49,22 +64,58 @@ export interface IndexResult {
49
64
  readonly files: number;
50
65
  /** Memory units written (file unit + extracted chunks). */
51
66
  readonly indexed: number;
52
- /** Units skipped as already-indexed (duplicate logical keys). */
67
+ /** Units whose active copy changed content — the old unit was
68
+ * superseded (confirmed `memory_supersedes` edge) and the new unit
69
+ * written under the same key. */
70
+ readonly updated: number;
71
+ /** Units whose source vanished from disk (deleted/moved file or
72
+ * definition) — demoted to superseded (`source_removed`); they no
73
+ * longer match queries. */
74
+ readonly reconciled: number;
75
+ /** The repo HEAD at index time (null outside a git tree) — stamped into
76
+ * every file/chunk unit's metadata as `codeVersion`. */
77
+ /** Units skipped as already-indexed (unchanged content under the same
78
+ * logical key). */
53
79
  readonly skipped: number;
54
80
  /** Files skipped because the extension is not indexable. */
55
81
  readonly unsupported: number;
82
+ readonly commit: string | null;
56
83
  /** Files that could not be walked/read/parsed/extracted/written. */
57
84
  readonly failed: readonly {
58
85
  readonly filePath: string;
59
86
  readonly message: string;
60
87
  }[];
61
88
  }
89
+ /** What a `reload` actually changed — so callers can react (alert, log,
90
+ * refuse) instead of guessing. */
91
+ export interface ReloadChanges {
92
+ /** The store database URL changed — the new db was bootstrapped (empty
93
+ * schema); the old db file is preserved on disk but no longer served.
94
+ * Nothing is migrated or merged. */
95
+ readonly storeUrl: boolean;
96
+ /** The jobs database URL changed — pending jobs stay durable in the
97
+ * old db (re-claimable by switching back or draining it externally). */
98
+ readonly jobsUrl: boolean;
99
+ /** Any other section changed (models, topics, graph, logging, …). */
100
+ readonly rest: boolean;
101
+ }
102
+ /** Result of a `reload`. */
103
+ export interface ReloadResult {
104
+ readonly config: DimpleConfig;
105
+ readonly changed: ReloadChanges;
106
+ /** `false` when the resolved config was identical to the live one —
107
+ * nothing was paused, booted, or restarted (safe for watchers). */
108
+ readonly reloaded: boolean;
109
+ }
62
110
  /**
63
111
  * The promise-based SDK facade — the ONLY surface app code touches.
64
112
  * Every method runs the underlying Effect at the boundary, remaps errors
65
113
  * onto the closed `DimpleError` hierarchy, and never leaks Effect types.
66
114
  */
67
115
  export interface Dimple {
116
+ /** The config-default embedding model's key — what `dream`/`repair`/
117
+ * `stats` use when no model is passed. */
118
+ readonly defaultModelId: string;
68
119
  readonly write: (input: WriteMemoryInput, options?: WriteOptions) => Promise<MemoryUnit>;
69
120
  readonly get: (id: string) => Promise<MemoryUnit>;
70
121
  readonly query: (text: string, options?: QueryOptions) => Promise<AugmentedMemory[]>;
@@ -83,19 +134,66 @@ export interface Dimple {
83
134
  readonly embed: (text: string) => Promise<number[]>;
84
135
  readonly dream: (modelKey: string) => Promise<void>;
85
136
  readonly repair: (modelKey: string) => Promise<void>;
137
+ /** The store's tables + columns (snake_case DB names — the SDK exposes
138
+ * camelCase domain names; `dimple db-schema` prints this map). */
139
+ readonly dbSchema: () => Promise<ReadonlyArray<{
140
+ readonly table: string;
141
+ readonly columns: ReadonlyArray<{
142
+ readonly name: string;
143
+ readonly type: string;
144
+ }>;
145
+ }>>;
146
+ /** Options for `explain` — pass `edgeKinds` for a scoped answer (the
147
+ * common ask is memory-only navigation: the three `memory_*` kinds keep
148
+ * the topic/concept legs out). */
86
149
  readonly explain: (input: {
87
150
  readonly kind: "memory" | "topic" | "concept";
88
151
  readonly id: string;
152
+ }, options?: {
153
+ readonly edgeKinds?: readonly string[];
154
+ readonly maxDepth?: number;
155
+ readonly maxNeighbors?: number;
156
+ readonly maxTotal?: number;
157
+ readonly includePending?: boolean;
89
158
  }) => Promise<ExplainResult>;
90
159
  readonly health: (modelKey: string) => Promise<TopicHealth>;
91
160
  readonly graph: {
92
161
  readonly addEdge: (input: AddEdgeInput) => Promise<unknown>;
93
162
  readonly candidates: () => Promise<GraphCandidatesResult>;
94
163
  readonly coverage: (modelKey: string) => Promise<GraphCoverage>;
164
+ /** Promote a pending edge to confirmed (for `memory_supersedes` the
165
+ * old endpoint flips to superseded in the same transaction). */
166
+ readonly confirm: (id: string) => Promise<void>;
167
+ /** Reject a pending edge (removed from the pending review set). */
168
+ readonly reject: (id: string) => Promise<void>;
169
+ /** Soft-delete an edge (`expired_at` set; excluded from traversal). */
170
+ readonly expire: (id: string) => Promise<void>;
95
171
  };
172
+ /**
173
+ * Apply a config change: pause (new calls wait, in-flight calls settle),
174
+ * boot a fresh stack with the re-resolved config, and swap it in.
175
+ *
176
+ * With no argument the ORIGINAL config source is re-resolved — a
177
+ * `dimple.jsonc` FILE PATH is re-read from disk, an inline object is
178
+ * re-validated. A config that fails validation or boot rejects the
179
+ * reload and leaves the current system untouched. An identical config
180
+ * resolves as `{ reloaded: false }` — no pause, no restart.
181
+ */
182
+ readonly reload: (input?: ConfigInput) => Promise<ReloadResult>;
96
183
  /** Stops the worker, closes the sinks and the store — idempotent. */
97
184
  readonly dispose: () => Promise<void>;
98
185
  }
186
+ /**
187
+ * The live handles the facade reads at call time. `makeDimple` swaps the
188
+ * stack under this state on `reload`, so methods always hit the stack that
189
+ * is current when they run.
190
+ */
191
+ export interface FacadeState {
192
+ readonly run: <A, E>(effect: Effect.Effect<A, E>) => Promise<A>;
193
+ readonly core: () => CoreInterface;
194
+ readonly parser: () => CodeParser["Service"];
195
+ readonly extractor: () => CodeExtractor["Service"];
196
+ }
99
197
  /** Build the promise facade over the booted core + runtime. */
100
- export declare const makeFacade: <ER>(runtime: ManagedRuntime.ManagedRuntime<never, ER>, core: CoreInterface, parser: CodeParser["Service"], extractor: CodeExtractor["Service"], dispose: () => Promise<void>) => Dimple;
198
+ export declare const makeFacade: (state: FacadeState, reload: (input?: ConfigInput) => Promise<ReloadResult>, dispose: () => Promise<void>) => Dimple;
101
199
  export type { DimpleError };
package/dist/errors.d.ts CHANGED
@@ -45,19 +45,41 @@ declare const DuplicateEdgeError_base: Schema.Class<DuplicateEdgeError, Schema.T
45
45
  /** A duplicate memory↔memory edge (same from/to/kind already exists). */
46
46
  export declare class DuplicateEdgeError extends DuplicateEdgeError_base {
47
47
  }
48
+ declare const EdgeNotFoundError_base: Schema.Class<EdgeNotFoundError, Schema.TaggedStruct<"Sdk.EdgeNotFound", {
49
+ readonly id: Schema.optional<Schema.String>;
50
+ readonly message: Schema.String;
51
+ }>, import("effect/Cause").YieldableError>;
52
+ /** The edge does not exist (confirm/reject/expire on a stale id). */
53
+ export declare class EdgeNotFoundError extends EdgeNotFoundError_base {
54
+ }
48
55
  declare const StoreFailure_base: Schema.Class<StoreFailure, Schema.TaggedStruct<"Sdk.StoreFailure", {
49
56
  readonly message: Schema.String;
50
57
  }>, import("effect/Cause").YieldableError>;
51
58
  /** Anything else — the typed fallback that keeps the union closed. */
52
59
  export declare class StoreFailure extends StoreFailure_base {
53
60
  }
61
+ declare const ReloadError_base: Schema.Class<ReloadError, Schema.TaggedStruct<"Sdk.Reload", {
62
+ readonly message: Schema.String;
63
+ }>, import("effect/Cause").YieldableError>;
64
+ /** `reload` was unavailable (no config source, disposed) or failed to
65
+ * drain in-flight calls before the swap. */
66
+ export declare class ReloadError extends ReloadError_base {
67
+ }
68
+ declare const EmbeddingFailure_base: Schema.Class<EmbeddingFailure, Schema.TaggedStruct<"Sdk.Embedding", {
69
+ readonly message: Schema.String;
70
+ }>, import("effect/Cause").YieldableError>;
71
+ /** Embedding pipeline failure — the `@huggingface/transformers` package is
72
+ * missing (local embeddings), the model failed to load, or the output
73
+ * dimensions mismatch the config. */
74
+ export declare class EmbeddingFailure extends EmbeddingFailure_base {
75
+ }
54
76
  declare const InputError_base: Schema.Class<InputError, Schema.TaggedStruct<"Sdk.InputError", {
55
77
  readonly message: Schema.String;
56
78
  }>, import("effect/Cause").YieldableError>;
57
79
  /** Invalid SDK call input (bad `limit`, `k`, profile shape, …). */
58
80
  export declare class InputError extends InputError_base {
59
81
  }
60
- export type DimpleError = ConfigError | NotFoundError | DuplicateKeyError | DuplicateEdgeError | LLMNotConfiguredError | JobFailedError | StoreFailure | InputError;
82
+ export type DimpleError = ConfigError | NotFoundError | DuplicateKeyError | DuplicateEdgeError | EdgeNotFoundError | LLMNotConfiguredError | EmbeddingFailure | JobFailedError | StoreFailure | ReloadError | InputError;
61
83
  /** Map an underlying failure onto the closed SDK hierarchy. */
62
84
  export declare const remapError: (error: unknown) => DimpleError;
63
85
  export {};
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export { makeDimple, type MakeDimpleOptions } from "./make.ts";
2
2
  export { type ConfigInput } from "./config.ts";
3
- export { type Dimple, type QueryOptions, type IndexOptions, type IndexResult } from "./dimple.ts";
4
- export { ConfigError, NotFoundError, DuplicateKeyError, DuplicateEdgeError, LLMNotConfiguredError, JobFailedError, StoreFailure, InputError, remapError, type DimpleError, } from "./errors.ts";
3
+ export type { DimpleConfig } from "./internal/config/index.js";
4
+ export { type Dimple, type QueryOptions, type IndexOptions, type IndexResult, type ReloadChanges, type ReloadResult, } from "./dimple.ts";
5
+ export { ConfigError, NotFoundError, DuplicateKeyError, DuplicateEdgeError, EdgeNotFoundError, LLMNotConfiguredError, JobFailedError, StoreFailure, ReloadError, EmbeddingFailure, InputError, remapError, type DimpleError, } from "./errors.ts";