wontopos 2.2.45 → 2.2.47

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 CHANGED
@@ -24,12 +24,47 @@ const ctx = await mem.recall("what does alice drink?", "alice");
24
24
  ## Why
25
25
 
26
26
  - **The same in every language** — identical recall whichever language a memory was written in (Korean · Japanese · Chinese · English).
27
- - **No LLM in the loop** — `store` / `search` / `recall` never call a language model. You pay retrieval, not generation.
28
- - **Bounded retrieval** — `recall()` returns a small, fixed-size slice regardless of how much you've stored (~1,000 tokens on `tablet-2`, the default engine).
27
+ - **No LLM reads your memories** — no language model reads your stored memories, on any model. You pay retrieval, not generation.
28
+ - **Bounded retrieval** — `recall()` returns the recent turns, at most `limit` long-term memories (default 10, up to 20) and at most `context_limit` rows of the surrounding conversation (default 10, up to 20), however much you've stored.
29
29
 
30
30
  ## Methods
31
31
 
32
- `add` · `addTurn` · `addBulk` · `update` · `search` · `searchFull` · `recall` · `history` · `stats` · `get` · `listMemories` · `delete` · `deleteAll`
32
+ | Method | What it does |
33
+ |---|---|
34
+ | **Write** | |
35
+ | `add(content, userId?, metadata?, opts?)` | Store one memory. Alias `store`. `opts.image` attaches a photo. |
36
+ | `addTurn(userMsg, assistantMsg, userId?)` | Store one exchange of a conversation. |
37
+ | `addBulk(content, userId?, category?, timestamp?)` | Backfill a long history in one call. |
38
+ | `update(oldMemoryId, newContent, userId?)` | Replace a fact that changed. The old version stays in its history. |
39
+ | **Read** | |
40
+ | `recall(query, userId?, opts?)` | The recent turns, the memories that match and the conversation around the best one, in one call. |
41
+ | `search(query, userId?, limit?, opts?)` | The memories that match, best first. |
42
+ | `searchFull(...)` | The same search with each part kept apart, `images` and `verify_used` included. |
43
+ | `searchSelf(...)` | The assistant's own words, kept apart from what others said. |
44
+ | `history(userId?)` | The recent conversation. |
45
+ | `get(userId, memoryId)` | One memory by id. |
46
+ | `listMemories` · `iterMemories` · `exportMemories` | Every memory in a store: a page at a time, as a stream, or all at once. |
47
+ | `lineage(userId, memoryId)` | The edits that followed one memory. |
48
+ | `revisions(userId?, opts?)` | How many of a store's memories were edited, and which. |
49
+ | `stats(userId?)` | Counts for a store. |
50
+ | `engram(name, query, userId?)` · `listEngrams()` | Built-in retrieval routines, and the ones a model can run. |
51
+ | **Photos** | |
52
+ | `listImages` · `iterImages` · `exportImages` | A store's image memories, newest first. |
53
+ | `getImage(userId, memoryId)` | The bytes of one image. |
54
+ | `forgetImage(userId, memoryId, { preview })` | Remove the photo from a memory. `preview: true` first says whether the memory itself stays. |
55
+ | **People** | |
56
+ | `addSpeaker` · `listSpeakers` · `removeSpeaker` | Register, list and unregister the people a store hears from. |
57
+ | `bySpeaker(speaker, userId?)` | What one person said, newest first. |
58
+ | **Stores** | |
59
+ | `createStore` · `listStores` · `deleteStore` | One isolated memory space per end-user, agent or topic. |
60
+ | `delete(userId, memoryId)` · `deleteAll(userId)` | Delete one memory, or every memory in a store. |
61
+ | **Account** | |
62
+ | `listModels()` | The models you can use, and what each can do. |
63
+ | `usage(days?)` | What this key, its workspace and its stores have spent. Free. |
64
+ | `ping()` | Check the key and the connection. |
65
+ | **Client** | |
66
+ | `withModel` · `withUser` · `withDeadline` · `withTimeout` · `withRetries` · `withSignal` | A copy of the client with another model, store, budget, timeout, retry count or cancel signal. |
67
+ | `Client.fromEnv()` · `rateLimit` | A client with the key from the environment; the last call's rate-limit numbers. |
33
68
 
34
69
  Where the store id goes depends on whether the method takes a payload. A method that
35
70
  writes or searches puts the payload first and the store second — `mem.add("...", "alice")`,
@@ -38,9 +73,32 @@ writes or searches puts the payload first and the store second — `mem.add("...
38
73
  Most of them let you leave it out for the client's default store. Five take it
39
74
  positionally and cannot — `get`, `getImage`, `delete`, `lineage` and `forgetImage` want
40
75
  an explicit `undefined` there, because a second required argument follows it. Python
41
- omits it in all five; this is the one place the three clients differ in shape rather than meaning.
76
+ omits it in all five, and Rust takes it positionally (`None` for the default store, where there is one).
42
77
  Let your editor confirm the order: every signature is typed. A store is one isolated memory space per end-user, agent, or topic. WHO said each memory inside a store is the `speaker` tag below — storing the assistant's own words never needs a separate id.
43
78
 
79
+ ## Putting recall into a prompt
80
+
81
+ ```ts
82
+ const ctx = await mem.recall(userMessage, "alice");
83
+
84
+ const system = [
85
+ "What you remember about this user, most relevant first:",
86
+ ...ctx.long_term.memories.map((m) => `- ${m.content}`),
87
+ "",
88
+ "Around the most relevant memory:",
89
+ ...ctx.context.around_top_memory,
90
+ "",
91
+ "The conversation so far:",
92
+ ...ctx.short_term.turns.map((t) => `${t.role}: ${t.content}`),
93
+ ].join("\n");
94
+
95
+ // Call your language model with `system` and the user's message, then store the exchange:
96
+ await mem.addTurn(userMessage, reply, "alice");
97
+ ```
98
+
99
+ Each row of `around_top_memory` starts with `[before] `, `[match] ` or `[after] `: where it
100
+ sits next to the best memory. Keep every list in the order it arrives.
101
+
44
102
  ## Who said it (speakers)
45
103
 
46
104
  Every memory can carry a speaker: `"me"` for the assistant's own words, or a
@@ -56,19 +114,56 @@ await mem.search("what did Bob say about deadlines?", "alice", 10, { speaker: "B
56
114
  ```
57
115
 
58
116
  `listSpeakers()` shows the registered people with per-person memory counts;
59
- `removeSpeaker()` unregisters (memories stay, the tag goes). A store registers
117
+ `removeSpeaker()` unregisters (memories stay; the tag goes, except on `tablet-2`, which keeps it on memories already stored). A store registers
60
118
  up to 50 people to start (a limit we plan to raise); `"me"` never needs
61
119
  registration and never counts against it.
62
120
 
121
+ ## Photos
122
+
123
+ On a model whose `listModels()` capabilities include `images`, a memory can carry a photo.
124
+ The caption is still required.
125
+
126
+ ```ts
127
+ import { readFileSync, writeFileSync } from "node:fs";
128
+
129
+ const data = readFileSync("beach.jpg").toString("base64");
130
+ const stored = await mem.add("At the beach with Bob", "alice", {}, { image: { data, taken_at: "2026-08-14" } });
131
+
132
+ const { bytes, contentType } = await mem.getImage("alice", stored.id!);
133
+ writeFileSync(`beach.${contentType.split("/")[1]}`, bytes);
134
+ ```
135
+
136
+ Both edges must be 700px or more, the long edge at most 2500px and the file at most 7MB;
137
+ anything else is refused (400) with the reason. A long edge over 1568px is stored at
138
+ 1568px, so keep your own copy of the original. `search` returns image memories after
139
+ the others, and `listImages` pages through them newest first. `forgetImage` removes the
140
+ photo, and it can remove the whole memory with it: call it with `preview: true` first,
141
+ and `memory_kept` says whether the memory stays.
142
+
143
+ ## Export, edit history and usage
144
+
145
+ - **Export:** `exportMemories(userId)` returns every memory in a store, the text you
146
+ stored and its metadata; `exportImages(userId)` every image memory. For a large store,
147
+ `iterMemories` and `iterImages` stream them a page at a time.
148
+ - **Edit history:** `update` replaces a fact that changed. `lineage(userId, memoryId)` lists
149
+ the versions from the id you pass forward, oldest first, and marks the one in force with
150
+ `is_current`; pass the first version's id to see the whole history. `revisions()` counts
151
+ how many of a store's memories were edited.
152
+ - **Usage:** `usage(days)` reports this key's spend, the workspace's spend over the window,
153
+ the stores that spent the most, and the balance. It is free, and works at a zero balance.
154
+
63
155
  ## Recall caching
64
156
 
65
- Opt in per search and repeated or extended queries reuse the previous result
66
- at 10% of the normal rate (Tablet and Scroll models).
157
+ Opt in per search and a repeated query reuses the previous result at 10% of the
158
+ normal rate; an extended one reuses part of it (Tablet and Scroll models).
67
159
 
68
160
  It is not free to turn on: the FIRST call writes the cache and bills the query
69
- tokens at 2x for a `5m` TTL, 3x for `1h`. Only hits inside the TTL bill at 0.1x.
70
- So it pays for a query you repeat or extend, and costs more for one you issue
71
- once — do not switch it on globally. Any write to the store invalidates its cache
161
+ tokens at 2x for a `5m` TTL, 3x for `1h`. The same query again inside the TTL bills
162
+ its query and its results at 0.1x. A query that extends the cached one (its text plus
163
+ more) bills the cached part at 0.1x, the new part at the write rate and its results
164
+ at the normal rate. A store keeps one cached query per model and set of options, so a
165
+ different query replaces it. So it pays for a query you repeat or extend, and costs
166
+ more for one you issue once — do not switch it on globally. Any write to the store invalidates its cache
72
167
  at once, so a hit can never predate a new memory.
73
168
 
74
169
  ```ts
@@ -82,12 +177,22 @@ Built in, no configuration needed:
82
177
 
83
178
  - **Automatic retries** — 429 and a 409 for a write already in flight always, and 408 /
84
179
  502 / 503 / 504 or a connection error only when a retry cannot double-process a write.
85
- The writes and the searches are POSTs, and any of those four may have been returned
86
- *after* the write already landed, so those get 429, that 409 and connect-level
87
- failures only. The GET and DELETE calls — `ping`, `listStores`, `listSpeakers`,
88
- `listModels`, `listEngrams`, `usage`, `deleteStore`, `removeSpeaker`, `forgetImage` —
89
- retry all four. Twice, with exponential backoff + jitter, honoring the server's
90
- `Retry-After` up to 30s. Tune with `new Client({ maxRetries })`; `0` disables.
180
+ The calls that are safe to send again retry all four: the reads sent as POST (`search`,
181
+ `recall`, `get`, `listMemories`, `stats`, ...), the GET and DELETE calls (`ping`,
182
+ `listStores`, `listSpeakers`, `listModels`, `listEngrams`, `usage`, `deleteStore`,
183
+ `removeSpeaker`, `forgetImage`), and the memory writes (`add`, `addTurn`, `addBulk`,
184
+ `update`). Those carry an idempotency key, so the service answers a repeat with the
185
+ first attempt's result instead of storing again. The other writes sent as POST
186
+ (`createStore`, `addSpeaker`, `delete`, `deleteAll`) get 429, that 409 and
187
+ connect-level failures only. Twice, with exponential backoff + jitter, honoring the
188
+ server's `Retry-After` up to 30s. Tune with `new Client({ maxRetries })`; `0` disables.
189
+ - **Your own retries** — pass `idempotencyKey` when the retry is yours (a job that is
190
+ re-run, a queue that redelivers). The service keeps a key for 24 hours and answers a
191
+ repeat with the first response, marked `replayed: true`. When the first attempt did
192
+ not come back with an answer, so whether it was applied is not known, a repeat
193
+ is refused with a `ConflictError` (409) rather than stored twice: check before sending
194
+ it again under a new key. Derive the key from what you store
195
+ (`` `import:${row.id}` ``), never a constant.
91
196
  - **Redirects refused** — the API key never follows a 3xx to another host.
92
197
  - **Timeouts** — 30s per attempt by default (`timeoutMs`), and a total budget for
93
198
  the whole call across every retry with `deadlineMs` / `withDeadline(ms)`. At the
@@ -128,8 +233,13 @@ timeouts, redirect refusal and the response cap all still apply.
128
233
 
129
234
  ## Security
130
235
 
131
- Built in, none of it configurable off:
236
+ Built in:
132
237
 
238
+ - **No key in a browser page** — anyone who opens a page can read a key in it, and with
239
+ it every memory in its workspace. A client in a browser page that would send the key to
240
+ the service is refused: call Wontopos from your server, or point `baseUrl` at a proxy of
241
+ your own that holds the key. `dangerouslyAllowBrowser: true` turns the refusal off, for
242
+ a local demo with a key you will revoke.
133
243
  - **Redirects refused** — a 3xx is an error, so the key never follows one to
134
244
  another host.
135
245
  - **Response size cap** — anything over 64MB is refused instead of buffered.
@@ -147,7 +257,8 @@ Built in, none of it configurable off:
147
257
  Every method returns a documented shape — `StoreResult`, `UpdateResult`,
148
258
  `RecallResult`, `EngramResult`, `StatsResult`, `SpeakersList`, `Memory[]`, ... —
149
259
  each with an index signature, so new server fields flow through without an SDK
150
- update. `search` options are typed too (`SearchOptions`: `cache_control`,
260
+ update. `searchSelf`'s result and `getImage`'s bytes are built by the client and
261
+ carry only their own fields. `search` options are typed too (`SearchOptions`: `cache_control`,
151
262
  `speaker`, plus `extra` for pass-through).
152
263
 
153
264
  ```ts
@@ -157,7 +268,8 @@ const hits: Memory[] = await mem.search("what did Bob say?", "alice", 10, { spea
157
268
  console.log(hits[0].speaker, hits[0].time_bucket); // typed: string | undefined
158
269
  ```
159
270
 
160
- `hits` arrives best-first — take it in the order given. `similarity` is a raw
271
+ `hits` holds memories best-first, then the assistant's own words and image memories in
272
+ their own order. Take it in the order given. `similarity` is a raw
161
273
  closeness score, not the ranking key: what produces the order is internal and is
162
274
  not returned, so sorting by `similarity` overrides the ranking and makes results
163
275
  worse. There is no `score` field.
@@ -183,7 +295,8 @@ try {
183
295
 
184
296
  A retired model answers `410`, thrown as `GoneError`: retrying cannot succeed, so name a
185
297
  live model (`listModels()` lists them, with `retires_at` on one that is scheduled to
186
- retire). `deleteStore` still works under a retired model.
298
+ retire). Before the day, the client warns once with the date the model you use retires.
299
+ `deleteStore` still works under a retired model.
187
300
 
188
301
  ## A different API host
189
302
 
@@ -214,7 +327,7 @@ data to fix it.
214
327
  ## Changelog
215
328
 
216
329
  The three clients release in lockstep — same version, same surface, same day. Patch
217
- releases are additive. Nine inside 2.2 were not, deliberately and each with its
330
+ releases are additive. Ten inside 2.2 were not, deliberately and each with its
218
331
  reason; the changelog lists them.
219
332
 
220
333
  See [CHANGELOG.md](https://github.com/wontopos/wontopos-node/blob/main/CHANGELOG.md).