wontopos 2.2.32 → 2.2.34
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 +18 -6
- package/dist/wontopos.d.ts +82 -34
- package/dist/wontopos.js +100 -55
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -4,10 +4,13 @@
|
|
|
4
4
|
npm install wontopos
|
|
5
5
|
```
|
|
6
6
|
|
|
7
|
+
Get an API key in the [console](https://wontopos.com). Keys look like `wos-live-...`;
|
|
8
|
+
the client also reads `WONTOPOS_API_KEY` from the environment.
|
|
9
|
+
|
|
7
10
|
```ts
|
|
8
11
|
import { Client } from "wontopos";
|
|
9
12
|
|
|
10
|
-
const mem = new Client({ apiKey: "wos-..." });
|
|
13
|
+
const mem = new Client({ apiKey: "wos-live-..." });
|
|
11
14
|
|
|
12
15
|
// Each end-user / agent / topic gets its own store — create it once.
|
|
13
16
|
// (A `default` store already exists, so you can skip this and omit the id.)
|
|
@@ -20,15 +23,15 @@ const ctx = await mem.recall("what does alice drink?", "alice");
|
|
|
20
23
|
|
|
21
24
|
## Why
|
|
22
25
|
|
|
23
|
-
- **Pure semantic retrieval** — no keyword/BM25. Identical recall in every language (
|
|
26
|
+
- **Pure semantic retrieval** — no keyword/BM25. Identical recall in every language (Korean · Japanese · Chinese · English).
|
|
24
27
|
- **No LLM in the loop** — `store` / `search` / `recall` never call a language model. You pay embeddings, not generation.
|
|
25
28
|
- **Bounded retrieval** — `recall()` returns a small, fixed-size slice (~1,200 tokens) regardless of how much you've stored.
|
|
26
29
|
|
|
27
30
|
## Methods
|
|
28
31
|
|
|
29
|
-
`add` · `addTurn` · `addBulk` · `update` · `search` · `recall` · `history` · `stats` · `get` · `listMemories` · `delete` · `deleteAll`
|
|
32
|
+
`add` · `addTurn` · `addBulk` · `update` · `search` · `searchFull` · `recall` · `history` · `stats` · `get` · `listMemories` · `delete` · `deleteAll`
|
|
30
33
|
|
|
31
|
-
|
|
34
|
+
Every method takes the store id as its second argument — `mem.add("...", "alice")`. Omit it to use the client's default store. 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.
|
|
32
35
|
|
|
33
36
|
## Who said it (speakers)
|
|
34
37
|
|
|
@@ -87,6 +90,9 @@ egress proxy this SDK could not connect at all — while the Python and Rust SDK
|
|
|
87
90
|
through, because `requests` and `reqwest` both read those variables. Pass a
|
|
88
91
|
proxy-aware `fetch` and the three behave the same:
|
|
89
92
|
|
|
93
|
+
This one needs `npm i undici` — Node's global `fetch` is built on undici, but the
|
|
94
|
+
package itself is not installed for you.
|
|
95
|
+
|
|
90
96
|
```ts
|
|
91
97
|
import { ProxyAgent } from "undici";
|
|
92
98
|
|
|
@@ -126,10 +132,17 @@ update. `search` options are typed too (`SearchOptions`: `cache_control`,
|
|
|
126
132
|
`speaker`, plus pass-through).
|
|
127
133
|
|
|
128
134
|
```ts
|
|
135
|
+
import { type Memory } from "wontopos";
|
|
136
|
+
|
|
129
137
|
const hits: Memory[] = await mem.search("what did Bob say?", "alice", 10, { speaker: "Bob" });
|
|
130
138
|
console.log(hits[0].speaker, hits[0].time_bucket); // typed: string | undefined
|
|
131
139
|
```
|
|
132
140
|
|
|
141
|
+
`hits` arrives best-first — take it in the order given. `similarity` is a raw
|
|
142
|
+
closeness score, not the ranking key: what produces the order is internal and is
|
|
143
|
+
not returned, so sorting by `similarity` overrides the ranking and makes results
|
|
144
|
+
worse. There is no `score` field.
|
|
145
|
+
|
|
133
146
|
## Errors
|
|
134
147
|
|
|
135
148
|
Any non-2xx response throws `WosError` with `status`, `message`, and — when the
|
|
@@ -162,8 +175,7 @@ const mem = new Client({ apiKey: "...", baseUrl: "https://wos.your-host.com" });
|
|
|
162
175
|
|
|
163
176
|
## Reporting a bug
|
|
164
177
|
|
|
165
|
-
Found something wrong, or something that looks unsafe? Tell us —
|
|
166
|
-
every report.
|
|
178
|
+
Found something wrong, or something that looks unsafe? Tell us — every report gets read.
|
|
167
179
|
|
|
168
180
|
- Bugs: <https://wontopos.com/contact?topic=bug>
|
|
169
181
|
- Security: <https://wontopos.com/contact?topic=security> (also published at
|
package/dist/wontopos.d.ts
CHANGED
|
@@ -84,8 +84,8 @@ export declare class RateLimitError extends WosError {
|
|
|
84
84
|
* `501` is NOT transient. It means the engine behind the model you selected does
|
|
85
85
|
* not implement that endpoint at all (the error carries `model` and `endpoint`).
|
|
86
86
|
* Retrying can never succeed — pick a model that supports it (`listModels`) or
|
|
87
|
-
* drop the call.
|
|
88
|
-
*
|
|
87
|
+
* drop the call. Treating the whole 5xx range as retryable sends a caller into a
|
|
88
|
+
* loop that cannot end.
|
|
89
89
|
*/
|
|
90
90
|
export declare class ServerError extends WosError {
|
|
91
91
|
}
|
|
@@ -99,7 +99,10 @@ export interface Memory {
|
|
|
99
99
|
content?: string;
|
|
100
100
|
/** Cognitive category, e.g. "general". */
|
|
101
101
|
category?: string;
|
|
102
|
-
/**
|
|
102
|
+
/** Raw closeness to the query — higher is closer. NOT the ranking key: results already
|
|
103
|
+
* arrive best-first, and what produces that order is internal and not returned, so
|
|
104
|
+
* re-sorting by `similarity` overrides the ranking and makes results worse. Take the
|
|
105
|
+
* list in the order given. There is no `score` field. */
|
|
103
106
|
similarity?: number;
|
|
104
107
|
/** Learned importance weight of the memory. */
|
|
105
108
|
importance?: number;
|
|
@@ -155,6 +158,13 @@ export interface StatusResult {
|
|
|
155
158
|
* shrink to it — so the extra pixels would be bytes nobody reads. This is a memory
|
|
156
159
|
* service, not a photo host: the picture it holds has the resolution of a memory.
|
|
157
160
|
*
|
|
161
|
+
* It can also come back in a different FORMAT. Downscaling means re-encoding, and
|
|
162
|
+
* we write lossless formats as WebP — send a PNG or a GIF over 1568px and `getImage`
|
|
163
|
+
* hands back `image/webp`; JPEG stays JPEG. So do not name the file from the
|
|
164
|
+
* extension you uploaded: read `contentType`. Nothing is re-encoded when the image
|
|
165
|
+
* already fits within 1568px, and a re-encode that would make the file BIGGER is
|
|
166
|
+
* thrown away and your bytes kept as they were.
|
|
167
|
+
*
|
|
158
168
|
* Keep your own copy if you need the full-resolution file. Either archive it
|
|
159
169
|
* yourself, or put its URL in `reference` — we store that string and never open it.
|
|
160
170
|
*
|
|
@@ -180,7 +190,8 @@ export interface ImageBytes {
|
|
|
180
190
|
* The image as stored. Write it to a file, or wrap it in a Blob to show it.
|
|
181
191
|
*
|
|
182
192
|
* This is the service's copy, not necessarily your upload: anything over 1568px
|
|
183
|
-
* on its long edge was downscaled on the way in
|
|
193
|
+
* on its long edge was downscaled on the way in, and re-encoded to WebP unless it
|
|
194
|
+
* was a JPEG (see `ImageInput`). Take the file extension from `contentType`.
|
|
184
195
|
*/
|
|
185
196
|
bytes: Uint8Array;
|
|
186
197
|
/** Sniffed from the BYTES, not from whatever the upload was named — e.g. `image/jpeg`. */
|
|
@@ -223,7 +234,7 @@ export interface RevisionsResult {
|
|
|
223
234
|
revised?: number;
|
|
224
235
|
/** Memories nothing has touched since they were written. Always `total - revised`. */
|
|
225
236
|
unrevised?: number;
|
|
226
|
-
/** Every memory in the store.
|
|
237
|
+
/** Every memory in the store. Internal records nobody stored directly are not counted. */
|
|
227
238
|
total?: number;
|
|
228
239
|
/** What `revised` counts, spelled out by the service. */
|
|
229
240
|
counts?: string;
|
|
@@ -272,7 +283,7 @@ export interface LineageResult {
|
|
|
272
283
|
export interface SpeakerPage {
|
|
273
284
|
speaker?: string;
|
|
274
285
|
memories: Memory[];
|
|
275
|
-
/**
|
|
286
|
+
/** How many underlying records a delete would actually remove, beyond what was returned. */
|
|
276
287
|
chunks?: number;
|
|
277
288
|
points_to_delete?: number;
|
|
278
289
|
returned?: number;
|
|
@@ -288,6 +299,25 @@ export interface UpdateResult {
|
|
|
288
299
|
status?: string;
|
|
289
300
|
[key: string]: unknown;
|
|
290
301
|
}
|
|
302
|
+
/** `searchFull` → everything one search answered with, not just the merged memories.
|
|
303
|
+
*
|
|
304
|
+
* `search` returns the memories and nothing else, which is the right answer for
|
|
305
|
+
* almost every call. Two options make it the wrong one: `max_images` asks for a lane
|
|
306
|
+
* that is delivered SEPARATELY (Tablet 2 splits photo results from text), and
|
|
307
|
+
* `verify` is reported on by `verify_used`. Merging away both means paying for a
|
|
308
|
+
* search you shaped and never seeing what came of it. */
|
|
309
|
+
export interface SearchResult {
|
|
310
|
+
/** The main lane — what others said / general memories. */
|
|
311
|
+
memories: Memory[];
|
|
312
|
+
/** The assistant's own words (speaker "me"); `[]` on a model without a self lane. */
|
|
313
|
+
self_memories: Memory[];
|
|
314
|
+
/** Image memories, when `max_images` asked for any; `[]` otherwise. */
|
|
315
|
+
images: Memory[];
|
|
316
|
+
/** Re-ask passes actually performed. Present only when `verify` was sent; lower
|
|
317
|
+
* than you asked for means the store had nothing further to add. */
|
|
318
|
+
verify_used?: number;
|
|
319
|
+
[key: string]: unknown;
|
|
320
|
+
}
|
|
291
321
|
export interface RecallResult {
|
|
292
322
|
short_term: {
|
|
293
323
|
turns: unknown[];
|
|
@@ -444,7 +474,7 @@ export interface SearchOptions {
|
|
|
444
474
|
* that need several distinct memories from far apart in the history; it does little
|
|
445
475
|
* on a single-fact lookup.
|
|
446
476
|
*
|
|
447
|
-
*
|
|
477
|
+
* Requires a re-ask-capable model. Older ones REFUSE the call (403) rather than
|
|
448
478
|
* quietly charging you for passes that never happened.
|
|
449
479
|
*/
|
|
450
480
|
verify?: number;
|
|
@@ -453,7 +483,7 @@ export interface SearchOptions {
|
|
|
453
483
|
* Out of range is refused, not clamped — silently cutting 5 to 1 would leave you
|
|
454
484
|
* believing you got five.
|
|
455
485
|
*
|
|
456
|
-
*
|
|
486
|
+
* Requires an image-capable model, and is refused (403) on one without it rather
|
|
457
487
|
* than answering with no images.
|
|
458
488
|
*/
|
|
459
489
|
max_images?: number;
|
|
@@ -570,8 +600,7 @@ export declare class Client {
|
|
|
570
600
|
withRetries(maxRetries: number): Client;
|
|
571
601
|
/** Store one memory. `metadata` optional — e.g. `{ event_date: "2026-03-01" }` for when the
|
|
572
602
|
* content actually happened. A plain date works (read as UTC midnight); a full RFC3339
|
|
573
|
-
* timestamp also works
|
|
574
|
-
* an opaque 400, which is why older examples here show the long one.
|
|
603
|
+
* timestamp also works; older examples here show the long form.
|
|
575
604
|
* `metadata.speaker`: "me" = the assistant's own words, or a person's name (up to 50 per store).
|
|
576
605
|
*
|
|
577
606
|
* `opts.idempotencyKey`: see {@link WriteOptions} — pass one to make YOUR retry
|
|
@@ -602,6 +631,21 @@ export declare class Client {
|
|
|
602
631
|
* speaker "me"), kept separate so the reader never confuses who said what. On a model
|
|
603
632
|
* without a self lane, `self_memories` is `[]`. `userId` optional (default store). */
|
|
604
633
|
searchSelf(query: string, userId?: string, limit?: number, opts?: SearchOptions): Promise<SelfSearchResult>;
|
|
634
|
+
/**
|
|
635
|
+
* Search, and keep every lane the answer came in.
|
|
636
|
+
*
|
|
637
|
+
* Same request as {@link Client.search} — reach for this one when the options make
|
|
638
|
+
* the merged array an incomplete answer:
|
|
639
|
+
*
|
|
640
|
+
* const r = await mem.searchFull("the day we moved", "alice", 10,
|
|
641
|
+
* { max_images: 3, verify: 2 });
|
|
642
|
+
* r.images.length; // the photo lane, which `search` drops
|
|
643
|
+
* r.verify_used; // how many re-ask passes actually ran (you are billed per pass)
|
|
644
|
+
*
|
|
645
|
+
* Both options need Tablet 2 or newer and are refused (403) on an older engine
|
|
646
|
+
* rather than accepted and ignored.
|
|
647
|
+
*/
|
|
648
|
+
searchFull(query: string, userId?: string, limit?: number, opts?: SearchOptions): Promise<SearchResult>;
|
|
605
649
|
/** One-call LLM context: short-term turns + long-term matches + surrounding context.
|
|
606
650
|
* `form` ("memoir"/"archive", Scroll 1.2+) renders each long-term memory's time in
|
|
607
651
|
* that form; `tz` is your UTC-offset hours for that rendering. */
|
|
@@ -612,7 +656,7 @@ export declare class Client {
|
|
|
612
656
|
* Long-term memories to recall, 5–20. Default 10. Out of range is refused, not
|
|
613
657
|
* clamped — asking for 20 and silently getting 10 reads as "that is all there is".
|
|
614
658
|
*
|
|
615
|
-
*
|
|
659
|
+
* Needs a limit-aware model. An older one recalls a fixed ten no matter what you
|
|
616
660
|
* send, so the API refuses the call (403) rather than answering with a number you
|
|
617
661
|
* did not ask for.
|
|
618
662
|
*/
|
|
@@ -665,18 +709,24 @@ export declare class Client {
|
|
|
665
709
|
/**
|
|
666
710
|
* Fetch the bytes of an image memory — the picture the SERVICE holds.
|
|
667
711
|
*
|
|
668
|
-
* Not necessarily your upload: an image whose long edge was
|
|
669
|
-
* downscaled to 1568 on the way in
|
|
670
|
-
*
|
|
671
|
-
*
|
|
672
|
-
*
|
|
673
|
-
*
|
|
674
|
-
*
|
|
675
|
-
*
|
|
676
|
-
*
|
|
712
|
+
* Not necessarily your upload, in size OR in format: an image whose long edge was
|
|
713
|
+
* over 1568px was downscaled to 1568 on the way in and re-encoded — lossless
|
|
714
|
+
* formats as WebP, so a PNG comes back as `image/webp`; JPEG stays JPEG — and that
|
|
715
|
+
* smaller picture is what was embedded, stored and comes back here. Nothing in the
|
|
716
|
+
* retrieval path looks above 1568, so the extra pixels would be bytes nobody reads.
|
|
717
|
+
* Keep your own copy for the full-resolution file.
|
|
718
|
+
*
|
|
719
|
+
* The type is sniffed from the bytes themselves rather than from whatever the file
|
|
720
|
+
* was called, so name the file from `contentType` rather than from what you sent.
|
|
721
|
+
* When the format changed, the response also carries `x-wos-image-converted-from`
|
|
722
|
+
* naming what you uploaded. Rejects with `NotFoundError` when this memory has no
|
|
723
|
+
* image, or when the service keeps no image bytes at all — it says "no" instead of
|
|
724
|
+
* handing back something empty, so "a memory with no image" never looks the same as
|
|
725
|
+
* "an image we lost".
|
|
677
726
|
*
|
|
678
727
|
* const { bytes, contentType } = await mem.getImage(undefined, id);
|
|
679
|
-
*
|
|
728
|
+
* const ext = contentType.split("/")[1]; // "webp" for a downscaled PNG
|
|
729
|
+
* await fs.writeFile(`image.${ext}`, bytes);
|
|
680
730
|
*/
|
|
681
731
|
getImage(userId: string | undefined, memoryId: string): Promise<ImageBytes>;
|
|
682
732
|
/**
|
|
@@ -721,10 +771,10 @@ export declare class Client {
|
|
|
721
771
|
*
|
|
722
772
|
* Counts memories a transform touched (supersede, update, retract, image removed).
|
|
723
773
|
* Deletions are NOT counted: a deleted memory leaves nothing to count. Neither are
|
|
724
|
-
*
|
|
725
|
-
* do not belong in a ratio that answers "how much of MY memory changed".
|
|
774
|
+
* the internal records derived from what you stored — nobody stored those directly,
|
|
775
|
+
* so they do not belong in a ratio that answers "how much of MY memory changed".
|
|
726
776
|
*
|
|
727
|
-
*
|
|
777
|
+
* By default this returns COUNTS ONLY, and the answer is the same size for a store
|
|
728
778
|
* of a hundred memories and a store of a hundred million. That is deliberate: a model
|
|
729
779
|
* asks this mid-conversation, and an answer that grew with the store would be
|
|
730
780
|
* unusable for exactly the customers who most need to ask.
|
|
@@ -745,10 +795,10 @@ export declare class Client {
|
|
|
745
795
|
* }
|
|
746
796
|
* ```
|
|
747
797
|
*
|
|
748
|
-
*
|
|
798
|
+
* Pages are ordered by when each memory was STORED, not by when it was edited.
|
|
749
799
|
* The response says so in `ordered_by`.
|
|
750
800
|
*
|
|
751
|
-
*
|
|
801
|
+
* Served from `/api/v1/won/*`, not `/api/v1/memory/*`. Won is the surface for calls
|
|
752
802
|
* a model makes ABOUT its memory rather than calls an application makes WITH it, and
|
|
753
803
|
* the address says so. The old path still answers, for clients published before
|
|
754
804
|
* 2026-08-18, and both share one rate-limit budget.
|
|
@@ -799,10 +849,9 @@ export declare class Client {
|
|
|
799
849
|
/**
|
|
800
850
|
* The engrams (and delivery forms) the selected model can actually run.
|
|
801
851
|
*
|
|
802
|
-
*
|
|
803
|
-
*
|
|
804
|
-
*
|
|
805
|
-
* and stopped hard-coding for the same reason. The service is the authority: what
|
|
852
|
+
* Ask rather than hard-code: a name copied from the docs freezes a caller to the
|
|
853
|
+
* catalogue as it was that day, and anything added later stays invisible. The
|
|
854
|
+
* service is the authority. What
|
|
806
855
|
* comes back depends on the model (delivery forms need Scroll 1.2+), so pass
|
|
807
856
|
* `withModel()` if you want another model's catalogue.
|
|
808
857
|
*
|
|
@@ -856,10 +905,9 @@ export declare class Client {
|
|
|
856
905
|
private readCapped;
|
|
857
906
|
/** The same cap, for a body that is not text.
|
|
858
907
|
*
|
|
859
|
-
* `
|
|
860
|
-
*
|
|
861
|
-
*
|
|
862
|
-
* megabytes was not. One implementation, two callers, so they cannot drift. */
|
|
908
|
+
* `arrayBuffer()` would buffer whatever arrives. The cap exists for a hostile or
|
|
909
|
+
* broken `baseUrl`, and an endpoint that returns megabytes is where that costs the
|
|
910
|
+
* most. One implementation, two callers, so they cannot drift. */
|
|
863
911
|
private readCappedBytes;
|
|
864
912
|
private request;
|
|
865
913
|
}
|
package/dist/wontopos.js
CHANGED
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
* runtime's (never disabled by this SDK; Node 18+ floors TLS at 1.2). Prefer
|
|
38
38
|
* `Client.fromEnv()` over keys in source code.
|
|
39
39
|
*/
|
|
40
|
-
const VERSION = "2.2.
|
|
40
|
+
const VERSION = "2.2.34";
|
|
41
41
|
/** Runtime info helps support debug a report ("node 18 on Windows...") —
|
|
42
42
|
* platform only, never anything identifying. Browsers have no `process` (and
|
|
43
43
|
* silently drop the UA header anyway). */
|
|
@@ -55,7 +55,14 @@ function logDebug(msg) {
|
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
57
|
const DEFAULT_BASE_URL = "https://api.wontopos.com";
|
|
58
|
-
|
|
58
|
+
/** The engine every call uses unless the caller names another.
|
|
59
|
+
*
|
|
60
|
+
* Tablet 2 costs the same per token as Tablet 1 and is the only released line that
|
|
61
|
+
* can serve images, re-ask passes (`verify`), and the self lane, so a caller who
|
|
62
|
+
* names nothing gets the engine that can answer the most. All models read the same
|
|
63
|
+
* memory, so switching is a header, not a migration. Pin an older one explicitly
|
|
64
|
+
* with `new Client({ apiKey, model: "tablet-1" })` or `withModel("tablet-1")`. */
|
|
65
|
+
const DEFAULT_MODEL = "tablet-2";
|
|
59
66
|
/** 429 = rate-limited BEFORE processing → always safe to retry (no write, no bill). */
|
|
60
67
|
const RETRY_ALWAYS = new Set([429]);
|
|
61
68
|
/** 502/503 are ambiguous for a write (gateway may return them AFTER the backend
|
|
@@ -96,8 +103,9 @@ function normalizeStoreId(id) {
|
|
|
96
103
|
* Bounded on purpose. The warning fires on ids that fold, and the shape it exists
|
|
97
104
|
* to catch is an email — which this SDK's own documented pattern ("one store per
|
|
98
105
|
* end user") turns into one entry PER USER, held for the life of the process and
|
|
99
|
-
* never released, even when the client is. A server
|
|
100
|
-
* this to 50,000 entries,
|
|
106
|
+
* never released, even when the client is. A server that forwards 50,000 distinct user
|
|
107
|
+
* ids would grow this to 50,000 entries, leaking in exactly the case the warning
|
|
108
|
+
* exists to catch. A `Set`
|
|
101
109
|
* keeps insertion order, so the oldest entry is the one evicted; dropping the
|
|
102
110
|
* oldest rather than refusing new ones means a collision that first shows up late
|
|
103
111
|
* still gets its one warning. */
|
|
@@ -151,16 +159,15 @@ function normalizeImage(image) {
|
|
|
151
159
|
if (typeof image.data !== "string" || image.data.trim() === "") {
|
|
152
160
|
throw new Error("image.data is required — base64 of the image (a data: URL is fine).");
|
|
153
161
|
}
|
|
154
|
-
//
|
|
155
|
-
//
|
|
156
|
-
//
|
|
157
|
-
//
|
|
162
|
+
// Trim before looking for the prefix. Checked against the raw string, one leading
|
|
163
|
+
// space (` data:image/png;base64,…`) hides the prefix, and the literal
|
|
164
|
+
// `data:image/png;base64,` travels as part of the base64. The engine answers 400
|
|
165
|
+
// "image could not be read" and the caller has no way to tell why.
|
|
158
166
|
const raw = image.data.trim();
|
|
159
167
|
const comma = raw.startsWith("data:") ? raw.indexOf(",") : -1;
|
|
160
|
-
//
|
|
161
|
-
//
|
|
162
|
-
//
|
|
163
|
-
// 전부 빼는 게 안전하다.
|
|
168
|
+
// Strip whitespace in the middle as well. `base64` and `openssl base64` wrap at 76
|
|
169
|
+
// columns, and trimming only the ends leaves those line breaks in the payload. The
|
|
170
|
+
// base64 alphabet contains no whitespace, so removing all of it is safe.
|
|
164
171
|
const data = (comma === -1 ? raw : raw.slice(comma + 1)).replace(/\s+/g, "");
|
|
165
172
|
if (data === "")
|
|
166
173
|
throw new Error("image.data is empty after stripping its data: prefix.");
|
|
@@ -309,8 +316,8 @@ export class RateLimitError extends WosError {
|
|
|
309
316
|
* `501` is NOT transient. It means the engine behind the model you selected does
|
|
310
317
|
* not implement that endpoint at all (the error carries `model` and `endpoint`).
|
|
311
318
|
* Retrying can never succeed — pick a model that supports it (`listModels`) or
|
|
312
|
-
* drop the call.
|
|
313
|
-
*
|
|
319
|
+
* drop the call. Treating the whole 5xx range as retryable sends a caller into a
|
|
320
|
+
* loop that cannot end.
|
|
314
321
|
*/
|
|
315
322
|
export class ServerError extends WosError {
|
|
316
323
|
}
|
|
@@ -545,8 +552,7 @@ export class Client {
|
|
|
545
552
|
// store (set via `new Client({ apiKey, userId })` or `withUser`).
|
|
546
553
|
/** Store one memory. `metadata` optional — e.g. `{ event_date: "2026-03-01" }` for when the
|
|
547
554
|
* content actually happened. A plain date works (read as UTC midnight); a full RFC3339
|
|
548
|
-
* timestamp also works
|
|
549
|
-
* an opaque 400, which is why older examples here show the long one.
|
|
555
|
+
* timestamp also works; older examples here show the long form.
|
|
550
556
|
* `metadata.speaker`: "me" = the assistant's own words, or a person's name (up to 50 per store).
|
|
551
557
|
*
|
|
552
558
|
* `opts.idempotencyKey`: see {@link WriteOptions} — pass one to make YOUR retry
|
|
@@ -604,6 +610,32 @@ export class Client {
|
|
|
604
610
|
const r = await this.post("/api/v1/memory/search", { ...opts, user_id: this.uid(userId), query, max_results: limit });
|
|
605
611
|
return { memories: asRecords(r.memories), self_memories: asRecords(r.self_memories) };
|
|
606
612
|
}
|
|
613
|
+
/**
|
|
614
|
+
* Search, and keep every lane the answer came in.
|
|
615
|
+
*
|
|
616
|
+
* Same request as {@link Client.search} — reach for this one when the options make
|
|
617
|
+
* the merged array an incomplete answer:
|
|
618
|
+
*
|
|
619
|
+
* const r = await mem.searchFull("the day we moved", "alice", 10,
|
|
620
|
+
* { max_images: 3, verify: 2 });
|
|
621
|
+
* r.images.length; // the photo lane, which `search` drops
|
|
622
|
+
* r.verify_used; // how many re-ask passes actually ran (you are billed per pass)
|
|
623
|
+
*
|
|
624
|
+
* Both options need Tablet 2 or newer and are refused (403) on an older engine
|
|
625
|
+
* rather than accepted and ignored.
|
|
626
|
+
*/
|
|
627
|
+
async searchFull(query, userId, limit = 10, opts = {}) {
|
|
628
|
+
warnOnUnknownFilters(opts.filters);
|
|
629
|
+
const r = await this.post("/api/v1/memory/search", { ...opts, user_id: this.uid(userId), query, max_results: limit });
|
|
630
|
+
// Spread first so a lane added later still arrives; the known ones are then
|
|
631
|
+
// normalized, because a broken proxy can null any of them.
|
|
632
|
+
return {
|
|
633
|
+
...r,
|
|
634
|
+
memories: asRecords(r.memories),
|
|
635
|
+
self_memories: asRecords(r.self_memories),
|
|
636
|
+
images: asRecords(r.images),
|
|
637
|
+
};
|
|
638
|
+
}
|
|
607
639
|
/** One-call LLM context: short-term turns + long-term matches + surrounding context.
|
|
608
640
|
* `form` ("memoir"/"archive", Scroll 1.2+) renders each long-term memory's time in
|
|
609
641
|
* that form; `tz` is your UTC-offset hours for that rendering. */
|
|
@@ -691,18 +723,24 @@ export class Client {
|
|
|
691
723
|
/**
|
|
692
724
|
* Fetch the bytes of an image memory — the picture the SERVICE holds.
|
|
693
725
|
*
|
|
694
|
-
* Not necessarily your upload: an image whose long edge was
|
|
695
|
-
* downscaled to 1568 on the way in
|
|
696
|
-
*
|
|
697
|
-
*
|
|
698
|
-
*
|
|
699
|
-
*
|
|
700
|
-
*
|
|
701
|
-
*
|
|
702
|
-
*
|
|
726
|
+
* Not necessarily your upload, in size OR in format: an image whose long edge was
|
|
727
|
+
* over 1568px was downscaled to 1568 on the way in and re-encoded — lossless
|
|
728
|
+
* formats as WebP, so a PNG comes back as `image/webp`; JPEG stays JPEG — and that
|
|
729
|
+
* smaller picture is what was embedded, stored and comes back here. Nothing in the
|
|
730
|
+
* retrieval path looks above 1568, so the extra pixels would be bytes nobody reads.
|
|
731
|
+
* Keep your own copy for the full-resolution file.
|
|
732
|
+
*
|
|
733
|
+
* The type is sniffed from the bytes themselves rather than from whatever the file
|
|
734
|
+
* was called, so name the file from `contentType` rather than from what you sent.
|
|
735
|
+
* When the format changed, the response also carries `x-wos-image-converted-from`
|
|
736
|
+
* naming what you uploaded. Rejects with `NotFoundError` when this memory has no
|
|
737
|
+
* image, or when the service keeps no image bytes at all — it says "no" instead of
|
|
738
|
+
* handing back something empty, so "a memory with no image" never looks the same as
|
|
739
|
+
* "an image we lost".
|
|
703
740
|
*
|
|
704
741
|
* const { bytes, contentType } = await mem.getImage(undefined, id);
|
|
705
|
-
*
|
|
742
|
+
* const ext = contentType.split("/")[1]; // "webp" for a downscaled PNG
|
|
743
|
+
* await fs.writeFile(`image.${ext}`, bytes);
|
|
706
744
|
*/
|
|
707
745
|
getImage(userId, memoryId) {
|
|
708
746
|
if (!memoryId || typeof memoryId !== "string") {
|
|
@@ -753,12 +791,23 @@ export class Client {
|
|
|
753
791
|
async *iterImages(userId, opts = {}) {
|
|
754
792
|
let before;
|
|
755
793
|
let skipIds;
|
|
794
|
+
// Same repeat-cursor guard as `iterMemories`. A server that hands back the
|
|
795
|
+
// cursor it was just given would otherwise re-yield one page MAX_PAGES times,
|
|
796
|
+
// and the caller reads those repeats as more images. The page cap alone bounds
|
|
797
|
+
// the walk; it does not stop the duplicates.
|
|
798
|
+
const seen = new Set();
|
|
756
799
|
for (let page = 0; page < MAX_PAGES; page++) {
|
|
757
800
|
const p = await this.listImages(userId, { limit: opts.pageSize, before, skipIds });
|
|
758
801
|
for (const m of asRecords(p.images))
|
|
759
802
|
yield m;
|
|
760
803
|
if (!p.has_more || !p.next_before)
|
|
761
804
|
break;
|
|
805
|
+
// The cursor is the PAIR: several images can share a timestamp, so `before`
|
|
806
|
+
// alone repeats across pages legitimately.
|
|
807
|
+
const key = `${p.next_before}|${(Array.isArray(p.next_skip_ids) ? p.next_skip_ids : []).join(",")}`;
|
|
808
|
+
if (seen.has(key))
|
|
809
|
+
break;
|
|
810
|
+
seen.add(key);
|
|
762
811
|
before = p.next_before;
|
|
763
812
|
skipIds = Array.isArray(p.next_skip_ids) ? p.next_skip_ids : undefined;
|
|
764
813
|
}
|
|
@@ -774,10 +823,10 @@ export class Client {
|
|
|
774
823
|
*
|
|
775
824
|
* Counts memories a transform touched (supersede, update, retract, image removed).
|
|
776
825
|
* Deletions are NOT counted: a deleted memory leaves nothing to count. Neither are
|
|
777
|
-
*
|
|
778
|
-
* do not belong in a ratio that answers "how much of MY memory changed".
|
|
826
|
+
* the internal records derived from what you stored — nobody stored those directly,
|
|
827
|
+
* so they do not belong in a ratio that answers "how much of MY memory changed".
|
|
779
828
|
*
|
|
780
|
-
*
|
|
829
|
+
* By default this returns COUNTS ONLY, and the answer is the same size for a store
|
|
781
830
|
* of a hundred memories and a store of a hundred million. That is deliberate: a model
|
|
782
831
|
* asks this mid-conversation, and an answer that grew with the store would be
|
|
783
832
|
* unusable for exactly the customers who most need to ask.
|
|
@@ -798,10 +847,10 @@ export class Client {
|
|
|
798
847
|
* }
|
|
799
848
|
* ```
|
|
800
849
|
*
|
|
801
|
-
*
|
|
850
|
+
* Pages are ordered by when each memory was STORED, not by when it was edited.
|
|
802
851
|
* The response says so in `ordered_by`.
|
|
803
852
|
*
|
|
804
|
-
*
|
|
853
|
+
* Served from `/api/v1/won/*`, not `/api/v1/memory/*`. Won is the surface for calls
|
|
805
854
|
* a model makes ABOUT its memory rather than calls an application makes WITH it, and
|
|
806
855
|
* the address says so. The old path still answers, for clients published before
|
|
807
856
|
* 2026-08-18, and both share one rate-limit budget.
|
|
@@ -875,10 +924,9 @@ export class Client {
|
|
|
875
924
|
/**
|
|
876
925
|
* The engrams (and delivery forms) the selected model can actually run.
|
|
877
926
|
*
|
|
878
|
-
*
|
|
879
|
-
*
|
|
880
|
-
*
|
|
881
|
-
* and stopped hard-coding for the same reason. The service is the authority: what
|
|
927
|
+
* Ask rather than hard-code: a name copied from the docs freezes a caller to the
|
|
928
|
+
* catalogue as it was that day, and anything added later stays invisible. The
|
|
929
|
+
* service is the authority. What
|
|
882
930
|
* comes back depends on the model (delivery forms need Scroll 1.2+), so pass
|
|
883
931
|
* `withModel()` if you want another model's catalogue.
|
|
884
932
|
*
|
|
@@ -909,9 +957,9 @@ export class Client {
|
|
|
909
957
|
}
|
|
910
958
|
/** Delete a store and ALL its memories. Returns `{ user_id, status }`. */
|
|
911
959
|
deleteStore(userId) {
|
|
912
|
-
// Validate
|
|
913
|
-
//
|
|
914
|
-
//
|
|
960
|
+
// Validate before warning. The warning helper lowercases the id, so a non-string
|
|
961
|
+
// reaches it and dies as "id.toLowerCase is not a function" — a TypeError from
|
|
962
|
+
// inside the SDK instead of a sentence about the id.
|
|
915
963
|
if (typeof userId !== "string" || !userId.trim())
|
|
916
964
|
throw new Error("userId is required (a non-blank string) — deleteStore never falls back to the default store.");
|
|
917
965
|
warnIfStoreIdCollapses(userId); // destructive: the collision warning belongs here too
|
|
@@ -939,11 +987,10 @@ export class Client {
|
|
|
939
987
|
// Guard: without a memory_id the API's forget endpoint means "delete the
|
|
940
988
|
// whole store". An undefined/"" slipping in here must never become a wipe.
|
|
941
989
|
//
|
|
942
|
-
// `.trim()` matters as much as the emptiness check: " " is truthy, so it
|
|
943
|
-
//
|
|
944
|
-
// reads the request as the whole-store form
|
|
945
|
-
// this SDK to be wrong.
|
|
946
|
-
// was the one that did not.
|
|
990
|
+
// `.trim()` matters as much as the emptiness check: " " is truthy, so without it
|
|
991
|
+
// a blank string passes the guard and travels as memory_id. A server that trims it
|
|
992
|
+
// back to nothing reads the request as the whole-store form, which is the most
|
|
993
|
+
// destructive way for this SDK to be wrong.
|
|
947
994
|
if (typeof memoryId !== "string" || !memoryId.trim()) {
|
|
948
995
|
throw new Error("memoryId is required (non-blank). To delete every memory in a store, call deleteAll(userId) explicitly.");
|
|
949
996
|
}
|
|
@@ -1063,10 +1110,9 @@ export class Client {
|
|
|
1063
1110
|
}
|
|
1064
1111
|
/** The same cap, for a body that is not text.
|
|
1065
1112
|
*
|
|
1066
|
-
* `
|
|
1067
|
-
*
|
|
1068
|
-
*
|
|
1069
|
-
* megabytes was not. One implementation, two callers, so they cannot drift. */
|
|
1113
|
+
* `arrayBuffer()` would buffer whatever arrives. The cap exists for a hostile or
|
|
1114
|
+
* broken `baseUrl`, and an endpoint that returns megabytes is where that costs the
|
|
1115
|
+
* most. One implementation, two callers, so they cannot drift. */
|
|
1070
1116
|
async readCappedBytes(res) {
|
|
1071
1117
|
const cl = res.headers.get("content-length");
|
|
1072
1118
|
if (cl && Number(cl) > MAX_RESPONSE_BYTES) {
|
|
@@ -1203,10 +1249,9 @@ export class Client {
|
|
|
1203
1249
|
msg = msg.slice(0, MAX_ERR_MSG) + "…(truncated)";
|
|
1204
1250
|
throw errorFor(res.status, msg, requestId);
|
|
1205
1251
|
}
|
|
1206
|
-
// An empty body is only legal when the STATUS says there is no body.
|
|
1207
|
-
//
|
|
1208
|
-
//
|
|
1209
|
-
// and Rust SDKs raised on the same response. Same rule in all three now.
|
|
1252
|
+
// An empty body is only legal when the STATUS says there is no body. Accepting
|
|
1253
|
+
// any empty 2xx would make an `add()` answered by a truncating proxy return `{}`,
|
|
1254
|
+
// which reads as a successful write with no id. All three SDKs apply this rule.
|
|
1210
1255
|
if (!text) {
|
|
1211
1256
|
if (NO_BODY_STATUS.has(res.status))
|
|
1212
1257
|
return {};
|
|
@@ -1228,10 +1273,10 @@ export class Client {
|
|
|
1228
1273
|
const got = data === null ? "null" : Array.isArray(data) ? "array" : typeof data;
|
|
1229
1274
|
throw new WosError(res.status, `expected a JSON object in the response, got ${got}`);
|
|
1230
1275
|
}
|
|
1231
|
-
//
|
|
1232
|
-
//
|
|
1233
|
-
//
|
|
1234
|
-
//
|
|
1276
|
+
// Surface whether the write was stored or replayed. The server says so with
|
|
1277
|
+
// `Idempotent-Replayed: true`, and it answers the question someone using an
|
|
1278
|
+
// idempotency key is asking: did my retry write, or is this the first response
|
|
1279
|
+
// coming back again?
|
|
1235
1280
|
if (res.headers.get("Idempotent-Replayed") === "true") {
|
|
1236
1281
|
data.replayed = true;
|
|
1237
1282
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wontopos",
|
|
3
|
-
"version": "2.2.
|
|
4
|
-
"description": "Wontopos
|
|
3
|
+
"version": "2.2.34",
|
|
4
|
+
"description": "Wontopos \u2014 long-term memory for AI agents. Pure semantic retrieval, identical recall in every language, ~100\u00d7 lower LLM bill.",
|
|
5
|
+
"repository": "github:wontopos/wontopos-typescript",
|
|
5
6
|
"keywords": [
|
|
6
7
|
"memory",
|
|
7
8
|
"ai",
|
|
@@ -37,7 +38,7 @@
|
|
|
37
38
|
},
|
|
38
39
|
"scripts": {
|
|
39
40
|
"build": "tsc",
|
|
40
|
-
"test": "npm run build && node --test
|
|
41
|
+
"test": "npm run build && node --test tests/client.test.mjs",
|
|
41
42
|
"prepublishOnly": "npm run build"
|
|
42
43
|
},
|
|
43
44
|
"devDependencies": {
|