plugmem 0.8.0 → 0.10.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 +136 -46
- package/index.d.ts +131 -22
- package/index.js +2 -1
- package/package.json +7 -7
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# plugmem
|
|
2
2
|
|
|
3
|
-
> ⚠️ Experimental. plugmem is mostly an AI-built experiment
|
|
3
|
+
> ⚠️ Experimental. plugmem is mostly an AI-built experiment, written with
|
|
4
4
|
> the help of a small local model (Qwen3.6-35B-A3B-UD-Q4_K_XL.gguf) and various
|
|
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.
|
|
@@ -9,11 +9,11 @@ An embeddable bitemporal memory database for local-first applications and
|
|
|
9
9
|
agents, embedded in your Node process. It stores short facts and answers a
|
|
10
10
|
query with ranked facts and edges plus an optional bounded rendered block.
|
|
11
11
|
|
|
12
|
-
File-backed on disk, no server, no daemon.
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
WebAssembly copy of the file in RAM and no 4 GiB
|
|
16
|
-
Bun.
|
|
12
|
+
File-backed on disk, no server, no daemon. The
|
|
13
|
+
[`plugmem-host`](https://docs.rs/plugmem-host/latest) engine is compiled to a
|
|
14
|
+
native addon through [napi-rs](https://napi.rs) and linked directly into the
|
|
15
|
+
process, so there is no WebAssembly copy of the file in RAM and no 4 GiB
|
|
16
|
+
ceiling. Runs on Node, Deno and Bun.
|
|
17
17
|
|
|
18
18
|
**Contents:** [Install](#install) · [Quick start](#quick-start) ·
|
|
19
19
|
[What it stores](#what-it-stores) · [Two clocks](#two-clocks) ·
|
|
@@ -47,11 +47,15 @@ const res = await db.recall({ query: "tokio", k: 5 });
|
|
|
47
47
|
console.log(res.rendered); // paste this into the prompt
|
|
48
48
|
// - [f0] user: the user prefers tokio (2026-08; active) #pref
|
|
49
49
|
|
|
50
|
+
const tags = await db.listTags({ prefix: "pre", limit: 64 });
|
|
51
|
+
console.log(tags.items); // [{ name: "pref", count: 1 }]
|
|
52
|
+
// await db.removeTag("pref"); // global: revises every current fact carrying it
|
|
53
|
+
|
|
50
54
|
db.close();
|
|
51
55
|
```
|
|
52
56
|
|
|
53
|
-
The query is `"tokio"`
|
|
54
|
-
|
|
57
|
+
The query is `"tokio"` rather than `"which runtime?"` because recall matches on
|
|
58
|
+
**words** when no embedder is configured, and "runtime"
|
|
55
59
|
appears nowhere in that fact, so the more natural question returns nothing.
|
|
56
60
|
Reach it through the graph instead with `entities: ["user"]`, or configure an
|
|
57
61
|
[embedder](#configuration-and-embeddings) and the meaning matches too. Only one
|
|
@@ -116,16 +120,16 @@ the second query has something to answer with.
|
|
|
116
120
|
|
|
117
121
|
`asOf` moves **both** clocks: a fact answers only if it was valid at that
|
|
118
122
|
instant *and* had already been recorded by then. The second half is the one
|
|
119
|
-
people trip over
|
|
120
|
-
because the memory
|
|
123
|
+
people trip over: an `asOf` earlier than a fact's `recordedAt` sees nothing,
|
|
124
|
+
because the memory had not recorded the fact yet. Answering with today's knowledge
|
|
121
125
|
would be the wrong answer to "what did I hold".
|
|
122
126
|
|
|
123
127
|
`validFrom` is the other half: a statement that became true before you heard of
|
|
124
128
|
it. Recording today that someone moved a week ago closes the previous interval a
|
|
125
|
-
week ago rather than now, so a query as of three days back finds **neither
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
+
week ago rather than now, so a query as of three days back finds **neither**.
|
|
130
|
+
The old fact had stopped being true, and the new one was not yet known. That
|
|
131
|
+
result follows directly from the two clocks; a single timestamp cannot express
|
|
132
|
+
it.
|
|
129
133
|
|
|
130
134
|
Two more queries over the same axes:
|
|
131
135
|
|
|
@@ -176,18 +180,45 @@ res.edges; // { src, rel, dst, provenance }[] — what the graph walked
|
|
|
176
180
|
res.truncated; // true if selection stopped at k or the budget with more left
|
|
177
181
|
```
|
|
178
182
|
|
|
179
|
-
`remember`
|
|
180
|
-
|
|
181
|
-
|
|
183
|
+
`remember` stores the new fact and returns its id **plus any live facts it may
|
|
184
|
+
duplicate or contradict**. If a preflight must not write, use
|
|
185
|
+
`rememberGuarded`: the database holds one write scope across its similarity
|
|
186
|
+
check and conditional insertion, so concurrent preflights cannot both pass.
|
|
187
|
+
|
|
188
|
+
**`entity` is what makes the guard a guard.** The detector compares the new text
|
|
189
|
+
against that entity's most recent live facts and against nothing else, so a
|
|
190
|
+
`rememberGuarded` call with **no** `entity` has no candidates and always returns
|
|
191
|
+
`status: "stored"` - it does not fail, it simply has nothing to compare against.
|
|
192
|
+
Six identical guarded writes with no entity produce six facts; the same six with
|
|
193
|
+
`entity` produce one and five `blocked`.
|
|
194
|
+
|
|
195
|
+
`checked` on the result says whether a comparison happened at all: `false` is a
|
|
196
|
+
fact stored exactly as `remember` would have stored it. Do not read `status:
|
|
197
|
+
"stored"` as "checked and clear" without it.
|
|
198
|
+
|
|
199
|
+
`similar` carries `{ id, score, reason }` - the ids, not the text. Resolve a
|
|
200
|
+
hit's wording with `get(id)` when you want to show the caller what it collided
|
|
201
|
+
with.
|
|
182
202
|
|
|
183
203
|
```typescript
|
|
184
|
-
const
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
204
|
+
const decision = await db.rememberGuarded({
|
|
205
|
+
text: "the user prefers async-std",
|
|
206
|
+
entity: "user",
|
|
207
|
+
});
|
|
208
|
+
if (decision.status === "blocked") {
|
|
209
|
+
for (const s of decision.similar) {
|
|
210
|
+
// revise/forget an old fact, or use ordinary remember to keep both
|
|
211
|
+
console.log(s.id, s.score, s.reason);
|
|
212
|
+
}
|
|
188
213
|
}
|
|
189
214
|
```
|
|
190
215
|
|
|
216
|
+
`blocked` has no `outcome`: it allocated no id and changed neither indexes nor
|
|
217
|
+
journal. Ordinary `remember` is also a safe complete write; it simply never rejects
|
|
218
|
+
one. Do not use `recall` for this check. Recall returns ranked context and can
|
|
219
|
+
return a weak nearest vector; its fused score is not cosine similarity or a
|
|
220
|
+
conflict threshold.
|
|
221
|
+
|
|
191
222
|
## API
|
|
192
223
|
|
|
193
224
|
Every method wraps the identically-named verb of the Rust `Database`; this layer
|
|
@@ -198,9 +229,11 @@ only moves arguments and results across the boundary.
|
|
|
198
229
|
| Method | Does |
|
|
199
230
|
|---|---|
|
|
200
231
|
| `remember(args)` | store a fact; resolves with its id and similar facts |
|
|
232
|
+
| `rememberGuarded(args)` | check similarity and store only if clear, without a check/write race |
|
|
201
233
|
| `rememberMany(args[])` | store a batch: one embedding round-trip, one journal sync |
|
|
202
234
|
| `revise(id, args)` | close a fact and record its successor |
|
|
203
235
|
| `forget(id)` | tombstone a fact; resolves with whether it was live |
|
|
236
|
+
| `removeTag(tag)` | remove a tag from every current fact while preserving facts/history |
|
|
204
237
|
| `link(args)` | upsert a typed edge, optionally with `provenance` |
|
|
205
238
|
| `unlink(args)` | close the current edge; resolves with whether one was open |
|
|
206
239
|
|
|
@@ -211,6 +244,7 @@ only moves arguments and results across the boundary.
|
|
|
211
244
|
| `recall(args?)` | ranked, fused, token-budgeted result (async) |
|
|
212
245
|
| `get(id)` | one fact's full card, or `null` (sync) |
|
|
213
246
|
| `tagsOf(id)` | that fact's tags (sync) |
|
|
247
|
+
| `listTags(options?)` | bounded lexical page of current tags and counts (async) |
|
|
214
248
|
| `stats()` | engine size counters (sync) |
|
|
215
249
|
| `path()` | the file this handle resolved to (sync) |
|
|
216
250
|
| `export()` | every open fact as one array (async, unbounded — see below) |
|
|
@@ -223,11 +257,35 @@ only moves arguments and results across the boundary.
|
|
|
223
257
|
| Method | Does |
|
|
224
258
|
|---|---|
|
|
225
259
|
| `maintain(mode?)` | `"auto"` (default), `"compact"`, `"reindex-text"`, `"optimize-vectors"`, `"full"`. No mode ever drops a revision or an edge version |
|
|
260
|
+
| `reembed(batchSize?)` | explicitly recompute every retained vector with the configured model and publish atomically; never invoked by `maintain('auto')` |
|
|
226
261
|
| `checkpoint()` | flush the journal into a fresh snapshot |
|
|
227
262
|
| `verify()` | full content-integrity sweep; rejects on the first inconsistency |
|
|
228
263
|
| `scrub(options?)` | start a resumable byte-level check of the snapshot |
|
|
229
264
|
| `recover(src, dst, options?)` | module function: salvage a damaged file into a clean copy |
|
|
230
265
|
|
|
266
|
+
The snapshot stores the model's readable vector-space identity, not only its
|
|
267
|
+
dimension. A changed model makes ordinary automatic embedding reject instead
|
|
268
|
+
of mixing incompatible vectors. `reembed` is the deliberate transition; it
|
|
269
|
+
runs on a libuv worker, leaves the JavaScript event loop responsive, keeps reads
|
|
270
|
+
live and makes concurrent writes reject with `PLUGMEM_BUSY`.
|
|
271
|
+
|
|
272
|
+
A mismatch does **not** stop the database opening, on a writer or a read-only
|
|
273
|
+
handle, and loses nothing. What fails is exactly two things: `recall` with a
|
|
274
|
+
`query` and `remember` with `text`. Everything else - `stats`, `get`, `tagsOf`,
|
|
275
|
+
`listTags`, entity/graph recall, `exportPage`, `forget`, `link`, `verify`,
|
|
276
|
+
`maintain`, `checkpoint`, `reembed` - keeps answering. So the content is safe
|
|
277
|
+
and recovery is always available, and a consumer only finds out at its first
|
|
278
|
+
lookup after the change: detect it by making the cheapest text recall and
|
|
279
|
+
watching for the error, rather than from a note of what was configured last
|
|
280
|
+
time.
|
|
281
|
+
|
|
282
|
+
`reembed` is idempotent; it rebuilds ONE database, so a workspace needs a pass
|
|
283
|
+
over every memory in it. On an EMPTY database it still makes one request whose
|
|
284
|
+
input is the empty string - a provider that rejects empty input fails a rebuild
|
|
285
|
+
that had nothing to rebuild. And switching an embedder on over a database built
|
|
286
|
+
without one breaks nothing and warns about nothing: compare `stats().vectors`
|
|
287
|
+
with `stats().facts` to notice the facts that have no vectors yet.
|
|
288
|
+
|
|
231
289
|
**Read-only handles** (`{ readOnly: true }`) observe another process's writer
|
|
232
290
|
over a published snapshot. The read verbs answer, the write verbs throw, and two
|
|
233
291
|
more appear: `generation()` (the pinned snapshot number) and `refresh()` (adopt
|
|
@@ -408,6 +466,7 @@ half_life_days = 30 # and treat anything older than a month as stale
|
|
|
408
466
|
enabled = true # false keeps settings but makes no embedder calls
|
|
409
467
|
url = "http://localhost:11434/v1/embeddings"
|
|
410
468
|
model = "nomic-embed-text"
|
|
469
|
+
space_id = "nomic-embed-text@v1" # optional; defaults to model
|
|
411
470
|
api_key_env = "OPENAI_API_KEY" # env var holding the bearer token
|
|
412
471
|
|
|
413
472
|
[maintenance]
|
|
@@ -440,7 +499,9 @@ embedder, its dimension governs and `dim` must agree.
|
|
|
440
499
|
The host uses one `OpenAiCompatEmbedder` implementation for OpenAI, Ollama,
|
|
441
500
|
LM Studio, vLLM and other OpenAI-compatible servers. `url` is the complete
|
|
442
501
|
embeddings endpoint exactly as provided (nothing is appended), and `model` is
|
|
443
|
-
the model name understood by that server.
|
|
502
|
+
the model name understood by that server. `space_id` optionally identifies the
|
|
503
|
+
exact semantic space and defaults to `model`; it is never discovered over the
|
|
504
|
+
network. Set `enabled = false` to keep the
|
|
444
505
|
settings without creating or calling the embedder; `$PLUGMEM_EMBEDDER_ENABLED`
|
|
445
506
|
overrides it with `true` or `false`.
|
|
446
507
|
|
|
@@ -459,14 +520,15 @@ embedder's HTTP round trip or an fsync would freeze every timer, socket and
|
|
|
459
520
|
callback in the process. Anything that can do that runs on a libuv worker and
|
|
460
521
|
returns a promise instead.
|
|
461
522
|
|
|
462
|
-
Promises: `Plugmem.open`, `remember`, `rememberMany`, `revise`, `recall`,
|
|
463
|
-
`forget`, `link`, `unlink`, `export`, `exportPage`, `verify`, `maintain`,
|
|
464
|
-
`checkpoint`,
|
|
465
|
-
`
|
|
523
|
+
Promises: `Plugmem.open`, `remember`, `rememberGuarded`, `rememberMany`, `revise`, `recall`,
|
|
524
|
+
`forget`, `removeTag`, `listTags`, `link`, `unlink`, `export`, `exportPage`, `verify`, `maintain`,
|
|
525
|
+
`checkpoint`, every database verb on `WorkspaceMemory`, and every registry
|
|
526
|
+
verb on `Workspace`.
|
|
466
527
|
|
|
467
|
-
Synchronous: `path`, `get`, `stats`, `tagsOf`, `generation`,
|
|
468
|
-
|
|
469
|
-
|
|
528
|
+
Synchronous: the direct handle's `path`, `get`, `stats`, `tagsOf`, `generation`,
|
|
529
|
+
`refresh`, `close`; and `Workspace.memory`, `release`, `closeIdle`, `openCount`,
|
|
530
|
+
`close`. Creating a logical reference touches no file. Its own database verbs
|
|
531
|
+
are promises because acquiring a cold lease may open and replay a file.
|
|
470
532
|
|
|
471
533
|
Arguments are still checked on your thread: a refused one **throws** at the call
|
|
472
534
|
site rather than rejecting later, so a mistake in your code and a failure in the
|
|
@@ -542,13 +604,13 @@ import { Workspace, type DbEntry } from "plugmem";
|
|
|
542
604
|
|
|
543
605
|
const ws = new Workspace("/srv/memories");
|
|
544
606
|
|
|
545
|
-
//
|
|
546
|
-
// a
|
|
547
|
-
const chat =
|
|
607
|
+
// This is only a name plus a weak reference to `ws`: no file is opened and no
|
|
608
|
+
// writer lock is held until a verb runs. A first write creates the memory.
|
|
609
|
+
const chat = ws.memory("chat-42");
|
|
548
610
|
await chat.remember({ text: "prefers tokio" });
|
|
549
611
|
|
|
550
612
|
// Another name is another memory. They cannot see each other.
|
|
551
|
-
const other =
|
|
613
|
+
const other = ws.memory("chat-99");
|
|
552
614
|
(await other.recall({ query: "tokio" })).facts.length; // 0
|
|
553
615
|
```
|
|
554
616
|
|
|
@@ -568,10 +630,10 @@ const byOwner: DbEntry[] = await ws.find("ann"); // → the same memo
|
|
|
568
630
|
```
|
|
569
631
|
|
|
570
632
|
A name is `[a-z0-9][a-z0-9_-]*` and **cannot express a path**, so it resolves to
|
|
571
|
-
exactly one named database inside the directory — traversal is not filtered out,
|
|
572
|
-
unconstructible. `
|
|
573
|
-
|
|
574
|
-
an empty result.
|
|
633
|
+
exactly one named database inside the directory — traversal is not filtered out,
|
|
634
|
+
it is unconstructible. `memory(name)` itself creates nothing. A write verb creates
|
|
635
|
+
an unused name; a read verb refuses it, so a typo is diagnosed rather than
|
|
636
|
+
answered with an empty result.
|
|
575
637
|
|
|
576
638
|
**Who may reach which memory is not this package's job.** The name comes from
|
|
577
639
|
your code, so the policy belongs there.
|
|
@@ -580,7 +642,8 @@ your code, so the policy belongs there.
|
|
|
580
642
|
|
|
581
643
|
| Method | Does |
|
|
582
644
|
|---|---|
|
|
583
|
-
| `
|
|
645
|
+
| `memory(name)` | return a lock-free logical `WorkspaceMemory` reference |
|
|
646
|
+
| `release(name)` | evict one inactive pooled handle; references remain valid |
|
|
584
647
|
| `list()` | every memory in the directory, from the filesystem — including undescribed ones |
|
|
585
648
|
| `entries()` | every described memory, from the registry |
|
|
586
649
|
| `find(query, k?)` | memories whose description or owner best matches |
|
|
@@ -592,20 +655,47 @@ your code, so the policy belongs there.
|
|
|
592
655
|
| `openCount()` | how many are open right now (sync) |
|
|
593
656
|
| `close()` | close every pooled memory and the registry |
|
|
594
657
|
|
|
595
|
-
`closeIdle()` matters more than it looks.
|
|
658
|
+
`closeIdle()` matters more than it looks. A pooled database holds its file's
|
|
596
659
|
exclusive lock, so a long-running process that never lets go makes its memories
|
|
597
660
|
unreachable from anything else on the machine. Call it on a timer — that is what
|
|
598
|
-
the idle timeout is for, liveness rather than memory. The pool
|
|
599
|
-
|
|
600
|
-
room
|
|
661
|
+
the idle timeout is for, liveness rather than memory. The pool is a hard bound on
|
|
662
|
+
open databases (`maxOpen`, default 16): an inactive least-recently-used entry is
|
|
663
|
+
closed to make room. If every slot belongs to an active verb, a different memory
|
|
664
|
+
gets `PLUGMEM_BUSY` immediately instead of waiting or opening a hidden extra
|
|
665
|
+
handle:
|
|
601
666
|
|
|
602
667
|
```ts
|
|
603
668
|
const ws = new Workspace("/srv/memories", { maxOpen: 16, idleTimeoutMs: 60_000 });
|
|
604
669
|
setInterval(() => ws.closeIdle(), 30_000);
|
|
605
670
|
```
|
|
606
671
|
|
|
607
|
-
|
|
608
|
-
|
|
672
|
+
Each `WorkspaceMemory` verb takes a scoped lease. While it runs, `release`,
|
|
673
|
+
`closeIdle` and LRU eviction cannot take that entry; after it returns, the entry
|
|
674
|
+
is eligible immediately. `ws.close()` invalidates every logical reference.
|
|
675
|
+
Garbage collection of a `WorkspaceMemory` neither opens nor closes anything.
|
|
676
|
+
|
|
677
|
+
This lifecycle applies only to workspaces. A direct `Plugmem.open(path)` still
|
|
678
|
+
returns an explicitly owned native handle, and `close()` remains how its writer
|
|
679
|
+
lock is released.
|
|
680
|
+
|
|
681
|
+
### Migration from the handle-returning workspace API
|
|
682
|
+
|
|
683
|
+
This is a breaking ownership change:
|
|
684
|
+
|
|
685
|
+
```ts
|
|
686
|
+
// before: a second native owner whose lifetime depended on JavaScript GC
|
|
687
|
+
const memory = await ws.open("chat-42");
|
|
688
|
+
memory.close();
|
|
689
|
+
|
|
690
|
+
// now: a stable logical reference; each verb owns one scoped lease
|
|
691
|
+
const memory = ws.memory("chat-42");
|
|
692
|
+
ws.release("chat-42"); // optional: release an inactive pooled lock now
|
|
693
|
+
```
|
|
694
|
+
|
|
695
|
+
There is no `WorkspaceMemory.close()`: it owns nothing to close. Database reads
|
|
696
|
+
such as `get`, `stats` and `tagsOf` are promises on this class because a cold
|
|
697
|
+
call may have to reopen and replay the file. The same methods on a direct
|
|
698
|
+
`Plugmem` remain synchronous.
|
|
609
699
|
|
|
610
700
|
`verify()` reports and never repairs, because a workspace is a directory a
|
|
611
701
|
person can edit, and guessing at their intent is how a consistency check loses
|
|
@@ -627,7 +717,7 @@ search. For those, use a dedicated system — [Qdrant](https://qdrant.tech),
|
|
|
627
717
|
|
|
628
718
|
## Other ways in
|
|
629
719
|
|
|
630
|
-
|
|
720
|
+
plugmem also ships interfaces for Rust, Python, agents and the terminal.
|
|
631
721
|
|
|
632
722
|
| You are | Use |
|
|
633
723
|
|---|---|
|
|
@@ -638,7 +728,7 @@ The same engine ships five ways. This package is the Node one.
|
|
|
638
728
|
| a person at a terminal | [`plugmem-cli`](https://docs.rs/plugmem-cli/latest) |
|
|
639
729
|
|
|
640
730
|
**Working with an LLM agent?** There is a companion
|
|
641
|
-
[skill](https://github.com/m62624/plugmem/blob/main/
|
|
731
|
+
[skill](https://github.com/m62624/plugmem/blob/main/skills/plugmem/SKILL.md) describing
|
|
642
732
|
the remember/recall loop, the contradiction workflow and the verbs. This package
|
|
643
733
|
ships it: `skill()` returns the text and `skillVersion()` the version it was
|
|
644
734
|
written against.
|
package/index.d.ts
CHANGED
|
@@ -133,6 +133,15 @@ export interface LinkArgs {
|
|
|
133
133
|
*/
|
|
134
134
|
provenance?: number
|
|
135
135
|
}
|
|
136
|
+
/** Bounded tag-catalog options. Omitted fields select the first 64 entries. */
|
|
137
|
+
export interface TagListOptions {
|
|
138
|
+
/** Exact, case-sensitive prefix. */
|
|
139
|
+
prefix?: string
|
|
140
|
+
/** Opaque cursor returned by the previous page. */
|
|
141
|
+
cursor?: string
|
|
142
|
+
/** Page size (maximum 256). */
|
|
143
|
+
limit?: number
|
|
144
|
+
}
|
|
136
145
|
/** Options for [`recover`]. */
|
|
137
146
|
export interface RecoverOptions {
|
|
138
147
|
/**
|
|
@@ -201,6 +210,25 @@ export interface RememberOutcome {
|
|
|
201
210
|
*/
|
|
202
211
|
similar: Array<Similar>
|
|
203
212
|
}
|
|
213
|
+
/**
|
|
214
|
+
* Result of `rememberGuarded`. Blocked results have no `outcome` because no
|
|
215
|
+
* fact id was allocated.
|
|
216
|
+
*/
|
|
217
|
+
export interface GuardedRememberOutcome {
|
|
218
|
+
status: 'stored' | 'blocked'
|
|
219
|
+
outcome?: RememberOutcome
|
|
220
|
+
similar: Array<Similar>
|
|
221
|
+
/**
|
|
222
|
+
* Whether the similarity detector had anything to compare against.
|
|
223
|
+
*
|
|
224
|
+
* `false` means the fact was stored WITHOUT a duplicate check: the
|
|
225
|
+
* detector is scoped to the fact's entity, so a call carrying no
|
|
226
|
+
* `entity` has no candidate set and cannot block anything, now or after
|
|
227
|
+
* any number of later writes. Always `true` on a blocked result, which
|
|
228
|
+
* by definition compared something.
|
|
229
|
+
*/
|
|
230
|
+
checked: boolean
|
|
231
|
+
}
|
|
204
232
|
/** One recalled fact. */
|
|
205
233
|
export interface RecalledFact {
|
|
206
234
|
/** The fact id. */
|
|
@@ -373,6 +401,31 @@ export interface ExportPage {
|
|
|
373
401
|
/** Pass this opaque cursor to the next call; absent when the scan is done. */
|
|
374
402
|
nextCursor?: number
|
|
375
403
|
}
|
|
404
|
+
/** One active tag and the number of current facts carrying it. */
|
|
405
|
+
export interface TagSummary {
|
|
406
|
+
name: string
|
|
407
|
+
count: number
|
|
408
|
+
}
|
|
409
|
+
/** One bounded, stable page of current tags. */
|
|
410
|
+
export interface TagPage {
|
|
411
|
+
items: Array<TagSummary>
|
|
412
|
+
nextCursor?: string
|
|
413
|
+
}
|
|
414
|
+
/** Result of removing one tag from all current facts. */
|
|
415
|
+
export interface RemoveTagReport {
|
|
416
|
+
affected: number
|
|
417
|
+
}
|
|
418
|
+
/** The report of an explicit complete vector-axis replacement. */
|
|
419
|
+
export interface ReembedReport {
|
|
420
|
+
previousSpace?: string
|
|
421
|
+
newSpace: string
|
|
422
|
+
previousDim: number
|
|
423
|
+
newDim: number
|
|
424
|
+
embedded: number
|
|
425
|
+
tombstonesSkipped: number
|
|
426
|
+
vectorBytes: number
|
|
427
|
+
hnswIndexed: number
|
|
428
|
+
}
|
|
376
429
|
/** The report of a `maintain` pass. */
|
|
377
430
|
export interface MaintainReport {
|
|
378
431
|
/** Tombstoned facts physically removed by this pass. */
|
|
@@ -448,7 +501,7 @@ export const enum MaintainMode {
|
|
|
448
501
|
* round-trip it would be the wrong direction of dependency.
|
|
449
502
|
*/
|
|
450
503
|
export interface DbEntry {
|
|
451
|
-
/** The memory's name — its identity, and what `Workspace.
|
|
504
|
+
/** The memory's name — its identity, and what `Workspace.memory` takes. */
|
|
452
505
|
db: string
|
|
453
506
|
/** What it is for. */
|
|
454
507
|
description: string
|
|
@@ -636,6 +689,14 @@ export declare class Plugmem {
|
|
|
636
689
|
* @throws synchronously in read-only mode.
|
|
637
690
|
*/
|
|
638
691
|
remember(args: RememberArgs): Promise<RememberOutcome>
|
|
692
|
+
/**
|
|
693
|
+
* Stores only when the same bounded Jaccard/cosine detector used by
|
|
694
|
+
* `remember().similar` finds no candidate. No other write can slip between
|
|
695
|
+
* the check and possible write; a blocked result performs no mutation.
|
|
696
|
+
* Runs on a libuv worker because
|
|
697
|
+
* automatic embedding and durable writes are blocking work.
|
|
698
|
+
*/
|
|
699
|
+
rememberGuarded(args: RememberArgs): Promise<GuardedRememberOutcome>
|
|
639
700
|
/**
|
|
640
701
|
* Stores a batch of facts and resolves with one outcome per input.
|
|
641
702
|
*
|
|
@@ -755,6 +816,17 @@ export declare class Plugmem {
|
|
|
755
816
|
scrub(options?: ScrubOptions | undefined | null): Promise<Scrub>
|
|
756
817
|
/** One fact's tags, or an empty array for an unknown or tombstoned id. */
|
|
757
818
|
tagsOf(id: number): Array<string>
|
|
819
|
+
/**
|
|
820
|
+
* One bounded page of current tags. Runs on a libuv worker so neither a
|
|
821
|
+
* mapped-page fault nor host locking can pause the JavaScript event loop.
|
|
822
|
+
*/
|
|
823
|
+
listTags(options?: TagListOptions | undefined | null): Promise<TagPage>
|
|
824
|
+
/**
|
|
825
|
+
* Removes a tag from every current fact by creating successor revisions.
|
|
826
|
+
* The bulk write and journal sync run on a libuv worker.
|
|
827
|
+
* @throws synchronously in read-only mode.
|
|
828
|
+
*/
|
|
829
|
+
removeTag(tag: string): Promise<RemoveTagReport>
|
|
758
830
|
/**
|
|
759
831
|
* Content-integrity check; rejects on the first inconsistency found.
|
|
760
832
|
*
|
|
@@ -774,6 +846,16 @@ export declare class Plugmem {
|
|
|
774
846
|
* the only mode that reclaims edge-history page slack.
|
|
775
847
|
*/
|
|
776
848
|
maintain(mode?: 'auto' | 'compact' | 'reindex-text' | 'optimize-vectors' | 'full'): Promise<MaintainReport>
|
|
849
|
+
/**
|
|
850
|
+
* Explicitly recomputes every retained fact with the configured embedder
|
|
851
|
+
* and atomically replaces the complete vector axis. This is never invoked
|
|
852
|
+
* by `maintain('auto')`.
|
|
853
|
+
*
|
|
854
|
+
* **Async**: provider and snapshot work runs on a libuv worker; the event
|
|
855
|
+
* loop remains responsive. Reads continue while it runs and writes reject
|
|
856
|
+
* with `PLUGMEM_BUSY` instead of waiting.
|
|
857
|
+
*/
|
|
858
|
+
reembed(batchSize?: number | undefined | null): Promise<ReembedReport>
|
|
777
859
|
/**
|
|
778
860
|
* Flushes the journal into a fresh snapshot. **Async** (returns a `Promise`):
|
|
779
861
|
* it writes and fsyncs a snapshot file, so it runs on a libuv worker thread.
|
|
@@ -838,9 +920,38 @@ export declare class Scrub {
|
|
|
838
920
|
active(): boolean
|
|
839
921
|
}
|
|
840
922
|
/**
|
|
841
|
-
* A
|
|
842
|
-
*
|
|
923
|
+
* A logical reference to one named memory.
|
|
924
|
+
*
|
|
925
|
+
* It owns no open database and no file lock. Each verb obtains a scoped
|
|
926
|
+
* workspace lease on a libuv worker, and dropping this JavaScript object has
|
|
927
|
+
* no resource-management meaning. The workspace is the owner; closing it
|
|
928
|
+
* invalidates every reference.
|
|
843
929
|
*/
|
|
930
|
+
export declare class WorkspaceMemory {
|
|
931
|
+
/** The stable workspace name this reference addresses. */
|
|
932
|
+
name(): string
|
|
933
|
+
remember(args: RememberArgs): Promise<RememberOutcome>
|
|
934
|
+
rememberGuarded(args: RememberArgs): Promise<GuardedRememberOutcome>
|
|
935
|
+
rememberMany(args: Array<RememberArgs>): Promise<RememberOutcome[]>
|
|
936
|
+
revise(id: number, args: RememberArgs): Promise<RememberOutcome>
|
|
937
|
+
recall(args?: RecallArgs | undefined | null): Promise<RecallResult>
|
|
938
|
+
forget(id: number): Promise<boolean>
|
|
939
|
+
link(args: LinkArgs): Promise<void>
|
|
940
|
+
unlink(args: LinkArgs): Promise<boolean>
|
|
941
|
+
get(id: number): Promise<FactSnapshot | null>
|
|
942
|
+
stats(): Promise<Stats>
|
|
943
|
+
export(): Promise<ExportedFact[]>
|
|
944
|
+
exportPage(cursor?: number | undefined | null): Promise<ExportPage>
|
|
945
|
+
exportEdges(onBatch: (edges: ExportedEdge[]) => void): Promise<number>
|
|
946
|
+
scrub(options?: ScrubOptions | undefined | null): Promise<Scrub>
|
|
947
|
+
tagsOf(id: number): Promise<string[]>
|
|
948
|
+
listTags(options?: TagListOptions | undefined | null): Promise<TagPage>
|
|
949
|
+
removeTag(tag: string): Promise<RemoveTagReport>
|
|
950
|
+
verify(): Promise<void>
|
|
951
|
+
maintain(mode?: 'auto' | 'compact' | 'reindex-text' | 'optimize-vectors' | 'full'): Promise<MaintainReport>
|
|
952
|
+
reembed(batchSize?: number | undefined | null): Promise<ReembedReport>
|
|
953
|
+
checkpoint(): Promise<void>
|
|
954
|
+
}
|
|
844
955
|
export declare class Workspace {
|
|
845
956
|
/**
|
|
846
957
|
* Opens the workspace rooted at `root`. Creates nothing: the directories
|
|
@@ -850,22 +961,22 @@ export declare class Workspace {
|
|
|
850
961
|
*/
|
|
851
962
|
constructor(root: string, options?: WorkspaceOptions | undefined | null)
|
|
852
963
|
/**
|
|
853
|
-
*
|
|
854
|
-
* class, and the same verbs, as a memory opened by path.
|
|
964
|
+
* Returns a logical reference to the memory named `db`.
|
|
855
965
|
*
|
|
856
|
-
*
|
|
857
|
-
*
|
|
858
|
-
*
|
|
859
|
-
*
|
|
860
|
-
|
|
966
|
+
* This does not open a file, acquire a lock or create the memory. Read
|
|
967
|
+
* verbs require it to exist; write verbs create it on first use. The
|
|
968
|
+
* returned object stays valid across pool eviction and `release()` and
|
|
969
|
+
* transparently reopens the memory on its next verb.
|
|
970
|
+
*/
|
|
971
|
+
memory(db: string): WorkspaceMemory
|
|
972
|
+
/**
|
|
973
|
+
* Evicts one inactive memory from the pool and releases its file lock.
|
|
861
974
|
*
|
|
862
|
-
*
|
|
863
|
-
*
|
|
864
|
-
*
|
|
865
|
-
* snapshot, and making room in the pool closes another memory — file work
|
|
866
|
-
* that the one thread running JavaScript must not be holding.
|
|
975
|
+
* Logical references remain valid. A later verb reopens the memory. If a
|
|
976
|
+
* verb is currently using it, this throws a typed `BUSY` error instead of
|
|
977
|
+
* waiting.
|
|
867
978
|
*/
|
|
868
|
-
|
|
979
|
+
release(db: string): boolean
|
|
869
980
|
/**
|
|
870
981
|
* Every memory in the directory, sorted by name.
|
|
871
982
|
*
|
|
@@ -934,12 +1045,10 @@ export declare class Workspace {
|
|
|
934
1045
|
/** How many memories are open right now. */
|
|
935
1046
|
openCount(): number
|
|
936
1047
|
/**
|
|
937
|
-
*
|
|
938
|
-
* and closes the workspace.
|
|
939
|
-
*
|
|
940
|
-
*
|
|
941
|
-
* own handle and holds its own lock until it is closed or garbage
|
|
942
|
-
* collected.
|
|
1048
|
+
* Invalidates every [`WorkspaceMemory`] reference, closes inactive pooled
|
|
1049
|
+
* memories and the registry, and closes the workspace. A verb already in
|
|
1050
|
+
* flight finishes with its scoped handle; its lock is released when that
|
|
1051
|
+
* verb returns. Every later call throws `CLOSED`.
|
|
943
1052
|
*/
|
|
944
1053
|
close(): void
|
|
945
1054
|
}
|
package/index.js
CHANGED
|
@@ -310,12 +310,13 @@ if (!nativeBinding) {
|
|
|
310
310
|
throw new Error(`Failed to load native binding`)
|
|
311
311
|
}
|
|
312
312
|
|
|
313
|
-
const { Plugmem, recover, Scrub, MaintainMode, Workspace, version, about, settingsHelp, skill, skillFull, skillVersion } = nativeBinding
|
|
313
|
+
const { Plugmem, recover, Scrub, MaintainMode, WorkspaceMemory, Workspace, version, about, settingsHelp, skill, skillFull, skillVersion } = nativeBinding
|
|
314
314
|
|
|
315
315
|
module.exports.Plugmem = Plugmem
|
|
316
316
|
module.exports.recover = recover
|
|
317
317
|
module.exports.Scrub = Scrub
|
|
318
318
|
module.exports.MaintainMode = MaintainMode
|
|
319
|
+
module.exports.WorkspaceMemory = WorkspaceMemory
|
|
319
320
|
module.exports.Workspace = Workspace
|
|
320
321
|
module.exports.version = version
|
|
321
322
|
module.exports.about = about
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "plugmem",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
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",
|
|
@@ -43,11 +43,11 @@
|
|
|
43
43
|
"typecheck": "tsc -p tsconfig.json"
|
|
44
44
|
},
|
|
45
45
|
"optionalDependencies": {
|
|
46
|
-
"plugmem-linux-x64-gnu": "0.
|
|
47
|
-
"plugmem-linux-arm64-gnu": "0.
|
|
48
|
-
"plugmem-darwin-x64": "0.
|
|
49
|
-
"plugmem-darwin-arm64": "0.
|
|
50
|
-
"plugmem-win32-x64-msvc": "0.
|
|
51
|
-
"plugmem-win32-arm64-msvc": "0.
|
|
46
|
+
"plugmem-linux-x64-gnu": "0.10.0",
|
|
47
|
+
"plugmem-linux-arm64-gnu": "0.10.0",
|
|
48
|
+
"plugmem-darwin-x64": "0.10.0",
|
|
49
|
+
"plugmem-darwin-arm64": "0.10.0",
|
|
50
|
+
"plugmem-win32-x64-msvc": "0.10.0",
|
|
51
|
+
"plugmem-win32-arm64-msvc": "0.10.0"
|
|
52
52
|
}
|
|
53
53
|
}
|