plugmem 0.5.0 → 0.6.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 +134 -13
- package/index.d.ts +199 -0
- package/index.js +3 -1
- package/package.json +8 -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
|
|
@@ -159,6 +160,7 @@ const res = await db.recall({
|
|
|
159
160
|
tags: ["work"], // filter: a fact must carry all of these
|
|
160
161
|
k: 10, // cap the number of facts
|
|
161
162
|
tokenBudget: 400, // cap the size of the block — your context budget
|
|
163
|
+
graphDepth: 3, // how far to walk from the anchors (default 2)
|
|
162
164
|
});
|
|
163
165
|
|
|
164
166
|
res.rendered; // string, prompt-ready
|
|
@@ -206,6 +208,8 @@ only moves arguments and results across the boundary.
|
|
|
206
208
|
| `path()` | the file this handle resolved to (sync) |
|
|
207
209
|
| `export()` | every open fact as one array (async, unbounded — see below) |
|
|
208
210
|
| `exportPage(cursor?)` | the same data in bounded pages of 128 (async) |
|
|
211
|
+
| `exportEdges(onBatch)` | every current edge, streamed in batches (async) |
|
|
212
|
+
| `configWarnings()` | anything in `config.toml` nothing claimed (sync) |
|
|
209
213
|
|
|
210
214
|
**Upkeep** — all async, all on a worker thread:
|
|
211
215
|
|
|
@@ -214,6 +218,8 @@ only moves arguments and results across the boundary.
|
|
|
214
218
|
| `maintain(mode?)` | `"auto"` (default), `"compact"`, `"reindex-text"`, `"optimize-vectors"`, `"full"`. No mode ever drops a revision or an edge version |
|
|
215
219
|
| `checkpoint()` | flush the journal into a fresh snapshot |
|
|
216
220
|
| `verify()` | full content-integrity sweep; rejects on the first inconsistency |
|
|
221
|
+
| `scrub(options?)` | start a resumable byte-level check of the snapshot |
|
|
222
|
+
| `recover(src, dst, options?)` | module function: salvage a damaged file into a clean copy |
|
|
217
223
|
|
|
218
224
|
**Read-only handles** (`{ readOnly: true }`) observe another process's writer
|
|
219
225
|
over a published snapshot. The read verbs answer, the write verbs throw, and two
|
|
@@ -238,13 +244,106 @@ const res = await db.recall({ query: text, vector: own });
|
|
|
238
244
|
Use it for vectors you already have, for a model that is not an OpenAI-shaped
|
|
239
245
|
HTTP endpoint, or for a deterministic test with no network.
|
|
240
246
|
|
|
241
|
-
###
|
|
247
|
+
### Backing up: facts are only half of it
|
|
242
248
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
249
|
+
`export`/`exportPage` dump facts. An **edge** is a statement between two
|
|
250
|
+
entities — `kim -works_on-> plugmem` — and belongs to no single fact, so a dump
|
|
251
|
+
of facts alone loses the graph. `exportEdges` is the other half.
|
|
252
|
+
|
|
253
|
+
It streams: the walk runs on a worker and hands your callback one batch at a
|
|
254
|
+
time, so memory stays flat whether the graph has ten edges or ten million. When
|
|
255
|
+
a callback is slower than the walk, the *worker* waits — never the event loop.
|
|
256
|
+
|
|
257
|
+
```javascript
|
|
258
|
+
const edges = [];
|
|
259
|
+
const count = await db.exportEdges((batch) => edges.push(...batch));
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
`count` is `2` here, and `edges` is complete the moment the promise resolves —
|
|
263
|
+
no extra tick needed:
|
|
264
|
+
|
|
265
|
+
```json
|
|
266
|
+
[
|
|
267
|
+
{ "src": "kim", "rel": "works_on", "dst": "plugmem", "provenance": 0 },
|
|
268
|
+
{ "src": "kim", "rel": "reports_to", "dst": "ann" }
|
|
269
|
+
]
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
`provenance` is the fact the edge follows from, when it was recorded with one.
|
|
273
|
+
It is **absent** rather than zero when there is none, so it can never be
|
|
274
|
+
mistaken for fact `0` — as the second edge above shows.
|
|
275
|
+
|
|
276
|
+
### Checking a file has not rotted
|
|
277
|
+
|
|
278
|
+
`verify()` and `scrub()` ask different questions, and neither replaces the other:
|
|
279
|
+
|
|
280
|
+
- **`verify()`** — does the *content* agree with itself? Text is valid UTF-8,
|
|
281
|
+
each vector belongs to its fact, both directions of every edge match.
|
|
282
|
+
- **`scrub()`** — are the *bytes* the ones that were written? It recomputes each
|
|
283
|
+
section's checksum and the whole-file hash. This is what catches a flipped bit
|
|
284
|
+
that the structure happily accepts.
|
|
285
|
+
|
|
286
|
+
A scrub is paced by you rather than run in one go, so it stays affordable on a
|
|
287
|
+
live database — the model ZFS uses. Each step checks up to a budget's worth of
|
|
288
|
+
bytes and returns:
|
|
289
|
+
|
|
290
|
+
```javascript
|
|
291
|
+
const scrub = await db.scrub(); // default budget: 1 MiB per step
|
|
292
|
+
let step;
|
|
293
|
+
while ((step = await scrub.next()) !== null) {
|
|
294
|
+
// step.doneBytes of step.totalBytes — progress through the snapshot file
|
|
295
|
+
}
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
`next()` returns a promise because a step reads from disk, not because hashing
|
|
299
|
+
is slow: over a memory-mapped file the bytes are paged in as they are read, so a
|
|
300
|
+
step is I/O of whatever length your storage takes. On the JS thread that would
|
|
301
|
+
freeze the process.
|
|
302
|
+
|
|
303
|
+
Two things to know. **Holding the object holds a lock** on the snapshot
|
|
304
|
+
generation it is scanning, so run it to completion or `close()` it. And it is
|
|
305
|
+
one-shot: after it returns `null`, or throws, `active()` is `false` and you ask
|
|
306
|
+
the database for another.
|
|
307
|
+
|
|
308
|
+
```javascript
|
|
309
|
+
const partial = await db.scrub({ budget: 16 * 1024 });
|
|
310
|
+
await partial.next(); // { doneBytes: 16384, totalBytes: <the file's size> }
|
|
311
|
+
partial.close(); // released; further next() calls return null
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
Damage rejects with `PLUGMEM_ENGINE` naming what failed its checksum.
|
|
315
|
+
|
|
316
|
+
### Repairing a damaged file
|
|
317
|
+
|
|
318
|
+
`recover` is a module function, not a method: it works on **paths**, and takes
|
|
319
|
+
the source's exclusive lock, so close your handle first.
|
|
320
|
+
|
|
321
|
+
```javascript
|
|
322
|
+
import { recover } from "plugmem";
|
|
323
|
+
|
|
324
|
+
const report = await recover("memory.plugmem", "repaired.plugmem");
|
|
325
|
+
// { kept: 1, droppedText: 0, droppedVector: 0, droppedMetadata: 0 }
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
**The source is never written.** It stays exactly as it was, as evidence; this
|
|
329
|
+
produces a repaired copy beside it, and swapping them is your decision. `dst`
|
|
330
|
+
must therefore be a different path — passing the same one throws.
|
|
331
|
+
|
|
332
|
+
The three `dropped` counts are the damage: each is a fact the source could not
|
|
333
|
+
produce intact. All zero means the image was content-clean and this was a
|
|
334
|
+
compaction. Memory stays proportional to the record count rather than the file,
|
|
335
|
+
so a database far larger than RAM can be recovered.
|
|
336
|
+
|
|
337
|
+
It handles **content** damage — the kind `verify()` reports. A snapshot whose
|
|
338
|
+
container will not parse at all is not salvageable here; that is what a backup
|
|
339
|
+
is for.
|
|
340
|
+
|
|
341
|
+
### The one thing the Rust library has and this does not
|
|
342
|
+
|
|
343
|
+
`import`. The JSONL dump format is defined by
|
|
344
|
+
[`plugmem-cli`](https://docs.rs/plugmem-cli/latest), not by the engine — there is
|
|
345
|
+
no `import` verb to mirror. A Node program holding records already has
|
|
346
|
+
`rememberMany` and `link`, which is what an importer is made of.
|
|
248
347
|
|
|
249
348
|
## Errors
|
|
250
349
|
|
|
@@ -294,6 +393,10 @@ path = "/path/to/memory.plugmem"
|
|
|
294
393
|
[engine]
|
|
295
394
|
dim = 768 # embedding size (0 = vectors off)
|
|
296
395
|
|
|
396
|
+
[recall] # optional — every key has a tuned default
|
|
397
|
+
w_vec = 2.0 # trust meaning over keywords in this memory
|
|
398
|
+
half_life_days = 30 # and treat anything older than a month as stale
|
|
399
|
+
|
|
297
400
|
[embedder] # optional — omit for lexical/tag/graph/time only
|
|
298
401
|
kind = "ollama" # or openai / lmstudio / vllm / llamacpp
|
|
299
402
|
url = "http://localhost:11434/v1/embeddings"
|
|
@@ -303,6 +406,24 @@ model = "nomic-embed-text"
|
|
|
303
406
|
fsync = "each_op" # or "on_snapshot": faster, loses the journal tail on an OS crash
|
|
304
407
|
```
|
|
305
408
|
|
|
409
|
+
`[engine]` is what a database is *built* with; changing one of those on an
|
|
410
|
+
existing file is refused. `[recall]` and `[index]` are the opposite — reopening
|
|
411
|
+
with different weights is how you change the ranking, so tune them freely. All
|
|
412
|
+
of them are in the [full settings reference](https://github.com/m62624/plugmem/blob/main/crates/plugmem-host/SETTINGS.md).
|
|
413
|
+
|
|
414
|
+
### When a key is misspelled
|
|
415
|
+
|
|
416
|
+
Unknown keys and sections do not stop anything, but they are not swallowed
|
|
417
|
+
either — a misspelled `w_vec` changes no behaviour, and silence would leave you
|
|
418
|
+
believing you had tuned something. **Read them once after opening**, because a
|
|
419
|
+
native addon has nowhere sensible to print:
|
|
420
|
+
|
|
421
|
+
```javascript
|
|
422
|
+
const db = await Plugmem.open("agent.plugmem", { config: "./plugmem.toml" });
|
|
423
|
+
for (const warning of db.configWarnings()) console.warn(warning);
|
|
424
|
+
// unknown setting [recall].w_vector — did you mean `w_vec`?
|
|
425
|
+
```
|
|
426
|
+
|
|
306
427
|
With an `[embedder]`, a text-only `remember`/`recall` embeds automatically, and
|
|
307
428
|
the provider's HTTP call happens outside the engine lock. The `dim` open option
|
|
308
429
|
sets the embedding size when there is no config; if the config built an
|
|
@@ -393,7 +514,7 @@ processes, sharing the OS page cache.
|
|
|
393
514
|
|
|
394
515
|
## Many memories in one directory
|
|
395
516
|
|
|
396
|
-
**Default: one memory
|
|
517
|
+
**Default: one logical memory backed by a local database layout.** `Plugmem.open(path)` and nothing here
|
|
397
518
|
applies.
|
|
398
519
|
|
|
399
520
|
The problem this solves: a process serving many conversations, tenants or
|
|
@@ -432,7 +553,7 @@ const byOwner: DbEntry[] = await ws.find("ann"); // → the same memo
|
|
|
432
553
|
```
|
|
433
554
|
|
|
434
555
|
A name is `[a-z0-9][a-z0-9_-]*` and **cannot express a path**, so it resolves to
|
|
435
|
-
exactly one
|
|
556
|
+
exactly one named database inside the directory — traversal is not filtered out, it is
|
|
436
557
|
unconstructible. `ws.open(name, false)` refuses a name that does not exist yet,
|
|
437
558
|
which is what a read should do so a typo is diagnosed rather than answered with
|
|
438
559
|
an empty result.
|
|
@@ -477,8 +598,8 @@ data.
|
|
|
477
598
|
|
|
478
599
|
## What it is not for
|
|
479
600
|
|
|
480
|
-
plugmem is for
|
|
481
|
-
operate. Its design centre is around 100 000 active facts on one machine, and
|
|
601
|
+
plugmem is for local-first application and agent memory: one process, one local database,
|
|
602
|
+
no service to operate. Its design centre is around 100 000 active facts on one machine, and
|
|
482
603
|
the benchmarks track 1M-operation profiles to show how the same engine behaves
|
|
483
604
|
under heavier local load.
|
|
484
605
|
|
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.6.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"
|
|
@@ -41,11 +41,11 @@
|
|
|
41
41
|
"typecheck": "tsc -p tsconfig.json"
|
|
42
42
|
},
|
|
43
43
|
"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.
|
|
44
|
+
"plugmem-linux-x64-gnu": "0.6.0",
|
|
45
|
+
"plugmem-linux-arm64-gnu": "0.6.0",
|
|
46
|
+
"plugmem-darwin-x64": "0.6.0",
|
|
47
|
+
"plugmem-darwin-arm64": "0.6.0",
|
|
48
|
+
"plugmem-win32-x64-msvc": "0.6.0",
|
|
49
|
+
"plugmem-win32-arm64-msvc": "0.6.0"
|
|
50
50
|
}
|
|
51
51
|
}
|