plugmem 0.1.4 → 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.
Files changed (2) hide show
  1. package/README.md +114 -20
  2. 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
@@ -78,15 +79,21 @@ db.close(); // release the file + lock explicitly
78
79
  ## Configuration & embeddings
79
80
 
80
81
  The constructor resolves settings **exactly like the CLI and MCP server**: an
81
- explicit `config` path wins, else `$PLUGMEM_CONFIG`, else
82
- `$XDG_CONFIG_HOME/plugmem/config.toml`, else all defaults.
82
+ explicit `config` path wins, else `$PLUGMEM_CONFIG`, else the platform config
83
+ directory, else all defaults. The database path is resolved as an explicit
84
+ constructor path, then `$PLUGMEM_DB`, then `[database].path`, then the platform
85
+ data directory. See the [full settings reference](https://github.com/m62624/plugmem/blob/main/crates/plugmem-host/SETTINGS.md)
86
+ for all fields and OS-specific paths.
83
87
 
84
88
  ```typescript
85
- const db = new Plugmem("agent.plugmem", { config: "./plugmem.toml" });
89
+ const db = new Plugmem(undefined, { config: "./plugmem.toml" });
86
90
  ```
87
91
 
88
92
  ```toml
89
93
  # plugmem.toml
94
+ [database]
95
+ path = "/path/to/memory.plugmem" # optional example
96
+
90
97
  [engine]
91
98
  dim = 768 # embedding size (0 = vectors off)
92
99
 
@@ -105,27 +112,114 @@ must agree. A `{ readOnly: true }` handle never auto-embeds — pass a vector.
105
112
 
106
113
  ## The verbs
107
114
 
108
- Method names mirror `plugmem-host`'s `Database` one-to-one.
115
+ Every method here is the identically-named `plugmem-host` `Database` verb; the
116
+ engine logic is entirely the host's.
117
+
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.
125
+
126
+ ### What the host has and this does not
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:
130
+
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
+ ```
109
204
 
110
- **Writer** (default): `remember`, `recall`, `revise(id, args)`, `forget(id)`,
111
- `link`, `get(id)`, `stats`, `export`, `verify`, and the two **async** verbs
112
- below. **Read-only** (`{ readOnly: true }`, observing another process's writer):
113
- `recall`, `get`, `stats`, `export`, `verify`, plus `generation()` (the pinned
114
- snapshot generation) and `refresh()` (advance to the writer's latest checkpoint);
115
- the write verbs throw.
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.
116
209
 
117
- **No `import` verb** bulk-loading a `backup.jsonl` reads a file on disk, which
118
- is [`plugmem-cli import`](https://docs.rs/plugmem-cli/latest)'s job. An agent
119
- remembers facts one at a time as the conversation goes.
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.
120
213
 
121
- ## Async and concurrency (no tokio)
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.
122
218
 
123
- `maintain()` and `checkpoint()` do real disk I/O (compaction, HNSW build,
124
- fsync), so they return a **`Promise`** and run on the **libuv** thread pool —
125
- they never block the event loop. Every other verb is microsecond-fast in memory
126
- and stays synchronous (a Promise there would be pure overhead). There is no async
127
- runtime: the engine is CPU-bound, and the one thing that can wait — a remote
128
- embedder's HTTP call — happens outside the engine lock.
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.
129
223
 
130
224
  `close()` releases the file and its lock; every verb afterwards throws, and it is
131
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.1.4",
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.1.4",
43
- "plugmem-linux-arm64-gnu": "0.1.4",
44
- "plugmem-darwin-x64": "0.1.4",
45
- "plugmem-darwin-arm64": "0.1.4",
46
- "plugmem-win32-x64-msvc": "0.1.4",
47
- "plugmem-win32-arm64-msvc": "0.1.4"
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
  }