plugmem 0.2.0 → 0.3.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 +105 -17
- package/package.json +11 -9
package/README.md
CHANGED
|
@@ -70,6 +70,7 @@ card.metadata; // Record<string,string> — keys sorted, {} when
|
|
|
70
70
|
|
|
71
71
|
db.revise(out.id, { text: "prefers async-std" });
|
|
72
72
|
db.link({ src: "user", rel: "works_at", dst: "acme" });
|
|
73
|
+
db.unlink({ src: "user", rel: "works_at", dst: "acme" }); // closes the current edge
|
|
73
74
|
|
|
74
75
|
await db.checkpoint(); // async (see below)
|
|
75
76
|
db.close(); // release the file + lock explicitly
|
|
@@ -111,27 +112,114 @@ must agree. A `{ readOnly: true }` handle never auto-embeds — pass a vector.
|
|
|
111
112
|
|
|
112
113
|
## The verbs
|
|
113
114
|
|
|
114
|
-
|
|
115
|
+
Every method here is the identically-named `plugmem-host` `Database` verb; the
|
|
116
|
+
engine logic is entirely the host's.
|
|
115
117
|
|
|
116
|
-
**Writer** (default): `remember`, `recall`, `revise(id, args)`,
|
|
117
|
-
`link`, `get(id)`, `
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
the
|
|
118
|
+
**Writer** (default): `remember`, `rememberMany`, `recall`, `revise(id, args)`,
|
|
119
|
+
`forget(id)`, `link`, `unlink`, `get(id)`, `tagsOf(id)`, `stats`, `export`,
|
|
120
|
+
`exportPage(cursor?)`, `verify`, and the async maintenance verbs below.
|
|
121
|
+
**Read-only** (`{ readOnly: true }`, observing another process's writer):
|
|
122
|
+
`recall`, `get`, `tagsOf`, `stats`, `export`, `exportPage(cursor?)`, `verify`,
|
|
123
|
+
plus `generation()` (the pinned snapshot generation) and `refresh()` (advance
|
|
124
|
+
to the writer's latest checkpoint); the write verbs throw.
|
|
122
125
|
|
|
123
|
-
|
|
124
|
-
is [`plugmem-cli import`](https://docs.rs/plugmem-cli/latest)'s job. An agent
|
|
125
|
-
remembers facts one at a time as the conversation goes.
|
|
126
|
+
### What the host has and this does not
|
|
126
127
|
|
|
127
|
-
|
|
128
|
+
The list above is the whole supported `Plugmem` surface. The only host
|
|
129
|
+
operation intentionally kept out of this boundary is path-level recovery:
|
|
128
130
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
131
|
+
| host verb | boundary note |
|
|
132
|
+
|---|---|
|
|
133
|
+
| `recover` | salvaging a damaged file is a path-level operation on the disk the process is running on — [`plugmem-cli recover`](https://docs.rs/plugmem-cli/latest)'s job, like `import` and `scrub`. |
|
|
134
|
+
| `remember_many` | Exposed as async `rememberMany(items)`. It writes a batch with one embedding round-trip and resolves with outcomes in input order. |
|
|
135
|
+
| `export_each` | Exposed as pull-based `exportPage(cursor?)`. Each Promise returns at most 128 facts and releases the native read lock before JS processes them; this gives Node backpressure without a cross-thread callback. |
|
|
136
|
+
| `tags_of` | Exposed as synchronous `tagsOf(id)`, returning one fact's tags or an empty array. |
|
|
137
|
+
|
|
138
|
+
**No `import` verb** either — bulk-loading a `backup.jsonl` reads a file on disk,
|
|
139
|
+
which is the CLI's job. A Node host can use `rememberMany` for bounded batches
|
|
140
|
+
when it already owns the input records.
|
|
141
|
+
|
|
142
|
+
## Many memories in one directory (optional)
|
|
143
|
+
|
|
144
|
+
**Default: one memory, one file.** `new Plugmem(path)` and nothing here applies.
|
|
145
|
+
|
|
146
|
+
A process that serves many independent memories — one per conversation, per
|
|
147
|
+
tenant, per project — can point at a directory and address them by name:
|
|
148
|
+
|
|
149
|
+
```ts
|
|
150
|
+
import { Workspace, type DbEntry } from "plugmem";
|
|
151
|
+
|
|
152
|
+
const ws = new Workspace("/srv/memories", { maxOpen: 16, idleTimeoutMs: 60_000 });
|
|
153
|
+
|
|
154
|
+
// `open` hands back the same `Plugmem` class, so a named memory has exactly the
|
|
155
|
+
// verbs a path-opened one has. A first write to an unused name creates it.
|
|
156
|
+
ws.open("chat-42").remember({ text: "prefers tokio" });
|
|
157
|
+
|
|
158
|
+
// Do not know the name? Ask what each memory is for. Owners are searchable too,
|
|
159
|
+
// even though an owner is a graph edge rather than text.
|
|
160
|
+
ws.describe("chat-42", { description: "release planning", owner: "ann" });
|
|
161
|
+
const hits: DbEntry[] = ws.find("release planning"); // → [{ db: "chat-42", … }]
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
A name is `[a-z0-9][a-z0-9_-]*` and **cannot represent a path**, so it resolves
|
|
165
|
+
to exactly one file inside the directory — traversal is not filtered out, it is
|
|
166
|
+
unconstructible. `open(name, false)` refuses a name that does not exist yet,
|
|
167
|
+
which is what a read should do so a typo is diagnosed rather than answered with
|
|
168
|
+
an empty result.
|
|
169
|
+
|
|
170
|
+
The pool bounds how many stay open; `closeIdle()` releases the rest. Call it on
|
|
171
|
+
a timer: an open memory holds the file's **exclusive lock**, so a long-running
|
|
172
|
+
process that never let go would make its memories unreachable from anything else
|
|
173
|
+
on the machine. That is what the idle timeout is for — liveness, not memory.
|
|
174
|
+
|
|
175
|
+
Two things to know before building on it. Memories are **independent**: nothing
|
|
176
|
+
searches across them and no entity links between them, so a fact filed in the
|
|
177
|
+
wrong one is unreachable from the other rather than merely misplaced. And **who
|
|
178
|
+
may reach which memory is not this package's responsibility** — the name comes
|
|
179
|
+
from your code, so put the policy there.
|
|
180
|
+
|
|
181
|
+
`reindex()` and `verify()` return promises: they open and read every memory in
|
|
182
|
+
the directory, which is not work for the main thread.
|
|
183
|
+
|
|
184
|
+
## Async and concurrency
|
|
185
|
+
|
|
186
|
+
Operations with unbounded storage or batch work use napi-rs `AsyncTask`: they
|
|
187
|
+
return a **`Promise`** and run on Node's **libuv** worker pool, keeping the event
|
|
188
|
+
loop available for application code. This includes `rememberMany`,
|
|
189
|
+
`exportPage`, `maintain`, `checkpoint`, `reindex` and `verify`.
|
|
190
|
+
|
|
191
|
+
For a bounded export, call `exportPage()` once, process its `facts`, then pass
|
|
192
|
+
`nextCursor` to the next call until it is absent:
|
|
193
|
+
|
|
194
|
+
```ts
|
|
195
|
+
let cursor: number | undefined;
|
|
196
|
+
do {
|
|
197
|
+
const page = await db.exportPage(cursor);
|
|
198
|
+
for (const fact of page.facts) {
|
|
199
|
+
await destination.write(fact);
|
|
200
|
+
}
|
|
201
|
+
cursor = page.nextCursor;
|
|
202
|
+
} while (cursor !== undefined);
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
The Promise is the completion boundary for that page: no callback remains
|
|
206
|
+
queued, and no database lock is held while the loop body runs. Each page has at
|
|
207
|
+
most 128 facts. A read-only handle pages one immutable checkpoint; when paging a
|
|
208
|
+
writer for a snapshot-style backup, do not mutate it between calls.
|
|
209
|
+
|
|
210
|
+
`rememberMany(items)` performs one batch embedding pass and one journal sync,
|
|
211
|
+
then resolves with outcomes in input order. A maintenance call may also return
|
|
212
|
+
a no-op report when there is nothing to purge, reindex or optimize.
|
|
213
|
+
|
|
214
|
+
`maintain(mode?)` takes `"auto"` (the default), `"compact"`, `"reindex-text"`,
|
|
215
|
+
`"optimize-vectors"` or `"full"`. No mode ever drops a fact revision or an edge
|
|
216
|
+
version; the heavier ones buy bytes and index freshness. `"full"` is the only
|
|
217
|
+
one that repacks the edge arenas, which a relink-heavy workload fragments.
|
|
218
|
+
|
|
219
|
+
Small single-record and read verbs stay synchronous for a direct API. A single
|
|
220
|
+
write can still wait for the configured durability policy, and a configured
|
|
221
|
+
embedder can perform a remote request; use `rememberMany` when that work should
|
|
222
|
+
run away from the event loop.
|
|
135
223
|
|
|
136
224
|
`close()` releases the file and its lock; every verb afterwards throws, and it is
|
|
137
225
|
idempotent (the handle is also released on garbage collection, but `close()`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "plugmem",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Native Node.js addon for plugmem: an embedded long-term memory engine for LLM agents (remember / recall / revise / forget over one local file).",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -31,19 +31,21 @@
|
|
|
31
31
|
"node": ">= 16"
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {
|
|
34
|
-
"@napi-rs/cli": "^2.18.4"
|
|
34
|
+
"@napi-rs/cli": "^2.18.4",
|
|
35
|
+
"typescript": "^5.9.3"
|
|
35
36
|
},
|
|
36
37
|
"scripts": {
|
|
37
38
|
"build": "napi build --platform --release",
|
|
38
39
|
"build:debug": "napi build --platform",
|
|
39
|
-
"test": "node --test __test__/*.test.mjs"
|
|
40
|
+
"test": "node --test __test__/*.test.mjs",
|
|
41
|
+
"typecheck": "tsc -p tsconfig.json"
|
|
40
42
|
},
|
|
41
43
|
"optionalDependencies": {
|
|
42
|
-
"plugmem-linux-x64-gnu": "0.
|
|
43
|
-
"plugmem-linux-arm64-gnu": "0.
|
|
44
|
-
"plugmem-darwin-x64": "0.
|
|
45
|
-
"plugmem-darwin-arm64": "0.
|
|
46
|
-
"plugmem-win32-x64-msvc": "0.
|
|
47
|
-
"plugmem-win32-arm64-msvc": "0.
|
|
44
|
+
"plugmem-linux-x64-gnu": "0.3.0",
|
|
45
|
+
"plugmem-linux-arm64-gnu": "0.3.0",
|
|
46
|
+
"plugmem-darwin-x64": "0.3.0",
|
|
47
|
+
"plugmem-darwin-arm64": "0.3.0",
|
|
48
|
+
"plugmem-win32-x64-msvc": "0.3.0",
|
|
49
|
+
"plugmem-win32-arm64-msvc": "0.3.0"
|
|
48
50
|
}
|
|
49
51
|
}
|