plugmem 0.5.0 → 0.7.0
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 +151 -22
- package/index.d.ts +199 -0
- package/index.js +3 -1
- package/package.json +10 -8
package/README.md
CHANGED
|
@@ -5,10 +5,11 @@
|
|
|
5
5
|
> Claude models, in roughly equal measure. Expect non-professional design
|
|
6
6
|
> choices, rough edges, broken behavior, or mistakes. Use it at your own risk.
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
An embeddable bitemporal memory database for local-first applications and
|
|
9
|
+
agents, embedded in your Node process. It stores short facts and answers a
|
|
10
|
+
query with ranked facts and edges plus an optional bounded rendered block.
|
|
10
11
|
|
|
11
|
-
|
|
12
|
+
File-backed on disk, no server, no daemon. It links into your process the way
|
|
12
13
|
SQLite does: the engine is [`plugmem-host`](https://docs.rs/plugmem-host/latest)
|
|
13
14
|
compiled to a native addon through [napi-rs](https://napi.rs), so there is no
|
|
14
15
|
WebAssembly copy of the file in RAM and no 4 GiB ceiling. Runs on Node, Deno and
|
|
@@ -42,13 +43,20 @@ const db = await Plugmem.open("agent.plugmem");
|
|
|
42
43
|
await db.remember({ text: "the user prefers tokio", entity: "user", tags: ["pref"] });
|
|
43
44
|
await db.remember({ text: "the release ships on friday", entity: "release" });
|
|
44
45
|
|
|
45
|
-
const res = await db.recall({ query: "
|
|
46
|
+
const res = await db.recall({ query: "tokio", k: 5 });
|
|
46
47
|
console.log(res.rendered); // paste this into the prompt
|
|
47
|
-
// - [f0] user: the user prefers tokio (2026-08; active)
|
|
48
|
+
// - [f0] user: the user prefers tokio (2026-08; active) #pref
|
|
48
49
|
|
|
49
50
|
db.close();
|
|
50
51
|
```
|
|
51
52
|
|
|
53
|
+
The query is `"tokio"` and not `"which runtime?"` for a reason worth knowing up
|
|
54
|
+
front: with no embedder configured, recall matches on **words**, and "runtime"
|
|
55
|
+
appears nowhere in that fact, so the more natural question returns nothing.
|
|
56
|
+
Reach it through the graph instead with `entities: ["user"]`, or configure an
|
|
57
|
+
[embedder](#configuration-and-embeddings) and the meaning matches too. Only one
|
|
58
|
+
of the four sources needs a model — see [How recall works](#how-recall-works).
|
|
59
|
+
|
|
52
60
|
`Plugmem.open` is a static method, not a constructor, because opening replays a
|
|
53
61
|
journal and maps a snapshot — work proportional to the file — and a JavaScript
|
|
54
62
|
constructor has no way to hand that to a worker thread. Everything is typed:
|
|
@@ -138,12 +146,12 @@ Not a vector lookup. Four sources run and are fused by
|
|
|
138
146
|
[reciprocal-rank fusion](https://plg.uwaterloo.ca/~gvcormac/cormacksigir09-rrf.pdf)
|
|
139
147
|
with a recency boost; tags filter and are not a source:
|
|
140
148
|
|
|
141
|
-
| Source | What it finds |
|
|
142
|
-
|
|
143
|
-
| **Lexical** — [BM25](https://en.wikipedia.org/wiki/Okapi_BM25) over a Unicode ([UAX #29](https://unicode.org/reports/tr29/)) tokenizer | exact terms, keyword overlap |
|
|
144
|
-
| **
|
|
145
|
-
| **
|
|
146
|
-
| **
|
|
149
|
+
| Source | What it finds | Needs an embedder |
|
|
150
|
+
|---|---|---|
|
|
151
|
+
| **Lexical** — [BM25](https://en.wikipedia.org/wiki/Okapi_BM25) over a Unicode ([UAX #29](https://unicode.org/reports/tr29/)) tokenizer | exact terms, keyword overlap | no |
|
|
152
|
+
| **Graph** — typed edges walked from the query's anchor entities | relational knowledge | no |
|
|
153
|
+
| **Temporal** — range scans over the `recordedAt` index, plus the validity test | "what was true then", time windows | no |
|
|
154
|
+
| **Semantic** — int8-quantized cosine, a flat scan below a threshold and an [HNSW](https://arxiv.org/abs/1603.09320) graph above | meaning, nearest neighbours | **yes** |
|
|
147
155
|
|
|
148
156
|
The sources compose. A query with no `query` string still answers from tags,
|
|
149
157
|
entities and time. **Without an embedder the system is complete** — the other
|
|
@@ -159,6 +167,7 @@ const res = await db.recall({
|
|
|
159
167
|
tags: ["work"], // filter: a fact must carry all of these
|
|
160
168
|
k: 10, // cap the number of facts
|
|
161
169
|
tokenBudget: 400, // cap the size of the block — your context budget
|
|
170
|
+
graphDepth: 3, // how far to walk from the anchors (default 2)
|
|
162
171
|
});
|
|
163
172
|
|
|
164
173
|
res.rendered; // string, prompt-ready
|
|
@@ -206,6 +215,8 @@ only moves arguments and results across the boundary.
|
|
|
206
215
|
| `path()` | the file this handle resolved to (sync) |
|
|
207
216
|
| `export()` | every open fact as one array (async, unbounded — see below) |
|
|
208
217
|
| `exportPage(cursor?)` | the same data in bounded pages of 128 (async) |
|
|
218
|
+
| `exportEdges(onBatch)` | every current edge, streamed in batches (async) |
|
|
219
|
+
| `configWarnings()` | anything in `config.toml` nothing claimed (sync) |
|
|
209
220
|
|
|
210
221
|
**Upkeep** — all async, all on a worker thread:
|
|
211
222
|
|
|
@@ -214,6 +225,8 @@ only moves arguments and results across the boundary.
|
|
|
214
225
|
| `maintain(mode?)` | `"auto"` (default), `"compact"`, `"reindex-text"`, `"optimize-vectors"`, `"full"`. No mode ever drops a revision or an edge version |
|
|
215
226
|
| `checkpoint()` | flush the journal into a fresh snapshot |
|
|
216
227
|
| `verify()` | full content-integrity sweep; rejects on the first inconsistency |
|
|
228
|
+
| `scrub(options?)` | start a resumable byte-level check of the snapshot |
|
|
229
|
+
| `recover(src, dst, options?)` | module function: salvage a damaged file into a clean copy |
|
|
217
230
|
|
|
218
231
|
**Read-only handles** (`{ readOnly: true }`) observe another process's writer
|
|
219
232
|
over a published snapshot. The read verbs answer, the write verbs throw, and two
|
|
@@ -238,13 +251,106 @@ const res = await db.recall({ query: text, vector: own });
|
|
|
238
251
|
Use it for vectors you already have, for a model that is not an OpenAI-shaped
|
|
239
252
|
HTTP endpoint, or for a deterministic test with no network.
|
|
240
253
|
|
|
241
|
-
###
|
|
254
|
+
### Backing up: facts are only half of it
|
|
242
255
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
256
|
+
`export`/`exportPage` dump facts. An **edge** is a statement between two
|
|
257
|
+
entities — `kim -works_on-> plugmem` — and belongs to no single fact, so a dump
|
|
258
|
+
of facts alone loses the graph. `exportEdges` is the other half.
|
|
259
|
+
|
|
260
|
+
It streams: the walk runs on a worker and hands your callback one batch at a
|
|
261
|
+
time, so memory stays flat whether the graph has ten edges or ten million. When
|
|
262
|
+
a callback is slower than the walk, the *worker* waits — never the event loop.
|
|
263
|
+
|
|
264
|
+
```javascript
|
|
265
|
+
const edges = [];
|
|
266
|
+
const count = await db.exportEdges((batch) => edges.push(...batch));
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
`count` is `2` here, and `edges` is complete the moment the promise resolves —
|
|
270
|
+
no extra tick needed:
|
|
271
|
+
|
|
272
|
+
```json
|
|
273
|
+
[
|
|
274
|
+
{ "src": "kim", "rel": "works_on", "dst": "plugmem", "provenance": 0 },
|
|
275
|
+
{ "src": "kim", "rel": "reports_to", "dst": "ann" }
|
|
276
|
+
]
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
`provenance` is the fact the edge follows from, when it was recorded with one.
|
|
280
|
+
It is **absent** rather than zero when there is none, so it can never be
|
|
281
|
+
mistaken for fact `0` — as the second edge above shows.
|
|
282
|
+
|
|
283
|
+
### Checking a file has not rotted
|
|
284
|
+
|
|
285
|
+
`verify()` and `scrub()` ask different questions, and neither replaces the other:
|
|
286
|
+
|
|
287
|
+
- **`verify()`** — does the *content* agree with itself? Text is valid UTF-8,
|
|
288
|
+
each vector belongs to its fact, both directions of every edge match.
|
|
289
|
+
- **`scrub()`** — are the *bytes* the ones that were written? It recomputes each
|
|
290
|
+
section's checksum and the whole-file hash. This is what catches a flipped bit
|
|
291
|
+
that the structure happily accepts.
|
|
292
|
+
|
|
293
|
+
A scrub is paced by you rather than run in one go, so it stays affordable on a
|
|
294
|
+
live database — the model ZFS uses. Each step checks up to a budget's worth of
|
|
295
|
+
bytes and returns:
|
|
296
|
+
|
|
297
|
+
```javascript
|
|
298
|
+
const scrub = await db.scrub(); // default budget: 1 MiB per step
|
|
299
|
+
let step;
|
|
300
|
+
while ((step = await scrub.next()) !== null) {
|
|
301
|
+
// step.doneBytes of step.totalBytes — progress through the snapshot file
|
|
302
|
+
}
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
`next()` returns a promise because a step reads from disk, not because hashing
|
|
306
|
+
is slow: over a memory-mapped file the bytes are paged in as they are read, so a
|
|
307
|
+
step is I/O of whatever length your storage takes. On the JS thread that would
|
|
308
|
+
freeze the process.
|
|
309
|
+
|
|
310
|
+
Two things to know. **Holding the object holds a lock** on the snapshot
|
|
311
|
+
generation it is scanning, so run it to completion or `close()` it. And it is
|
|
312
|
+
one-shot: after it returns `null`, or throws, `active()` is `false` and you ask
|
|
313
|
+
the database for another.
|
|
314
|
+
|
|
315
|
+
```javascript
|
|
316
|
+
const partial = await db.scrub({ budget: 16 * 1024 });
|
|
317
|
+
await partial.next(); // { doneBytes: 16384, totalBytes: <the file's size> }
|
|
318
|
+
partial.close(); // released; further next() calls return null
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
Damage rejects with `PLUGMEM_ENGINE` naming what failed its checksum.
|
|
322
|
+
|
|
323
|
+
### Repairing a damaged file
|
|
324
|
+
|
|
325
|
+
`recover` is a module function, not a method: it works on **paths**, and takes
|
|
326
|
+
the source's exclusive lock, so close your handle first.
|
|
327
|
+
|
|
328
|
+
```javascript
|
|
329
|
+
import { recover } from "plugmem";
|
|
330
|
+
|
|
331
|
+
const report = await recover("memory.plugmem", "repaired.plugmem");
|
|
332
|
+
// { kept: 1, droppedText: 0, droppedVector: 0, droppedMetadata: 0 }
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
**The source is never written.** It stays exactly as it was, as evidence; this
|
|
336
|
+
produces a repaired copy beside it, and swapping them is your decision. `dst`
|
|
337
|
+
must therefore be a different path — passing the same one throws.
|
|
338
|
+
|
|
339
|
+
The three `dropped` counts are the damage: each is a fact the source could not
|
|
340
|
+
produce intact. All zero means the image was content-clean and this was a
|
|
341
|
+
compaction. Memory stays proportional to the record count rather than the file,
|
|
342
|
+
so a database far larger than RAM can be recovered.
|
|
343
|
+
|
|
344
|
+
It handles **content** damage — the kind `verify()` reports. A snapshot whose
|
|
345
|
+
container will not parse at all is not salvageable here; that is what a backup
|
|
346
|
+
is for.
|
|
347
|
+
|
|
348
|
+
### The one thing the Rust library has and this does not
|
|
349
|
+
|
|
350
|
+
`import`. The JSONL dump format is defined by
|
|
351
|
+
[`plugmem-cli`](https://docs.rs/plugmem-cli/latest), not by the engine — there is
|
|
352
|
+
no `import` verb to mirror. A Node program holding records already has
|
|
353
|
+
`rememberMany` and `link`, which is what an importer is made of.
|
|
248
354
|
|
|
249
355
|
## Errors
|
|
250
356
|
|
|
@@ -294,6 +400,10 @@ path = "/path/to/memory.plugmem"
|
|
|
294
400
|
[engine]
|
|
295
401
|
dim = 768 # embedding size (0 = vectors off)
|
|
296
402
|
|
|
403
|
+
[recall] # optional — every key has a tuned default
|
|
404
|
+
w_vec = 2.0 # trust meaning over keywords in this memory
|
|
405
|
+
half_life_days = 30 # and treat anything older than a month as stale
|
|
406
|
+
|
|
297
407
|
[embedder] # optional — omit for lexical/tag/graph/time only
|
|
298
408
|
kind = "ollama" # or openai / lmstudio / vllm / llamacpp
|
|
299
409
|
url = "http://localhost:11434/v1/embeddings"
|
|
@@ -303,6 +413,24 @@ model = "nomic-embed-text"
|
|
|
303
413
|
fsync = "each_op" # or "on_snapshot": faster, loses the journal tail on an OS crash
|
|
304
414
|
```
|
|
305
415
|
|
|
416
|
+
`[engine]` is what a database is *built* with; changing one of those on an
|
|
417
|
+
existing file is refused. `[recall]` and `[index]` are the opposite — reopening
|
|
418
|
+
with different weights is how you change the ranking, so tune them freely. All
|
|
419
|
+
of them are in the [full settings reference](https://github.com/m62624/plugmem/blob/main/crates/plugmem-host/SETTINGS.md).
|
|
420
|
+
|
|
421
|
+
### When a key is misspelled
|
|
422
|
+
|
|
423
|
+
Unknown keys and sections do not stop anything, but they are not swallowed
|
|
424
|
+
either — a misspelled `w_vec` changes no behaviour, and silence would leave you
|
|
425
|
+
believing you had tuned something. **Read them once after opening**, because a
|
|
426
|
+
native addon has nowhere sensible to print:
|
|
427
|
+
|
|
428
|
+
```javascript
|
|
429
|
+
const db = await Plugmem.open("agent.plugmem", { config: "./plugmem.toml" });
|
|
430
|
+
for (const warning of db.configWarnings()) console.warn(warning);
|
|
431
|
+
// unknown setting [recall].w_vector — did you mean `w_vec`?
|
|
432
|
+
```
|
|
433
|
+
|
|
306
434
|
With an `[embedder]`, a text-only `remember`/`recall` embeds automatically, and
|
|
307
435
|
the provider's HTTP call happens outside the engine lock. The `dim` open option
|
|
308
436
|
sets the embedding size when there is no config; if the config built an
|
|
@@ -393,7 +521,7 @@ processes, sharing the OS page cache.
|
|
|
393
521
|
|
|
394
522
|
## Many memories in one directory
|
|
395
523
|
|
|
396
|
-
**Default: one memory
|
|
524
|
+
**Default: one logical memory backed by a local database layout.** `Plugmem.open(path)` and nothing here
|
|
397
525
|
applies.
|
|
398
526
|
|
|
399
527
|
The problem this solves: a process serving many conversations, tenants or
|
|
@@ -432,7 +560,7 @@ const byOwner: DbEntry[] = await ws.find("ann"); // → the same memo
|
|
|
432
560
|
```
|
|
433
561
|
|
|
434
562
|
A name is `[a-z0-9][a-z0-9_-]*` and **cannot express a path**, so it resolves to
|
|
435
|
-
exactly one
|
|
563
|
+
exactly one named database inside the directory — traversal is not filtered out, it is
|
|
436
564
|
unconstructible. `ws.open(name, false)` refuses a name that does not exist yet,
|
|
437
565
|
which is what a read should do so a typo is diagnosed rather than answered with
|
|
438
566
|
an empty result.
|
|
@@ -477,8 +605,8 @@ data.
|
|
|
477
605
|
|
|
478
606
|
## What it is not for
|
|
479
607
|
|
|
480
|
-
plugmem is for
|
|
481
|
-
operate. Its design centre is around 100 000 active facts on one machine, and
|
|
608
|
+
plugmem is for local-first application and agent memory: one process, one local database,
|
|
609
|
+
no service to operate. Its design centre is around 100 000 active facts on one machine, and
|
|
482
610
|
the benchmarks track 1M-operation profiles to show how the same engine behaves
|
|
483
611
|
under heavier local load.
|
|
484
612
|
|
|
@@ -491,11 +619,12 @@ search. For those, use a dedicated system — [Qdrant](https://qdrant.tech),
|
|
|
491
619
|
|
|
492
620
|
## Other ways in
|
|
493
621
|
|
|
494
|
-
The same engine ships
|
|
622
|
+
The same engine ships five ways. This package is the Node one.
|
|
495
623
|
|
|
496
624
|
| You are | Use |
|
|
497
625
|
|---|---|
|
|
498
626
|
| writing JavaScript / TypeScript for Node | **this package** |
|
|
627
|
+
| writing Python | [`plugmem`](https://pypi.org/project/plugmem/) on PyPI |
|
|
499
628
|
| writing Rust | [`plugmem-host`](https://docs.rs/plugmem-host/latest) — the engine in your process |
|
|
500
629
|
| an agent, or another language | [`plugmem-mcp`](https://docs.rs/plugmem-mcp/latest) — a stdio JSON-RPC sidecar |
|
|
501
630
|
| a person at a terminal | [`plugmem-cli`](https://docs.rs/plugmem-cli/latest) |
|
package/index.d.ts
CHANGED
|
@@ -98,6 +98,16 @@ export interface RecallArgs {
|
|
|
98
98
|
* the engine is still in the flat regime, below `flat_to_hnsw`.
|
|
99
99
|
*/
|
|
100
100
|
ef?: number
|
|
101
|
+
/**
|
|
102
|
+
* How many edges the graph source may follow from an anchor entity
|
|
103
|
+
* (default: the configured `graph_depth`). `0` asks for the anchors' own
|
|
104
|
+
* facts and no neighbours.
|
|
105
|
+
*
|
|
106
|
+
* Per call for the same reason `k` and `tokenBudget` are: how wide a net
|
|
107
|
+
* to cast belongs to the question. "What is known around this person"
|
|
108
|
+
* wants more hops than "what is this person's stated preference".
|
|
109
|
+
*/
|
|
110
|
+
graphDepth?: number
|
|
101
111
|
/**
|
|
102
112
|
* A precomputed embedding. Its length must equal the configured `dim`.
|
|
103
113
|
*
|
|
@@ -123,6 +133,53 @@ export interface LinkArgs {
|
|
|
123
133
|
*/
|
|
124
134
|
provenance?: number
|
|
125
135
|
}
|
|
136
|
+
/** Options for [`recover`]. */
|
|
137
|
+
export interface RecoverOptions {
|
|
138
|
+
/**
|
|
139
|
+
* Embedding dimension, as `OpenOptions.dim`. It must match what the source
|
|
140
|
+
* database stores, so a memory with vectors needs this (or a `config`
|
|
141
|
+
* naming the embedder) or the salvage cannot open it.
|
|
142
|
+
*/
|
|
143
|
+
dim?: number
|
|
144
|
+
/** Path to a `config.toml`; the standard discovery applies when omitted. */
|
|
145
|
+
config?: string
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Salvages a content-corrupt memory: reads `src`, drops the facts that fail
|
|
149
|
+
* the content checks, and writes a clean, compacted image to `dst`.
|
|
150
|
+
*
|
|
151
|
+
* **`src` is never written.** The damaged file stays exactly as it was, as
|
|
152
|
+
* evidence — this produces a repaired copy beside it, and swapping them is
|
|
153
|
+
* your decision, not this function's. `dst` must therefore be a different
|
|
154
|
+
* path.
|
|
155
|
+
*
|
|
156
|
+
* **It handles *content* damage**, the kind `verify` reports: text that is not
|
|
157
|
+
* valid UTF-8, a vector slot that disagrees with its fact, metadata that will
|
|
158
|
+
* not decode. *Structural* damage — a snapshot whose container will not parse
|
|
159
|
+
* at all — is not salvageable here and rejects; that is what a backup is for.
|
|
160
|
+
*
|
|
161
|
+
* Memory stays proportional to the record count rather than the image: the
|
|
162
|
+
* two large pools are streamed through temp files, so a database far bigger
|
|
163
|
+
* than RAM can be recovered as long as its graph fits.
|
|
164
|
+
*
|
|
165
|
+
* **Async**: it opens, sweeps and rewrites a whole database.
|
|
166
|
+
*
|
|
167
|
+
* @throws `PLUGMEM_LOCKED` if either path is open elsewhere; `PLUGMEM_ENGINE`
|
|
168
|
+
* if `dst` resolves to the same file as `src`, or the source will not parse;
|
|
169
|
+
* `PLUGMEM_OPEN` for an IO failure on either path.
|
|
170
|
+
*/
|
|
171
|
+
export declare function recover(src: string, dst: string, options?: RecoverOptions | undefined | null): Promise<RecoverReport>
|
|
172
|
+
/** Options for `Plugmem#scrub`. */
|
|
173
|
+
export interface ScrubOptions {
|
|
174
|
+
/**
|
|
175
|
+
* Bytes to hash per step, at most. Default: the engine's own (1 MiB).
|
|
176
|
+
*
|
|
177
|
+
* This is the knob trading responsiveness for throughput: smaller steps
|
|
178
|
+
* return to JavaScript more often, larger ones spend less time crossing
|
|
179
|
+
* back and forth. It changes no answer, only the grain.
|
|
180
|
+
*/
|
|
181
|
+
budget?: number
|
|
182
|
+
}
|
|
126
183
|
/** One similar / potentially-conflicting live fact surfaced by `remember`. */
|
|
127
184
|
export interface Similar {
|
|
128
185
|
/** The existing fact's id. */
|
|
@@ -263,6 +320,52 @@ export interface ExportedFact {
|
|
|
263
320
|
/** Validity start (unix ms; preserved on import). */
|
|
264
321
|
validFrom: number
|
|
265
322
|
}
|
|
323
|
+
/**
|
|
324
|
+
* One exported edge — the shape `exportEdges` streams, and the same fields the
|
|
325
|
+
* CLI's JSONL dump writes for an edge.
|
|
326
|
+
*
|
|
327
|
+
* Edges are not part of a fact's dump: a fact names its tags and metadata, but
|
|
328
|
+
* an edge is a statement *between* two entities and outlives any single fact.
|
|
329
|
+
* That is why a complete backup is the two streams together.
|
|
330
|
+
*/
|
|
331
|
+
export interface ExportedEdge {
|
|
332
|
+
/** Source entity name. */
|
|
333
|
+
src: string
|
|
334
|
+
/** The relation, verbatim. */
|
|
335
|
+
rel: string
|
|
336
|
+
/** Destination entity name. */
|
|
337
|
+
dst: string
|
|
338
|
+
/**
|
|
339
|
+
* The fact this edge follows from, if it was recorded with one. Absent
|
|
340
|
+
* rather than a sentinel, so "no provenance" cannot be mistaken for fact 0.
|
|
341
|
+
*/
|
|
342
|
+
provenance?: number
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* What a `recover` salvaged, and what it had to leave behind.
|
|
346
|
+
*
|
|
347
|
+
* The three `dropped` counts are the damage: each is a fact the source could
|
|
348
|
+
* not produce intact, so a non-zero total means the recovered copy is smaller
|
|
349
|
+
* than the original claimed to be. Zeroes across the board mean the image was
|
|
350
|
+
* content-clean and this was a compaction.
|
|
351
|
+
*/
|
|
352
|
+
export interface RecoverReport {
|
|
353
|
+
/** Facts written to the destination — the survivors. */
|
|
354
|
+
kept: number
|
|
355
|
+
/** Dropped: the stored text was not valid UTF-8. */
|
|
356
|
+
droppedText: number
|
|
357
|
+
/** Dropped: the vector slot was out of range or disagreed with the fact. */
|
|
358
|
+
droppedVector: number
|
|
359
|
+
/** Dropped: the metadata blob did not decode to a well-formed map. */
|
|
360
|
+
droppedMetadata: number
|
|
361
|
+
}
|
|
362
|
+
/** How far a `Scrub` has got. */
|
|
363
|
+
export interface ScrubProgress {
|
|
364
|
+
/** Bytes checksummed so far; equals `totalBytes` on the last step. */
|
|
365
|
+
doneBytes: number
|
|
366
|
+
/** The generation file's length — the total to checksum. */
|
|
367
|
+
totalBytes: number
|
|
368
|
+
}
|
|
266
369
|
/** One bounded page returned by `exportPage`. */
|
|
267
370
|
export interface ExportPage {
|
|
268
371
|
/** Open facts in fact-id order; never longer than the native page bound. */
|
|
@@ -498,6 +601,18 @@ export declare class Plugmem {
|
|
|
498
601
|
* snapshot (`PLUGMEM_NEEDS_CHECKPOINT`), or on an IO error.
|
|
499
602
|
*/
|
|
500
603
|
static open(path?: string | undefined | null, options?: OpenOptions | undefined | null): Promise<Plugmem>
|
|
604
|
+
/**
|
|
605
|
+
* What `config.toml` said that nothing claimed — a misspelled key, a
|
|
606
|
+
* misspelled section — one human-readable line each, empty when the file
|
|
607
|
+
* was clean.
|
|
608
|
+
*
|
|
609
|
+
* It is a value rather than a printed warning because a native addon has
|
|
610
|
+
* nowhere sensible to print: stderr belongs to the host application, which
|
|
611
|
+
* may be a server that logs as JSON. **Read it once after opening and log
|
|
612
|
+
* it your own way** — ignoring it puts you back where a typo silently
|
|
613
|
+
* changes nothing.
|
|
614
|
+
*/
|
|
615
|
+
configWarnings(): Array<string>
|
|
501
616
|
/**
|
|
502
617
|
* The file this memory is open on.
|
|
503
618
|
*
|
|
@@ -594,6 +709,50 @@ export declare class Plugmem {
|
|
|
594
709
|
* mutate it during a snapshot-style backup; a read-only handle is stable.
|
|
595
710
|
*/
|
|
596
711
|
exportPage(cursor?: number | undefined | null): Promise<ExportPage>
|
|
712
|
+
/**
|
|
713
|
+
* Streams every current edge to `onBatch`, in batches, and resolves with
|
|
714
|
+
* the number streamed.
|
|
715
|
+
*
|
|
716
|
+
* **The other half of a backup.** `export`/`exportPage` dump facts; an edge
|
|
717
|
+
* is a statement between two entities and belongs to no single fact, so a
|
|
718
|
+
* dump of facts alone silently loses the graph.
|
|
719
|
+
*
|
|
720
|
+
* **Async and bounded.** The walk runs on a libuv worker; batches reach
|
|
721
|
+
* `onBatch` on the JS thread through a threadsafe function with a queue of
|
|
722
|
+
* four. When the callback is slower than the walk, the *worker* waits — the
|
|
723
|
+
* event loop never does, and peak memory is four batches whatever the
|
|
724
|
+
* database's size.
|
|
725
|
+
*
|
|
726
|
+
* **When the promise resolves, every batch has been delivered.** Worth
|
|
727
|
+
* saying because a threadsafe-function call only queues work for the JS
|
|
728
|
+
* thread, so the natural implementation resolves early and hands you a
|
|
729
|
+
* half-filled accumulator on some runs and a full one on others. Each
|
|
730
|
+
* batch is acknowledged from the JS thread and the worker waits for the
|
|
731
|
+
* receipts, so reading your results straight after the `await` is correct.
|
|
732
|
+
*
|
|
733
|
+
* `onBatch` throwing is not caught here: it surfaces as an uncaught error,
|
|
734
|
+
* as it would from any callback Node invokes on your behalf.
|
|
735
|
+
*/
|
|
736
|
+
exportEdges(onBatch: (edges: ExportedEdge[]) => void): Promise<number>
|
|
737
|
+
/**
|
|
738
|
+
* Starts a resumable byte-level check of the current published snapshot,
|
|
739
|
+
* and resolves with the [`Scrub`](crate::scrub::Scrub) that paces it.
|
|
740
|
+
*
|
|
741
|
+
* The counterpart to `verify`, not a replacement: `verify` asks whether
|
|
742
|
+
* the *content* agrees with itself, this asks whether the *bytes* are the
|
|
743
|
+
* ones that were written. Only the second catches a flipped bit that the
|
|
744
|
+
* structure accepts.
|
|
745
|
+
*
|
|
746
|
+
* Available on a writer as well as a read-only handle: a scrub reads the
|
|
747
|
+
* published generation from disk and has no opinion about which handle
|
|
748
|
+
* asked. It does need something published — checkpoint once first.
|
|
749
|
+
*
|
|
750
|
+
* **The returned object holds a shared lock** on the generation it scans
|
|
751
|
+
* for as long as it lives, so run it to completion or `close()` it.
|
|
752
|
+
*
|
|
753
|
+
* @throws `PLUGMEM_NEEDS_CHECKPOINT` when nothing has been published yet.
|
|
754
|
+
*/
|
|
755
|
+
scrub(options?: ScrubOptions | undefined | null): Promise<Scrub>
|
|
597
756
|
/** One fact's tags, or an empty array for an unknown or tombstoned id. */
|
|
598
757
|
tagsOf(id: number): Array<string>
|
|
599
758
|
/**
|
|
@@ -638,6 +797,46 @@ export declare class Plugmem {
|
|
|
638
797
|
*/
|
|
639
798
|
close(): void
|
|
640
799
|
}
|
|
800
|
+
/**
|
|
801
|
+
* A resumable byte-level check of one snapshot generation.
|
|
802
|
+
*
|
|
803
|
+
* **Holding this object holds a lock.** It pins the generation it is scanning
|
|
804
|
+
* with a shared lock for its whole life, so the writer's garbage collection
|
|
805
|
+
* cannot reclaim that generation under it. Finish the scan, or `close()` it —
|
|
806
|
+
* do not park one indefinitely.
|
|
807
|
+
*
|
|
808
|
+
* One-shot: once it has returned `null`, or thrown, it is done. Ask the
|
|
809
|
+
* database for another to scan again.
|
|
810
|
+
*/
|
|
811
|
+
export declare class Scrub {
|
|
812
|
+
/**
|
|
813
|
+
* Hashes the next slice and resolves with the progress so far, or `null`
|
|
814
|
+
* when the scan is complete.
|
|
815
|
+
*
|
|
816
|
+
* **Async**: see the module note — a step is disk I/O, not arithmetic.
|
|
817
|
+
*
|
|
818
|
+
* Concurrent calls are serialized rather than refused: the cursor has one
|
|
819
|
+
* position, so two overlapping steps would be two different slices of the
|
|
820
|
+
* same scan, which is what a caller pacing it from two places asked for.
|
|
821
|
+
*
|
|
822
|
+
* @throws on the first mismatch, naming what failed its checksum. The
|
|
823
|
+
* scrub is finished at that point; a further call resolves `null`.
|
|
824
|
+
*/
|
|
825
|
+
next(): Promise<ScrubProgress | null>
|
|
826
|
+
/**
|
|
827
|
+
* Releases the pinned generation now, abandoning an unfinished scan.
|
|
828
|
+
*
|
|
829
|
+
* Idempotent, and the same bargain as `Plugmem#close`: waiting for the
|
|
830
|
+
* garbage collector to do it means the writer cannot reclaim that
|
|
831
|
+
* generation until it happens.
|
|
832
|
+
*/
|
|
833
|
+
close(): void
|
|
834
|
+
/**
|
|
835
|
+
* Whether this scrub still has work — `false` once it finished, failed or
|
|
836
|
+
* was closed. Cheap: it inspects the handle, not the file.
|
|
837
|
+
*/
|
|
838
|
+
active(): boolean
|
|
839
|
+
}
|
|
641
840
|
/**
|
|
642
841
|
* A directory of named memories — the napi mirror of
|
|
643
842
|
* [`plugmem_host::Workspace`].
|
package/index.js
CHANGED
|
@@ -310,9 +310,11 @@ if (!nativeBinding) {
|
|
|
310
310
|
throw new Error(`Failed to load native binding`)
|
|
311
311
|
}
|
|
312
312
|
|
|
313
|
-
const { Plugmem, MaintainMode, Workspace, version, about, settingsHelp, skill, skillFull, skillVersion } = nativeBinding
|
|
313
|
+
const { Plugmem, recover, Scrub, MaintainMode, Workspace, version, about, settingsHelp, skill, skillFull, skillVersion } = nativeBinding
|
|
314
314
|
|
|
315
315
|
module.exports.Plugmem = Plugmem
|
|
316
|
+
module.exports.recover = recover
|
|
317
|
+
module.exports.Scrub = Scrub
|
|
316
318
|
module.exports.MaintainMode = MaintainMode
|
|
317
319
|
module.exports.Workspace = Workspace
|
|
318
320
|
module.exports.version = version
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "plugmem",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Native Node.js addon for plugmem: an embedded
|
|
3
|
+
"version": "0.7.0",
|
|
4
|
+
"description": "Native Node.js addon for plugmem: an embedded bitemporal memory and retrieval engine for local-first applications and agents (remember / recall / revise / forget over one local database).",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "git+https://github.com/m62624/plugmem.git"
|
|
@@ -32,20 +32,22 @@
|
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {
|
|
34
34
|
"@napi-rs/cli": "^2.18.4",
|
|
35
|
+
"oxlint": "^1.0.0",
|
|
35
36
|
"typescript": "^5.9.3"
|
|
36
37
|
},
|
|
37
38
|
"scripts": {
|
|
38
39
|
"build": "napi build --platform --release",
|
|
39
40
|
"build:debug": "napi build --platform",
|
|
41
|
+
"lint": "oxlint __test__ --deny-warnings",
|
|
40
42
|
"test": "node --test __test__/*.test.mjs",
|
|
41
43
|
"typecheck": "tsc -p tsconfig.json"
|
|
42
44
|
},
|
|
43
45
|
"optionalDependencies": {
|
|
44
|
-
"plugmem-linux-x64-gnu": "0.
|
|
45
|
-
"plugmem-linux-arm64-gnu": "0.
|
|
46
|
-
"plugmem-darwin-x64": "0.
|
|
47
|
-
"plugmem-darwin-arm64": "0.
|
|
48
|
-
"plugmem-win32-x64-msvc": "0.
|
|
49
|
-
"plugmem-win32-arm64-msvc": "0.
|
|
46
|
+
"plugmem-linux-x64-gnu": "0.7.0",
|
|
47
|
+
"plugmem-linux-arm64-gnu": "0.7.0",
|
|
48
|
+
"plugmem-darwin-x64": "0.7.0",
|
|
49
|
+
"plugmem-darwin-arm64": "0.7.0",
|
|
50
|
+
"plugmem-win32-x64-msvc": "0.7.0",
|
|
51
|
+
"plugmem-win32-arm64-msvc": "0.7.0"
|
|
50
52
|
}
|
|
51
53
|
}
|